Chapter 7 Applications
7.1 1 Ether Wallet
一个基本钱包的例子。
任何人都可以发送ETH。
只有所有者可以退出。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract EtherWallet {
address payable public owner;
constructor() {
owner = payable(msg.sender);
}
receive() external payable {}
function withdraw(uint256 _amount) external {
require(msg.sender == owner, "caller is not owner");
payable(msg.sender).transfer(_amount);
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
7.2 2 多签名钱包
让我们创建一个多签名钱包。这是规格说明。
钱包的主人可以 1. 提交交易 2. 批准和撤销对未决交易的批准 3. 在获得足够多的所有者批准后,任何人都可以执行交易。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract MultiSigWallet {
event Deposit(address indexed sender, uint256 amount, uint256 balance);
event SubmitTransaction(
address indexed owner,
uint256 indexed txIndex,
address indexed to,
uint256 value,
bytes data
);
event ConfirmTransaction(address indexed owner, uint256 indexed txIndex);
event RevokeConfirmation(address indexed owner, uint256 indexed txIndex);
event ExecuteTransaction(address indexed owner, uint256 indexed txIndex);
address[] public owners;
mapping(address => bool) public isOwner;
uint256 public numConfirmationsRequired;
struct Transaction {
address to;
uint256 value;
bytes data;
bool executed;
uint256 numConfirmations;
}
// mapping from tx index => owner => bool
mapping(uint256 => mapping(address => bool)) public isConfirmed;
Transaction[] public transactions;
modifier onlyOwner() {
require(isOwner[msg.sender], "not owner");
_;
}
modifier txExists(uint256 _txIndex) {
require(_txIndex < transactions.length, "tx does not exist");
_;
}
modifier notExecuted(uint256 _txIndex) {
require(!transactions[_txIndex].executed, "tx already executed");
_;
}
modifier notConfirmed(uint256 _txIndex) {
require(!isConfirmed[_txIndex][msg.sender], "tx already confirmed");
_;
}
constructor(address[] memory _owners, uint256 _numConfirmationsRequired) {
require(_owners.length > 0, "owners required");
require(
_numConfirmationsRequired > 0
&& _numConfirmationsRequired <= _owners.length,
"invalid number of required confirmations"
);
for (uint256 i = 0; i < _owners.length; i++) {
address owner = _owners[i];
require(owner != address(0), "invalid owner");
require(!isOwner[owner], "owner not unique");
isOwner[owner] = true;
owners.push(owner);
}
numConfirmationsRequired = _numConfirmationsRequired;
}
receive() external payable {
emit Deposit(msg.sender, msg.value, address(this).balance);
}
function submitTransaction(address _to, uint256 _value, bytes memory _data)
public
onlyOwner
{
uint256 txIndex = transactions.length;
transactions.push(
Transaction({
to: _to,
value: _value,
data: _data,
executed: false,
numConfirmations: 0
})
);
emit SubmitTransaction(msg.sender, txIndex, _to, _value, _data);
}
function confirmTransaction(uint256 _txIndex)
public
onlyOwner
txExists(_txIndex)
notExecuted(_txIndex)
notConfirmed(_txIndex)
{
Transaction storage transaction = transactions[_txIndex];
transaction.numConfirmations += 1;
isConfirmed[_txIndex][msg.sender] = true;
emit ConfirmTransaction(msg.sender, _txIndex);
}
function executeTransaction(uint256 _txIndex)
public
onlyOwner
txExists(_txIndex)
notExecuted(_txIndex)
{
Transaction storage transaction = transactions[_txIndex];
require(
transaction.numConfirmations >= numConfirmationsRequired,
"cannot execute tx"
);
transaction.executed = true;
(bool success,) =
transaction.to.call{value: transaction.value}(transaction.data);
require(success, "tx failed");
emit ExecuteTransaction(msg.sender, _txIndex);
}
function revokeConfirmation(uint256 _txIndex)
public
onlyOwner
txExists(_txIndex)
notExecuted(_txIndex)
{
Transaction storage transaction = transactions[_txIndex];
require(isConfirmed[_txIndex][msg.sender], "tx not confirmed");
transaction.numConfirmations -= 1;
isConfirmed[_txIndex][msg.sender] = false;
emit RevokeConfirmation(msg.sender, _txIndex);
}
function getOwners() public view returns (address[] memory) {
return owners;
}
function getTransactionCount() public view returns (uint256) {
return transactions.length;
}
function getTransaction(uint256 _txIndex)
public
view
returns (
address to,
uint256 value,
bytes memory data,
bool executed,
uint256 numConfirmations
)
{
Transaction storage transaction = transactions[_txIndex];
return (
transaction.to,
transaction.value,
transaction.data,
transaction.executed,
transaction.numConfirmations
);
}
}
7.3 3 Merkle树
默克尔树允许以加密方式证明包含某个元素
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract MerkleProof {
function verify(
bytes32[] memory proof,
bytes32 root,
bytes32 leaf,
uint256 index
) public pure returns (bool) {
bytes32 hash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
bytes32 proofElement = proof[i];
if (index % 2 == 0) {
hash = keccak256(abi.encodePacked(hash, proofElement));
} else {
hash = keccak256(abi.encodePacked(proofElement, hash));
}
index = index / 2;
}
return hash == root;
}
}
contract TestMerkleProof is MerkleProof {
bytes32[] public hashes;
constructor() {
string[4] memory transactions =
["alice -> bob", "bob -> dave", "carol -> alice", "dave -> bob"];
for (uint256 i = 0; i < transactions.length; i++) {
hashes.push(keccak256(abi.encodePacked(transactions[i])));
}
uint256 n = transactions.length;
uint256 offset = 0;
while (n > 0) {
for (uint256 i = 0; i < n - 1; i += 2) {
hashes.push(
keccak256(
abi.encodePacked(
hashes[offset + i], hashes[offset + i + 1]
)
)
);
}
offset += n;
n = n / 2;
}
}
function getRoot() public view returns (bytes32) {
return hashes[hashes.length - 1];
}
/* verify
3rd leaf
0xdca3326ad7e8121bf9cf9c12333e6b2271abe823ec9edfe42f813b1e768fa57b
root
0xcc086fcc038189b4641db2cc4f1de3bb132aefbd65d510d817591550937818c7
index
2
proof
0x8da9e1c820f9dbd1589fd6585872bc1063588625729e7ab0797cfc63a00bd950
0x995788ffc103b987ad50f5e5707fd094419eb12d9552cc423bd0cd86a3861433
*/
}
7.4 4 Iterable映射
下面是一个如何创建可迭代映射的示例。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
library IterableMapping {
// Iterable mapping from address to uint;
struct Map {
address[] keys;
mapping(address => uint256) values;
mapping(address => uint256) indexOf;
mapping(address => bool) inserted;
}
function get(Map storage map, address key) public view returns (uint256) {
return map.values[key];
}
function getKeyAtIndex(Map storage map, uint256 index)
public
view
returns (address)
{
return map.keys[index];
}
function size(Map storage map) public view returns (uint256) {
return map.keys.length;
}
function set(Map storage map, address key, uint256 val) public {
if (map.inserted[key]) {
map.values[key] = val;
} else {
map.inserted[key] = true;
map.values[key] = val;
map.indexOf[key] = map.keys.length;
map.keys.push(key);
}
}
function remove(Map storage map, address key) public {
if (!map.inserted[key]) {
return;
}
delete map.inserted[key];
delete map.values[key];
uint256 index = map.indexOf[key];
address lastKey = map.keys[map.keys.length - 1];
map.indexOf[lastKey] = index;
delete map.indexOf[key];
map.keys[index] = lastKey;
map.keys.pop();
}
}
contract TestIterableMap {
using IterableMapping for IterableMapping.Map;
IterableMapping.Map private map;
function testIterableMap() public {
map.set(address(0), 0);
map.set(address(1), 100);
map.set(address(2), 200); // insert
map.set(address(2), 200); // update
map.set(address(3), 300);
for (uint256 i = 0; i < map.size(); i++) {
address key = map.getKeyAtIndex(i);
assert(map.get(key) == i * 100);
}
map.remove(address(1));
// keys = [address(0), address(3), address(2)]
assert(map.size() == 3);
assert(map.getKeyAtIndex(0) == address(0));
assert(map.getKeyAtIndex(1) == address(3));
assert(map.getKeyAtIndex(2) == address(2));
}
}
7.5 5 ERC20 标准
7.5.1 ERC20 标准
任何遵循 ERC20 标准的合约都是 ERC20 代币。
ERC20 代币提供以下功能:
- 转账代币
- 允许他人代表代币持有者转账
以下是 ERC20 的接口:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
}
7.5.2 ERC20 代币合约示例
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "./IERC20.sol";
contract ERC20 is IERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
string public name;
string public symbol;
uint8 public decimals;
constructor(string memory _name, string memory _symbol, uint8 _decimals) {
name = _name;
symbol = _symbol;
decimals = _decimals;
}
function transfer(address recipient, uint256 amount) external returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(msg.sender, recipient, amount);
return true;
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) {
allowance[sender][msg.sender] -= amount;
balanceOf[sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(sender, recipient, amount);
return true;
}
function _mint(address to, uint256 amount) internal {
balanceOf[to] += amount;
totalSupply += amount;
emit Transfer(address(0), to, amount);
}
function _burn(address from, uint256 amount) internal {
balanceOf[from] -= amount;
totalSupply -= amount;
emit Transfer(from, address(0), amount);
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function burn(address from, uint256 amount) external {
_burn(from, amount);
}
}
7.5.3 创建您自己的 ERC20 代币
使用 OpenZeppelin 创建自己的 ERC20 代币非常简单。
以下是一个示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "./ERC20.sol";
contract MyToken is ERC20 {
constructor(string memory name, string memory symbol, uint8 decimals)
ERC20(name, symbol, decimals)
{
// 向 msg.sender 发行 100 个代币
// 类似于
// 1 美元 = 100 分
// 1 代币 = 1 * (10 ** decimals)
_mint(msg.sender, 100 * 10 ** uint256(decimals));
}
}
7.5.4 代币交换合约
以下是一个示例合约 TokenSwap,用于交换一种 ERC20 代币为另一种。
该合约将通过调用 transferFrom(address sender, address recipient, uint256 amount) 来交换代币,该调用会将代币从发送者转移到接收者。
为了确保 transferFrom 成功,发送者必须:
- 在其余额中拥有超过交换金额的代币
- 通过调用
approve允许TokenSwap提取交换金额的代币
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "./IERC20.sol";
/*
如何交换代币
1. Alice 拥有 100 个来自 AliceCoin 的代币,这是一个 ERC20 代币。
2. Bob 拥有 100 个来自 BobCoin 的代币,这也是一个 ERC20 代币。
3. Alice 和 Bob 想要交换 10 个 AliceCoin 以换取 20 个 BobCoin。
4. Alice 或 Bob 部署 `TokenSwap`
5. Alice 授权 `TokenSwap` 提取 10 个 AliceCoin
6. Bob 授权 `TokenSwap` 提取 20 个 BobCoin
7. Alice 或 Bob 调用 `TokenSwap.swap()`
8. Alice 和 Bob 成功交换了代币。
*/
contract TokenSwap {
IERC20 public token1;
address public owner1;
uint256 public amount1;
IERC20 public token2;
address public owner2;
uint256 public amount2;
constructor(
address _token1,
address _owner1,
uint256 _amount1,
address _token2,
address _owner2,
uint256 _amount2
) {
token1 = IERC20(_token1);
owner1 = _owner1;
amount1 = _amount1;
token2 = IERC20(_token2);
owner2 = _owner2;
amount2 = _amount2;
}
function swap() public {
require(msg.sender == owner1 || msg.sender == owner2, "Not authorized");
require(token1.allowance(owner1, address(this)) >= amount1, "Token 1 allowance too low");
require(token2.allowance(owner2, address(this)) >= amount2, "Token 2 allowance too low");
_safeTransferFrom(token1, owner1, owner2, amount1);
_safeTransferFrom(token2, owner2, owner1, amount2);
}
function _safeTransferFrom(
IERC20 token,
address sender,
address recipient,
uint256 amount
) private {
bool sent = token.transferFrom(sender, recipient, amount);
require(sent, "Token transfer failed");
}
}
以上是 ERC20 标准及其实现的完整示例,涵盖了创建、管理和交换代币的基本功能。
7.6 6 ERC721
7.6.1 ERC721 标准简介
ERC721 是一种非同质化代币(NFT)标准,用于在以太坊区块链上创建独特且不可互换的代币。与 ERC20 标准的同质化代币(如法定货币或某种代币)不同,ERC721 代币每个都有独特的属性和价值,使其适用于数字艺术、收藏品、游戏资产等场景。
7.6.2 主要特点
- 唯一性:每个 ERC721 代币都有一个唯一的标识符(Token ID),使其在区块链上不可替代。
- 所有权管理:ERC721 标准提供了跟踪代币所有权的机制,可以在不同的用户之间转移代币。
- 智能合约:ERC721 代币的创建和管理通过智能合约实现,支持在合约中定义代币的特性和功能。
- 可扩展性:可以根据需求扩展代币的功能,例如添加元数据(如艺术品的描述、创作者信息等)。
7.6.3 ERC721 接口
以下是 ERC721 的基本接口示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
interface IERC721 {
function balanceOf(address owner) external view returns (uint256);
function ownerOf(uint256 tokenId) external view returns (address);
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function transferFrom(address from, address to, uint256 tokenId) external;
function approve(address to, uint256 tokenId) external;
function getApproved(uint256 tokenId) external view returns (address);
function setApprovalForAll(address operator, bool approved) external;
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
7.6.4 应用场景
- 数字艺术:艺术家可以创建和销售独一无二的数字艺术作品。
- 游戏资产:玩家可以拥有和交易游戏中的虚拟物品,如角色、皮肤和武器。
- 收藏品:如数字卡片、虚拟土地等,允许用户收藏和交易独特的资产。
ERC721 标准的出现推动了 NFT 生态系统的发展,使数字资产的独特性和稀缺性得到了有效保障。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
interface IERC165 {
function supportsInterface(bytes4 interfaceID)
external
view
returns (bool);
}
interface IERC721 is IERC165 {
function balanceOf(address owner) external view returns (uint256 balance);
function ownerOf(uint256 tokenId) external view returns (address owner);
function safeTransferFrom(address from, address to, uint256 tokenId)
external;
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes calldata data
) external;
function transferFrom(address from, address to, uint256 tokenId) external;
function approve(address to, uint256 tokenId) external;
function getApproved(uint256 tokenId)
external
view
returns (address operator);
function setApprovalForAll(address operator, bool _approved) external;
function isApprovedForAll(address owner, address operator)
external
view
returns (bool);
}
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
contract ERC721 is IERC721 {
event Transfer(
address indexed from, address indexed to, uint256 indexed id
);
event Approval(
address indexed owner, address indexed spender, uint256 indexed id
);
event ApprovalForAll(
address indexed owner, address indexed operator, bool approved
);
// Mapping from token ID to owner address
mapping(uint256 => address) internal _ownerOf;
// Mapping owner address to token count
mapping(address => uint256) internal _balanceOf;
// Mapping from token ID to approved address
mapping(uint256 => address) internal _approvals;
// Mapping from owner to operator approvals
mapping(address => mapping(address => bool)) public isApprovedForAll;
function supportsInterface(bytes4 interfaceId)
external
pure
returns (bool)
{
return interfaceId == type(IERC721).interfaceId
|| interfaceId == type(IERC165).interfaceId;
}
function ownerOf(uint256 id) external view returns (address owner) {
owner = _ownerOf[id];
require(owner != address(0), "token doesn't exist");
}
function balanceOf(address owner) external view returns (uint256) {
require(owner != address(0), "owner = zero address");
return _balanceOf[owner];
}
function setApprovalForAll(address operator, bool approved) external {
isApprovedForAll[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
function approve(address spender, uint256 id) external {
address owner = _ownerOf[id];
require(
msg.sender == owner || isApprovedForAll[owner][msg.sender],
"not authorized"
);
_approvals[id] = spender;
emit Approval(owner, spender, id);
}
function getApproved(uint256 id) external view returns (address) {
require(_ownerOf[id] != address(0), "token doesn't exist");
return _approvals[id];
}
function _isApprovedOrOwner(address owner, address spender, uint256 id)
internal
view
returns (bool)
{
return (
spender == owner || isApprovedForAll[owner][spender]
|| spender == _approvals[id]
);
}
function transferFrom(address from, address to, uint256 id) public {
require(from == _ownerOf[id], "from != owner");
require(to != address(0), "transfer to zero address");
require(_isApprovedOrOwner(from, msg.sender, id), "not authorized");
_balanceOf[from]--;
_balanceOf[to]++;
_ownerOf[id] = to;
delete _approvals[id];
emit Transfer(from, to, id);
}
function safeTransferFrom(address from, address to, uint256 id) external {
transferFrom(from, to, id);
require(
to.code.length == 0
|| IERC721Receiver(to).onERC721Received(msg.sender, from, id, "")
== IERC721Receiver.onERC721Received.selector,
"unsafe recipient"
);
}
function safeTransferFrom(
address from,
address to,
uint256 id,
bytes calldata data
) external {
transferFrom(from, to, id);
require(
to.code.length == 0
|| IERC721Receiver(to).onERC721Received(msg.sender, from, id, data)
== IERC721Receiver.onERC721Received.selector,
"unsafe recipient"
);
}
function _mint(address to, uint256 id) internal {
require(to != address(0), "mint to zero address");
require(_ownerOf[id] == address(0), "already minted");
_balanceOf[to]++;
_ownerOf[id] = to;
emit Transfer(address(0), to, id);
}
function _burn(uint256 id) internal {
address owner = _ownerOf[id];
require(owner != address(0), "not minted");
_balanceOf[owner] -= 1;
delete _ownerOf[id];
delete _approvals[id];
emit Transfer(owner, address(0), id);
}
}
contract MyNFT is ERC721 {
function mint(address to, uint256 id) external {
_mint(to, id);
}
function burn(uint256 id) external {
require(msg.sender == _ownerOf[id], "not owner");
_burn(id);
}
}
7.7 7 ERC1155
ERC1155 是一种多代币标准,允许在同一个合约中同时管理同质化代币(如 ERC20)和非同质化代币(如 ERC721)。该标准旨在提高区块链应用的灵活性和效率,特别是在游戏和数字资产领域。
7.7.1 主要特点
多代币支持:ERC1155 合约可以同时管理多种类型的代币,用户可以在同一合约中创建和转移不同种类的代币,降低了合约部署和交易成本。
批量操作:支持批量转移和批量查询功能,可以一次性处理多个代币的转移,提高了交易效率。例如,可以同时转移多种资产,而不必逐一操作。
元数据支持:ERC1155 支持为每种代币提供元数据,允许开发者描述代币的属性和功能,从而增强用户体验。
更低的 gas 成本:由于批量处理和共享合约的特性,相比于单独的 ERC721 或 ERC20 合约,ERC1155 可以显著降低交易成本。
7.7.2 ERC1155 接口
以下是 ERC1155 的基本接口示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
interface IERC1155 {
function balanceOf(address account, uint256 id) external view returns (uint256);
function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory);
function setApprovalForAll(address operator, bool approved) external;
function isApprovedForAll(address account, address operator) external view returns (bool);
function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external;
function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external;
}
7.7.3 应用场景
游戏行业:在游戏中,开发者可以使用 ERC1155 创建多种游戏资产,如角色、道具和虚拟货币,用户可以在同一合约中交易和管理这些资产。
数字艺术:艺术家可以发布同时具有可替代性和不可替代性的艺术作品,支持不同形式的艺术作品,如限量版和独特作品。
收藏品:支持不同类型的收藏品,用户可以在同一合约中收藏和交易各种类型的资产。
ERC1155 标准的引入大大增强了区块链资产的灵活性和可操作性,推动了 NFT 生态系统的多样化发展。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
interface IERC1155 {
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 value,
bytes calldata data
) external;
function safeBatchTransferFrom(
address from,
address to,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external;
function balanceOf(address owner, uint256 id)
external
view
returns (uint256);
function balanceOfBatch(address[] calldata owners, uint256[] calldata ids)
external
view
returns (uint256[] memory);
function setApprovalForAll(address operator, bool approved) external;
function isApprovedForAll(address owner, address operator)
external
view
returns (bool);
}
interface IERC1155TokenReceiver {
function onERC1155Received(
address operator,
address from,
uint256 id,
uint256 value,
bytes calldata data
) external returns (bytes4);
function onERC1155BatchReceived(
address operator,
address from,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external returns (bytes4);
}
contract ERC1155 is IERC1155 {
event TransferSingle(
address indexed operator,
address indexed from,
address indexed to,
uint256 id,
uint256 value
);
event TransferBatch(
address indexed operator,
address indexed from,
address indexed to,
uint256[] ids,
uint256[] values
);
event ApprovalForAll(
address indexed owner, address indexed operator, bool approved
);
event URI(string value, uint256 indexed id);
// owner => id => balance
mapping(address => mapping(uint256 => uint256)) public balanceOf;
// owner => operator => approved
mapping(address => mapping(address => bool)) public isApprovedForAll;
function balanceOfBatch(address[] calldata owners, uint256[] calldata ids)
external
view
returns (uint256[] memory balances)
{
require(owners.length == ids.length, "owners length != ids length");
balances = new uint256[](owners.length);
unchecked {
for (uint256 i = 0; i < owners.length; i++) {
balances[i] = balanceOf[owners[i]][ids[i]];
}
}
}
function setApprovalForAll(address operator, bool approved) external {
isApprovedForAll[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 value,
bytes calldata data
) external {
require(
msg.sender == from || isApprovedForAll[from][msg.sender],
"not approved"
);
require(to != address(0), "to = 0 address");
balanceOf[from][id] -= value;
balanceOf[to][id] += value;
emit TransferSingle(msg.sender, from, to, id, value);
if (to.code.length > 0) {
require(
IERC1155TokenReceiver(to).onERC1155Received(
msg.sender, from, id, value, data
) == IERC1155TokenReceiver.onERC1155Received.selector,
"unsafe transfer"
);
}
}
function safeBatchTransferFrom(
address from,
address to,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external {
require(
msg.sender == from || isApprovedForAll[from][msg.sender],
"not approved"
);
require(to != address(0), "to = 0 address");
require(ids.length == values.length, "ids length != values length");
for (uint256 i = 0; i < ids.length; i++) {
balanceOf[from][ids[i]] -= values[i];
balanceOf[to][ids[i]] += values[i];
}
emit TransferBatch(msg.sender, from, to, ids, values);
if (to.code.length > 0) {
require(
IERC1155TokenReceiver(to).onERC1155BatchReceived(
msg.sender, from, ids, values, data
) == IERC1155TokenReceiver.onERC1155BatchReceived.selector,
"unsafe transfer"
);
}
}
// ERC165
function supportsInterface(bytes4 interfaceId)
external
view
returns (bool)
{
return interfaceId == 0x01ffc9a7 // ERC165 Interface ID for ERC165
|| interfaceId == 0xd9b67a26 // ERC165 Interface ID for ERC1155
|| interfaceId == 0x0e89341c; // ERC165 Interface ID for ERC1155MetadataURI
}
// ERC1155 Metadata URI
function uri(uint256 id) public view virtual returns (string memory) {}
// Internal functions
function _mint(address to, uint256 id, uint256 value, bytes memory data)
internal
{
require(to != address(0), "to = 0 address");
balanceOf[to][id] += value;
emit TransferSingle(msg.sender, address(0), to, id, value);
if (to.code.length > 0) {
require(
IERC1155TokenReceiver(to).onERC1155Received(
msg.sender, address(0), id, value, data
) == IERC1155TokenReceiver.onERC1155Received.selector,
"unsafe transfer"
);
}
}
function _batchMint(
address to,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) internal {
require(to != address(0), "to = 0 address");
require(ids.length == values.length, "ids length != values length");
for (uint256 i = 0; i < ids.length; i++) {
balanceOf[to][ids[i]] += values[i];
}
emit TransferBatch(msg.sender, address(0), to, ids, values);
if (to.code.length > 0) {
require(
IERC1155TokenReceiver(to).onERC1155BatchReceived(
msg.sender, address(0), ids, values, data
) == IERC1155TokenReceiver.onERC1155BatchReceived.selector,
"unsafe transfer"
);
}
}
function _burn(address from, uint256 id, uint256 value) internal {
require(from != address(0), "from = 0 address");
balanceOf[from][id] -= value;
emit TransferSingle(msg.sender, from, address(0), id, value);
}
function _batchBurn(
address from,
uint256[] calldata ids,
uint256[] calldata values
) internal {
require(from != address(0), "from = 0 address");
require(ids.length == values.length, "ids length != values length");
for (uint256 i = 0; i < ids.length; i++) {
balanceOf[from][ids[i]] -= values[i];
}
emit TransferBatch(msg.sender, from, address(0), ids, values);
}
}
contract MyMultiToken is ERC1155 {
function mint(uint256 id, uint256 value, bytes memory data) external {
_mint(msg.sender, id, value, data);
}
function batchMint(
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external {
_batchMint(msg.sender, ids, values, data);
}
function burn(uint256 id, uint256 value) external {
_burn(msg.sender, id, value);
}
function batchBurn(uint256[] calldata ids, uint256[] calldata values)
external
{
_batchBurn(msg.sender, ids, values);
}
}
7.8 8 Gasless Token Transfer
Gasless Token Transfer 是一种新兴的区块链技术,允许用户在无需支付交易手续费(gas)的情况下进行代币转移。这种机制通常通过使用代理合约或代币经济模型来实现,旨在提升用户体验并降低参与门槛。
7.8.1 主要特点
免交易费用:用户无需直接支付 gas 费用,降低了参与区块链交易的成本,使更多用户能够轻松进入和使用区块链应用。
用户体验优化:通过简化转账过程,用户可以在不担心 gas 费用的情况下进行频繁的代币交易,提升了应用的可用性。
代理合约:通常,gasless 转账通过一个代理合约来实现。用户授权该合约处理他们的代币转账,合约在后台承担了手续费。
补偿机制:一些实现可能通过补偿机制(如通过 DApp 的收入或其他方式)来支付 gas 费用,从而使用户能够享受无手续费的体验。
7.8.2 应用场景
去中心化金融(DeFi):在 DeFi 应用中,用户可以更轻松地进行流动性提供、借贷等操作,而不需要担心 gas 费用的波动。
游戏:在区块链游戏中,玩家可以频繁地进行交易而不受 gas 费用影响,从而增强游戏体验。
NFT 市场:用户可以在 NFT 交易市场中更轻松地买卖资产,鼓励更多用户参与。
Gasless ERC20 token transfer with Meta transaction
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
interface IERC20Permit {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount)
external
returns (bool);
function allowance(address owner, address spender)
external
view
returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount)
external
returns (bool);
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
}
contract GaslessTokenTransfer {
function send(
address token,
address sender,
address receiver,
uint256 amount,
uint256 fee,
uint256 deadline,
// Permit signature
uint8 v,
bytes32 r,
bytes32 s
) external {
// Permit
IERC20Permit(token).permit(
sender, address(this), amount + fee, deadline, v, r, s
);
// Send amount to receiver
IERC20Permit(token).transferFrom(sender, receiver, amount);
// Take fee - send fee to msg.sender
IERC20Permit(token).transferFrom(sender, msg.sender, fee);
}
}
Example ERC20 that implements permit copied from solmate
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol)
/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol)
/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it.
abstract contract ERC20 {
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(
address indexed owner, address indexed spender, uint256 amount
);
string public name;
string public symbol;
uint8 public immutable decimals;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
uint256 internal immutable INITIAL_CHAIN_ID;
bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR;
mapping(address => uint256) public nonces;
constructor(string memory _name, string memory _symbol, uint8 _decimals) {
name = _name;
symbol = _symbol;
decimals = _decimals;
INITIAL_CHAIN_ID = block.chainid;
INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
}
function approve(address spender, uint256 amount)
public
virtual
returns (bool)
{
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transfer(address to, uint256 amount)
public
virtual
returns (bool)
{
balanceOf[msg.sender] -= amount;
unchecked {
balanceOf[to] += amount;
}
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount)
public
virtual
returns (bool)
{
uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max) {
allowance[from][msg.sender] = allowed - amount;
}
balanceOf[from] -= amount;
unchecked {
balanceOf[to] += amount;
}
emit Transfer(from, to, amount);
return true;
}
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");
unchecked {
address recoveredAddress = ecrecover(
keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
),
owner,
spender,
value,
nonces[owner]++,
deadline
)
)
)
),
v,
r,
s
);
require(
recoveredAddress != address(0) && recoveredAddress == owner,
"INVALID_SIGNER"
);
allowance[recoveredAddress][spender] = value;
}
emit Approval(owner, spender, value);
}
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
return block.chainid == INITIAL_CHAIN_ID
? INITIAL_DOMAIN_SEPARATOR
: computeDomainSeparator();
}
function computeDomainSeparator() internal view virtual returns (bytes32) {
return keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
keccak256(bytes(name)),
keccak256("1"),
block.chainid,
address(this)
)
);
}
function _mint(address to, uint256 amount) internal virtual {
totalSupply += amount;
unchecked {
balanceOf[to] += amount;
}
emit Transfer(address(0), to, amount);
}
function _burn(address from, uint256 amount) internal virtual {
balanceOf[from] -= amount;
unchecked {
totalSupply -= amount;
}
emit Transfer(from, address(0), amount);
}
}
contract ERC20Permit is ERC20 {
constructor(string memory _name, string memory _symbol, uint8 _decimals)
ERC20(_name, _symbol, _decimals)
{}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
7.9 9 Simple Bytecode Contract
Simple Bytecode Contract 是一种基本的智能合约,其主要功能是展示如何通过字节码直接与以太坊网络进行交互。这种合约通常用于教育和测试目的,帮助开发者理解以太坊的底层机制。
7.9.1 主要特点
字节码表示:合约的所有功能和状态都是通过字节码实现的,这种方式强调了智能合约在以太坊虚拟机(EVM)中的执行过程。
简化结构:Simple Bytecode Contract 通常包含最少的功能,便于开发者理解基本的合约结构和 EVM 操作。
可部署性:用户可以将字节码直接部署到以太坊网络中,而无需复杂的编程环境。
低级交互:通过简单的字节码合约,开发者可以学习如何进行底层的操作,比如状态存储、调用和返回值处理。
7.9.2 应用场景
教育目的:作为学习智能合约开发的基础,帮助初学者了解 EVM 的工作原理和合约的基本概念。
测试与实验:开发者可以在测试网络上部署简单字节码合约,以验证其理解和测试不同的 EVM 操作。
性能分析:研究人员可以使用这些合约来分析和优化合约的执行效率和 gas 成本。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Factory {
event Log(address addr);
// Deploys a contract that always returns 255
function deploy() external {
bytes memory bytecode = hex"6960ff60005260206000f3600052600a6016f3";
address addr;
assembly {
// create(value, offset, size)
addr := create(0, add(bytecode, 0x20), 0x13)
}
require(addr != address(0));
emit Log(addr);
}
}
interface IContract {
function getValue() external view returns (uint256);
}
// https://www.evm.codes/playground
/*
Run time code - return 255
60ff60005260206000f3
// Store 255 to memory
mstore(p, v) - store v at memory p to p + 32
PUSH1 0xff
PUSH1 0
MSTORE
// Return 32 bytes from memory
return(p, s) - end execution and return data from memory p to p + s
PUSH1 0x20
PUSH1 0
RETURN
Creation code - return runtime code
6960ff60005260206000f3600052600a6016f3
// Store run time code to memory
PUSH10 0X60ff60005260206000f3
PUSH1 0
MSTORE
// Return 10 bytes from memory starting at offset 22
PUSH1 0x0a
PUSH1 0x16
RETURN
*/
7.10 10 Precompute Contract Address with Create2
Create2 是以太坊引入的一种方法,用于在部署智能合约时预计算合约地址。这种机制允许开发者在合约部署之前预测合约的地址,从而提升合约的可预测性和安全性。
7.10.1 主要特点
可预测性:使用 Create2,开发者可以根据给定的参数(如盐值和字节码)预先计算出合约地址。这使得合约地址在部署之前是可知的,有助于其他合约或用户进行交互。
盐值机制:Create2 允许开发者使用一个自定义的盐值(salt),这一机制增加了生成地址的多样性。不同的盐值和字节码组合将生成不同的合约地址。
安全性增强:由于可以预先计算合约地址,开发者能够在合约交互中验证地址的正确性,从而降低了合约被错误地址攻击的风险。
无状态合约:Create2 支持部署无状态合约,这意味着即使在合约被销毁后,其地址仍然有效,可以用于某些特定用途,如代币的逻辑分发。
7.10.2 应用场景
合约工厂模式:开发者可以创建合约工厂,使用 Create2 在已知地址上部署多个合约实例,从而减少部署和交互的复杂性。
代币和 NFT:在代币或 NFT 项目中,Create2 可用于确保所有相关合约的地址在部署前都是可预测的,简化了用户交互和集成流程。
高效的合约升级:通过预计算地址,开发者可以设计合约升级机制,使得新版本合约能够在特定地址下被部署,从而无缝替换旧版本。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Factory {
// Returns the address of the newly deployed contract
function deploy(address _owner, uint256 _foo, bytes32 _salt)
public
payable
returns (address)
{
// This syntax is a newer way to invoke create2 without assembly, you just need to pass salt
// https://docs.soliditylang.org/en/latest/control-structures.html#salted-contract-creations-create2
return address(new TestContract{salt: _salt}(_owner, _foo));
}
}
// This is the older way of doing it using assembly
contract FactoryAssembly {
event Deployed(address addr, uint256 salt);
// 1. Get bytecode of contract to be deployed
// NOTE: _owner and _foo are arguments of the TestContract's constructor
function getBytecode(address _owner, uint256 _foo)
public
pure
returns (bytes memory)
{
bytes memory bytecode = type(TestContract).creationCode;
return abi.encodePacked(bytecode, abi.encode(_owner, _foo));
}
// 2. Compute the address of the contract to be deployed
// NOTE: _salt is a random number used to create an address
function getAddress(bytes memory bytecode, uint256 _salt)
public
view
returns (address)
{
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff), address(this), _salt, keccak256(bytecode)
)
);
// NOTE: cast last 20 bytes of hash to address
return address(uint160(uint256(hash)));
}
// 3. Deploy the contract
// NOTE:
// Check the event log Deployed which contains the address of the deployed TestContract.
// The address in the log should equal the address computed from above.
function deploy(bytes memory bytecode, uint256 _salt) public payable {
address addr;
/*
NOTE: How to call create2
create2(v, p, n, s)
create new contract with code at memory p to p + n
and send v wei
and return the new address
where new address = first 20 bytes of keccak256(0xff + address(this) + s + keccak256(mem[p…(p+n)))
s = big-endian 256-bit value
*/
assembly {
addr :=
create2(
callvalue(), // wei sent with current call
// Actual code starts after skipping the first 32 bytes
add(bytecode, 0x20),
mload(bytecode), // Load the size of code contained in the first 32 bytes
_salt // Salt from function arguments
)
if iszero(extcodesize(addr)) { revert(0, 0) }
}
emit Deployed(addr, _salt);
}
}
contract TestContract {
address public owner;
uint256 public foo;
constructor(address _owner, uint256 _foo) payable {
owner = _owner;
foo = _foo;
}
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
7.11 10 Minimal Proxy Contract
最小代理合约(Minimal Proxy Contract)是一种轻量级的智能合约设计模式,主要用于节省部署成本和减少区块链上的存储需求。它通过代理合约的方式,实现对逻辑合约的调用,而不需要在每个实例中重复存储所有的逻辑。
7.11.1 主要特点
节省 Gas 成本:最小代理合约的代码量极小,主要只包含必要的转发逻辑。这使得部署代理合约的 Gas 成本显著低于直接部署完整合约。
灵活的逻辑更新:代理合约可以指向不同的逻辑合约,从而实现逻辑的更新和替换。这种方式允许开发者在不影响用户地址和状态的情况下,更新合约功能。
标准化模式:根据 EIP-1167 标准,最小代理合约的实现方法得到了广泛认可,确保了合约的可互操作性。
透明性:虽然使用代理模式,但用户仍然可以清楚地看到与其交互的逻辑合约地址,保证了透明性。
7.11.2 应用场景
可升级合约:在需要频繁更新合约逻辑的应用中,使用最小代理合约可以方便地管理版本更新。
节约资源:对于需要部署大量相似合约的场景(如代币发行),最小代理合约可以显著减少部署和存储成本。
合约工厂:可以通过工厂合约创建多个代理合约,每个合约可以指向相同的逻辑合约,便于管理和扩展。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// original code
// https://github.com/optionality/clone-factory/blob/master/contracts/CloneFactory.sol
contract MinimalProxy {
function clone(address target) external returns (address result) {
// convert address to 20 bytes
bytes20 targetBytes = bytes20(target);
// actual code //
// 3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3
// creation code //
// copy runtime code into memory and return it
// 3d602d80600a3d3981f3
// runtime code //
// code to delegatecall to address
// 363d3d373d3d3d363d73 address 5af43d82803e903d91602b57fd5bf3
assembly {
/*
reads the 32 bytes of memory starting at pointer stored in 0x40
In solidity, the 0x40 slot in memory is special: it contains the "free memory pointer"
which points to the end of the currently allocated memory.
*/
let clone := mload(0x40)
// store 32 bytes to memory starting at "clone"
mstore(
clone,
0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000
)
/*
| 20 bytes |
0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000
^
pointer
*/
// store 32 bytes to memory starting at "clone" + 20 bytes
// 0x14 = 20
mstore(add(clone, 0x14), targetBytes)
/*
| 20 bytes | 20 bytes |
0x3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe
^
pointer
*/
// store 32 bytes to memory starting at "clone" + 40 bytes
// 0x28 = 40
mstore(
add(clone, 0x28),
0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000
)
/*
| 20 bytes | 20 bytes | 15 bytes |
0x3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3
*/
// create new contract
// send 0 Ether
// code starts at pointer stored in "clone"
// code size 0x37 (55 bytes)
result := create(0, clone, 0x37)
}
}
}
7.12 11 Upgradeable Proxy
可升级代理(Upgradeable Proxy)是一种设计模式,允许智能合约在不改变合约地址的情况下,升级其逻辑。该模式使得合约的功能可以随着时间的推移进行更新和扩展,同时保持用户的状态和交互地址不变。
7.12.0.1 主要特点
逻辑分离:可升级代理将合约的逻辑与状态分离。逻辑合约包含功能实现,而代理合约则负责转发请求并管理状态。这种结构使得逻辑可以独立更新。
灵活性和可维护性:通过更改代理合约中的指向逻辑合约的地址,可以轻松升级功能,而无需迁移用户数据或更改合约地址。
透明性:虽然用户与代理合约交互,但它们可以通过代理合约清晰地了解当前逻辑合约的地址,从而保持透明性。
标准化:根据 EIP-1967 等标准,可升级代理的实现得到了广泛认可,确保了其可互操作性和安全性。
7.12.0.2 应用场景
持续开发:在需要频繁更新合约功能的项目中,例如去中心化金融(DeFi)应用,使用可升级代理可以轻松实现功能改进和修复。
治理和管理:可以通过治理机制控制合约的升级,确保所有利益相关者的参与和透明度。
复杂系统:在复杂的合约系统中,多个代理合约可以指向不同的逻辑合约,从而实现更高层次的可扩展性和模块化。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// Transparent upgradeable proxy pattern
contract CounterV1 {
uint256 public count;
function inc() external {
count += 1;
}
}
contract CounterV2 {
uint256 public count;
function inc() external {
count += 1;
}
function dec() external {
count -= 1;
}
}
contract BuggyProxy {
address public implementation;
address public admin;
constructor() {
admin = msg.sender;
}
function _delegate() private {
(bool ok,) = implementation.delegatecall(msg.data);
require(ok, "delegatecall failed");
}
fallback() external payable {
_delegate();
}
receive() external payable {
_delegate();
}
function upgradeTo(address _implementation) external {
require(msg.sender == admin, "not authorized");
implementation = _implementation;
}
}
contract Dev {
function selectors() external view returns (bytes4, bytes4, bytes4) {
return (
Proxy.admin.selector,
Proxy.implementation.selector,
Proxy.upgradeTo.selector
);
}
}
contract Proxy {
// All functions / variables should be private, forward all calls to fallback
// -1 for unknown preimage
// 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
bytes32 private constant IMPLEMENTATION_SLOT =
bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);
// 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
bytes32 private constant ADMIN_SLOT =
bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1);
constructor() {
_setAdmin(msg.sender);
}
modifier ifAdmin() {
if (msg.sender == _getAdmin()) {
_;
} else {
_fallback();
}
}
function _getAdmin() private view returns (address) {
return StorageSlot.getAddressSlot(ADMIN_SLOT).value;
}
function _setAdmin(address _admin) private {
require(_admin != address(0), "admin = zero address");
StorageSlot.getAddressSlot(ADMIN_SLOT).value = _admin;
}
function _getImplementation() private view returns (address) {
return StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value;
}
function _setImplementation(address _implementation) private {
require(
_implementation.code.length > 0, "implementation is not contract"
);
StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = _implementation;
}
// Admin interface //
function changeAdmin(address _admin) external ifAdmin {
_setAdmin(_admin);
}
// 0x3659cfe6
function upgradeTo(address _implementation) external ifAdmin {
_setImplementation(_implementation);
}
// 0xf851a440
function admin() external ifAdmin returns (address) {
return _getAdmin();
}
// 0x5c60da1b
function implementation() external ifAdmin returns (address) {
return _getImplementation();
}
// User interface //
function _delegate(address _implementation) internal virtual {
assembly {
// Copy msg.data. We take full control of memory in this inline assembly
// block because it will not return to Solidity code. We overwrite the
// Solidity scratch pad at memory position 0.
// calldatacopy(t, f, s) - copy s bytes from calldata at position f to mem at position t
// calldatasize() - size of call data in bytes
calldatacopy(0, 0, calldatasize())
// Call the implementation.
// out and outsize are 0 because we don't know the size yet.
// delegatecall(g, a, in, insize, out, outsize) -
// - call contract at address a
// - with input mem[in…(in+insize))
// - providing g gas
// - and output area mem[out…(out+outsize))
// - returning 0 on error (eg. out of gas) and 1 on success
let result :=
delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)
// Copy the returned data.
// returndatacopy(t, f, s) - copy s bytes from returndata at position f to mem at position t
// returndatasize() - size of the last returndata
returndatacopy(0, 0, returndatasize())
switch result
// delegatecall returns 0 on error.
case 0 {
// revert(p, s) - end execution, revert state changes, return data mem[p…(p+s))
revert(0, returndatasize())
}
default {
// return(p, s) - end execution, return data mem[p…(p+s))
return(0, returndatasize())
}
}
}
function _fallback() private {
_delegate(_getImplementation());
}
fallback() external payable {
_fallback();
}
receive() external payable {
_fallback();
}
}
contract ProxyAdmin {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function getProxyAdmin(address proxy) external view returns (address) {
(bool ok, bytes memory res) =
proxy.staticcall(abi.encodeCall(Proxy.admin, ()));
require(ok, "call failed");
return abi.decode(res, (address));
}
function getProxyImplementation(address proxy)
external
view
returns (address)
{
(bool ok, bytes memory res) =
proxy.staticcall(abi.encodeCall(Proxy.implementation, ()));
require(ok, "call failed");
return abi.decode(res, (address));
}
function changeProxyAdmin(address payable proxy, address admin)
external
onlyOwner
{
Proxy(proxy).changeAdmin(admin);
}
function upgrade(address payable proxy, address implementation)
external
onlyOwner
{
Proxy(proxy).upgradeTo(implementation);
}
}
library StorageSlot {
struct AddressSlot {
address value;
}
function getAddressSlot(bytes32 slot)
internal
pure
returns (AddressSlot storage r)
{
assembly {
r.slot := slot
}
}
}
contract TestSlot {
bytes32 public constant slot = keccak256("TEST_SLOT");
function getSlot() external view returns (address) {
return StorageSlot.getAddressSlot(slot).value;
}
function writeSlot(address _addr) external {
StorageSlot.getAddressSlot(slot).value = _addr;
}
}
7.13 12 Deploy Any Contract
- Deploy any contract by calling Proxy.deploy(bytes memory _code)
- For this example, you can get the contract bytecodes by calling Helper.getBytecode1 and Helper.getBytecode2
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Proxy {
event Deploy(address);
receive() external payable {}
function deploy(bytes memory _code)
external
payable
returns (address addr)
{
assembly {
// create(v, p, n)
// v = amount of ETH to send
// p = pointer in memory to start of code
// n = size of code
addr := create(callvalue(), add(_code, 0x20), mload(_code))
}
// return address 0 on error
require(addr != address(0), "deploy failed");
emit Deploy(addr);
}
function execute(address _target, bytes memory _data) external payable {
(bool success,) = _target.call{value: msg.value}(_data);
require(success, "failed");
}
}
contract TestContract1 {
address public owner = msg.sender;
function setOwner(address _owner) public {
require(msg.sender == owner, "not owner");
owner = _owner;
}
}
contract TestContract2 {
address public owner = msg.sender;
uint256 public value = msg.value;
uint256 public x;
uint256 public y;
constructor(uint256 _x, uint256 _y) payable {
x = _x;
y = _y;
}
}
contract Helper {
function getBytecode1() external pure returns (bytes memory) {
bytes memory bytecode = type(TestContract1).creationCode;
return bytecode;
}
function getBytecode2(uint256 _x, uint256 _y)
external
pure
returns (bytes memory)
{
bytes memory bytecode = type(TestContract2).creationCode;
return abi.encodePacked(bytecode, abi.encode(_x, _y));
}
function getCalldata(address _owner) external pure returns (bytes memory) {
return abi.encodeWithSignature("setOwner(address)", _owner);
}
}
7.14 13 Write to Any Slot
Solidity存储就像一个长度为2^256的数组。阵列中的每个插槽可以存储32个字节。
声明的顺序和状态变量的类型定义了它将使用哪些插槽。
但是,使用程序集,您可以写入任何插槽。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
library StorageSlot {
// Wrap address in a struct so that it can be passed around as a storage pointer
struct AddressSlot {
address value;
}
function getAddressSlot(bytes32 slot)
internal
pure
returns (AddressSlot storage pointer)
{
assembly {
// Get the pointer to AddressSlot stored at slot
pointer.slot := slot
}
}
}
contract TestSlot {
bytes32 public constant TEST_SLOT = keccak256("TEST_SLOT");
function write(address _addr) external {
StorageSlot.AddressSlot storage data =
StorageSlot.getAddressSlot(TEST_SLOT);
data.value = _addr;
}
function get() external view returns (address) {
StorageSlot.AddressSlot storage data =
StorageSlot.getAddressSlot(TEST_SLOT);
return data.value;
}
}
7.15 14 单向支付渠道
支付渠道允许参与者在链下重复转移以太币。
以下是本合同的使用方式:
- Alice部署合约,用一些以太币为其提供资金。
- Alice通过签署消息(链下)授权付款,并将签名发送给Bob。
- Bob通过向智能合约提交签名的消息来要求付款。
- 如果Bob没有要求付款,Alice将在合同到期后取回她的以太币
这被称为单向支付渠道,因为支付只能从Alice到Bob单向进行。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "./ECDSA.sol";
contract ReentrancyGuard {
bool private locked;
modifier guard() {
require(!locked);
locked = true;
_;
locked = false;
}
}
contract UniDirectionalPaymentChannel is ReentrancyGuard {
using ECDSA for bytes32;
address payable public sender;
address payable public receiver;
uint256 private constant DURATION = 7 * 24 * 60 * 60;
uint256 public expiresAt;
constructor(address payable _receiver) payable {
require(_receiver != address(0), "receiver = zero address");
sender = payable(msg.sender);
receiver = _receiver;
expiresAt = block.timestamp + DURATION;
}
function _getHash(uint256 _amount) private view returns (bytes32) {
// NOTE: sign with address of this contract to protect agains
// replay attack on other contracts
return keccak256(abi.encodePacked(address(this), _amount));
}
function getHash(uint256 _amount) external view returns (bytes32) {
return _getHash(_amount);
}
function _getEthSignedHash(uint256 _amount)
private
view
returns (bytes32)
{
return _getHash(_amount).toEthSignedMessageHash();
}
function getEthSignedHash(uint256 _amount)
external
view
returns (bytes32)
{
return _getEthSignedHash(_amount);
}
function _verify(uint256 _amount, bytes memory _sig)
private
view
returns (bool)
{
return _getEthSignedHash(_amount).recover(_sig) == sender;
}
function verify(uint256 _amount, bytes memory _sig)
external
view
returns (bool)
{
return _verify(_amount, _sig);
}
function close(uint256 _amount, bytes memory _sig) external guard {
require(msg.sender == receiver, "!receiver");
require(_verify(_amount, _sig), "invalid sig");
(bool sent,) = receiver.call{value: _amount}("");
require(sent, "Failed to send Ether");
selfdestruct(sender);
}
function cancel() external {
require(msg.sender == sender, "!sender");
require(block.timestamp >= expiresAt, "!expired");
selfdestruct(sender);
}
}
7.16 15 Bi-Directional Payment Channel
双向支付渠道允许参与者Alice和Bob在链下重复转移以太币。
付款可以双向进行,Alice支付Bob,Bob支付Alice。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "./ECDSA.sol";
/*
Opening a channel
1. Alice and Bob fund a multi-sig wallet
2. Precompute payment channel address
3. Alice and Bob exchanges signatures of initial balances
4. Alice and Bob creates a transaction that can deploy a payment channel from
the multi-sig wallet
Update channel balances
1. Repeat steps 1 - 3 from opening a channel
2. From multi-sig wallet create a transaction that will
- delete the transaction that would have deployed the old payment channel
- and then create a transaction that can deploy a payment channel with the
new balances
Closing a channel when Alice and Bob agree on the final balance
1. From multi-sig wallet create a transaction that will
- send payments to Alice and Bob
- and then delete the transaction that would have created the payment channel
Closing a channel when Alice and Bob do not agree on the final balances
1. Deploy payment channel from multi-sig
2. call challengeExit() to start the process of closing a channel
3. Alice and Bob can withdraw funds once the channel is expired
*/
contract BiDirectionalPaymentChannel {
using ECDSA for bytes32;
event ChallengeExit(address indexed sender, uint256 nonce);
event Withdraw(address indexed to, uint256 amount);
address payable[2] public users;
mapping(address => bool) public isUser;
mapping(address => uint256) public balances;
uint256 public challengePeriod;
uint256 public expiresAt;
uint256 public nonce;
modifier checkBalances(uint256[2] memory _balances) {
require(
address(this).balance >= _balances[0] + _balances[1],
"balance of contract must be >= to the total balance of users"
);
_;
}
// NOTE: deposit from multi-sig wallet
constructor(
address payable[2] memory _users,
uint256[2] memory _balances,
uint256 _expiresAt,
uint256 _challengePeriod
) payable checkBalances(_balances) {
require(_expiresAt > block.timestamp, "Expiration must be > now");
require(_challengePeriod > 0, "Challenge period must be > 0");
for (uint256 i = 0; i < _users.length; i++) {
address payable user = _users[i];
require(!isUser[user], "user must be unique");
users[i] = user;
isUser[user] = true;
balances[user] = _balances[i];
}
expiresAt = _expiresAt;
challengePeriod = _challengePeriod;
}
function verify(
bytes[2] memory _signatures,
address _contract,
address[2] memory _signers,
uint256[2] memory _balances,
uint256 _nonce
) public pure returns (bool) {
for (uint256 i = 0; i < _signatures.length; i++) {
/*
NOTE: sign with address of this contract to protect
agains replay attack on other contracts
*/
bool valid = _signers[i]
== keccak256(abi.encodePacked(_contract, _balances, _nonce))
.toEthSignedMessageHash().recover(_signatures[i]);
if (!valid) {
return false;
}
}
return true;
}
modifier checkSignatures(
bytes[2] memory _signatures,
uint256[2] memory _balances,
uint256 _nonce
) {
// Note: copy storage array to memory
address[2] memory signers;
for (uint256 i = 0; i < users.length; i++) {
signers[i] = users[i];
}
require(
verify(_signatures, address(this), signers, _balances, _nonce),
"Invalid signature"
);
_;
}
modifier onlyUser() {
require(isUser[msg.sender], "Not user");
_;
}
function challengeExit(
uint256[2] memory _balances,
uint256 _nonce,
bytes[2] memory _signatures
)
public
onlyUser
checkSignatures(_signatures, _balances, _nonce)
checkBalances(_balances)
{
require(block.timestamp < expiresAt, "Expired challenge period");
require(_nonce > nonce, "Nonce must be greater than the current nonce");
for (uint256 i = 0; i < _balances.length; i++) {
balances[users[i]] = _balances[i];
}
nonce = _nonce;
expiresAt = block.timestamp + challengePeriod;
emit ChallengeExit(msg.sender, nonce);
}
function withdraw() public onlyUser {
require(
block.timestamp >= expiresAt, "Challenge period has not expired yet"
);
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool sent,) = msg.sender.call{value: amount}("");
require(sent, "Failed to send Ether");
emit Withdraw(msg.sender, amount);
}
}
7.17 16 英式拍卖
拍卖流程
- NFT的卖家部署该合约。
- 拍卖持续7天。
- 参与者可以通过存入高于当前最高出价者的ETH进行出价。
- 所有出价者如果不是当前最高出价,可以撤回他们的出价。
拍卖结束后
- 最高出价者成为NFT的新拥有者。
- 卖家收到最高出价的ETH。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
interface IERC721 {
function safeTransferFrom(address from, address to, uint256 tokenId)
external;
function transferFrom(address, address, uint256) external;
}
contract EnglishAuction {
event Start();
event Bid(address indexed sender, uint256 amount);
event Withdraw(address indexed bidder, uint256 amount);
event End(address winner, uint256 amount);
IERC721 public nft;
uint256 public nftId;
address payable public seller;
uint256 public endAt;
bool public started;
bool public ended;
address public highestBidder;
uint256 public highestBid;
mapping(address => uint256) public bids;
constructor(address _nft, uint256 _nftId, uint256 _startingBid) {
nft = IERC721(_nft);
nftId = _nftId;
seller = payable(msg.sender);
highestBid = _startingBid;
}
function start() external {
require(!started, "started");
require(msg.sender == seller, "not seller");
nft.transferFrom(msg.sender, address(this), nftId);
started = true;
endAt = block.timestamp + 7 days;
emit Start();
}
function bid() external payable {
require(started, "not started");
require(block.timestamp < endAt, "ended");
require(msg.value > highestBid, "value < highest");
if (highestBidder != address(0)) {
bids[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit Bid(msg.sender, msg.value);
}
function withdraw() external {
uint256 bal = bids[msg.sender];
bids[msg.sender] = 0;
payable(msg.sender).transfer(bal);
emit Withdraw(msg.sender, bal);
}
function end() external {
require(started, "not started");
require(block.timestamp >= endAt, "not ended");
require(!ended, "ended");
ended = true;
if (highestBidder != address(0)) {
nft.safeTransferFrom(address(this), highestBidder, nftId);
seller.transfer(highestBid);
} else {
nft.safeTransferFrom(address(this), seller, nftId);
}
emit End(highestBidder, highestBid);
}
}
7.18 17 荷兰式拍卖
拍卖流程
- NFT的卖家部署该合约,并设定NFT的起始价格。
- 拍卖持续7天。
- NFT的价格随时间降低。
- 参与者可以通过存入高于智能合约计算的当前价格的ETH进行购买。
拍卖结束
当有买家购买NFT时,拍卖结束。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
interface IERC721 {
function transferFrom(address _from, address _to, uint256 _nftId)
external;
}
contract DutchAuction {
uint256 private constant DURATION = 7 days;
IERC721 public immutable nft;
uint256 public immutable nftId;
address payable public immutable seller;
uint256 public immutable startingPrice;
uint256 public immutable startAt;
uint256 public immutable expiresAt;
uint256 public immutable discountRate;
constructor(
uint256 _startingPrice,
uint256 _discountRate,
address _nft,
uint256 _nftId
) {
seller = payable(msg.sender);
startingPrice = _startingPrice;
startAt = block.timestamp;
expiresAt = block.timestamp + DURATION;
discountRate = _discountRate;
require(
_startingPrice >= _discountRate * DURATION, "starting price < min"
);
nft = IERC721(_nft);
nftId = _nftId;
}
function getPrice() public view returns (uint256) {
uint256 timeElapsed = block.timestamp - startAt;
uint256 discount = discountRate * timeElapsed;
return startingPrice - discount;
}
function buy() external payable {
require(block.timestamp < expiresAt, "auction expired");
uint256 price = getPrice();
require(msg.value >= price, "ETH < price");
nft.transferFrom(seller, msg.sender, nftId);
uint256 refund = msg.value - price;
if (refund > 0) {
payable(msg.sender).transfer(refund);
}
selfdestruct(seller);
}
}
7.19 18 众筹ERC20代币
众筹流程
- 用户创建一个众筹活动。
- 用户可以进行承诺,将他们的代币转移到活动中。
- 当活动结束后,如果承诺的总金额超过活动目标,活动创建者可以领取资金。
- 否则,若活动未达到目标,用户可以撤回他们的承诺。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
interface IERC20 {
function transfer(address, uint256) external returns (bool);
function transferFrom(address, address, uint256) external returns (bool);
}
contract CrowdFund {
event Launch(
uint256 id,
address indexed creator,
uint256 goal,
uint32 startAt,
uint32 endAt
);
event Cancel(uint256 id);
event Pledge(uint256 indexed id, address indexed caller, uint256 amount);
event Unpledge(uint256 indexed id, address indexed caller, uint256 amount);
event Claim(uint256 id);
event Refund(uint256 id, address indexed caller, uint256 amount);
struct Campaign {
// Creator of campaign
address creator;
// Amount of tokens to raise
uint256 goal;
// Total amount pledged
uint256 pledged;
// Timestamp of start of campaign
uint32 startAt;
// Timestamp of end of campaign
uint32 endAt;
// True if goal was reached and creator has claimed the tokens.
bool claimed;
}
IERC20 public immutable token;
// Total count of campaigns created.
// It is also used to generate id for new campaigns.
uint256 public count;
// Mapping from id to Campaign
mapping(uint256 => Campaign) public campaigns;
// Mapping from campaign id => pledger => amount pledged
mapping(uint256 => mapping(address => uint256)) public pledgedAmount;
constructor(address _token) {
token = IERC20(_token);
}
function launch(uint256 _goal, uint32 _startAt, uint32 _endAt) external {
require(_startAt >= block.timestamp, "start at < now");
require(_endAt >= _startAt, "end at < start at");
require(_endAt <= block.timestamp + 90 days, "end at > max duration");
count += 1;
campaigns[count] = Campaign({
creator: msg.sender,
goal: _goal,
pledged: 0,
startAt: _startAt,
endAt: _endAt,
claimed: false
});
emit Launch(count, msg.sender, _goal, _startAt, _endAt);
}
function cancel(uint256 _id) external {
Campaign memory campaign = campaigns[_id];
require(campaign.creator == msg.sender, "not creator");
require(block.timestamp < campaign.startAt, "started");
delete campaigns[_id];
emit Cancel(_id);
}
function pledge(uint256 _id, uint256 _amount) external {
Campaign storage campaign = campaigns[_id];
require(block.timestamp >= campaign.startAt, "not started");
require(block.timestamp <= campaign.endAt, "ended");
campaign.pledged += _amount;
pledgedAmount[_id][msg.sender] += _amount;
token.transferFrom(msg.sender, address(this), _amount);
emit Pledge(_id, msg.sender, _amount);
}
function unpledge(uint256 _id, uint256 _amount) external {
Campaign storage campaign = campaigns[_id];
require(block.timestamp <= campaign.endAt, "ended");
campaign.pledged -= _amount;
pledgedAmount[_id][msg.sender] -= _amount;
token.transfer(msg.sender, _amount);
emit Unpledge(_id, msg.sender, _amount);
}
function claim(uint256 _id) external {
Campaign storage campaign = campaigns[_id];
require(campaign.creator == msg.sender, "not creator");
require(block.timestamp > campaign.endAt, "not ended");
require(campaign.pledged >= campaign.goal, "pledged < goal");
require(!campaign.claimed, "claimed");
campaign.claimed = true;
token.transfer(campaign.creator, campaign.pledged);
emit Claim(_id);
}
function refund(uint256 _id) external {
Campaign memory campaign = campaigns[_id];
require(block.timestamp > campaign.endAt, "not ended");
require(campaign.pledged < campaign.goal, "pledged >= goal");
uint256 bal = pledgedAmount[_id][msg.sender];
pledgedAmount[_id][msg.sender] = 0;
token.transfer(msg.sender, bal);
emit Refund(_id, msg.sender, bal);
}
}
7.20 19 Multi Call
使用 for 循环和 staticcall 聚合多个查询。以下是一个简单的合约示例,展示如何从多个合约中查询数据
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract MultiCall {
function multiCall(address[] calldata targets, bytes[] calldata data)
external
view
returns (bytes[] memory)
{
require(targets.length == data.length, "target length != data length");
bytes[] memory results = new bytes[](data.length);
for (uint256 i; i < targets.length; i++) {
(bool success, bytes memory result) = targets[i].staticcall(data[i]);
require(success, "call failed");
results[i] = result;
}
return results;
}
}
Contract to test MultiCall
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract TestMultiCall {
function test(uint256 _i) external pure returns (uint256) {
return _i;
}
function getData(uint256 _i) external pure returns (bytes memory) {
return abi.encodeWithSelector(this.test.selector, _i);
}
}
7.21 20 Multi Delegatecall
这段代码展示了如何使用 delegatecall 实现多函数调用的智能合约示例。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract MultiDelegatecall {
error DelegatecallFailed();
function multiDelegatecall(bytes[] memory data)
external
payable
returns (bytes[] memory results)
{
results = new bytes[](data.length);
for (uint256 i; i < data.length; i++) {
(bool ok, bytes memory res) = address(this).delegatecall(data[i]);
if (!ok) {
revert DelegatecallFailed();
}
results[i] = res;
}
}
}
// Why use multi delegatecall? Why not multi call?
// alice -> multi call --- call ---> test (msg.sender = multi call)
// alice -> test --- delegatecall ---> test (msg.sender = alice)
contract TestMultiDelegatecall is MultiDelegatecall {
event Log(address caller, string func, uint256 i);
function func1(uint256 x, uint256 y) external {
// msg.sender = alice
emit Log(msg.sender, "func1", x + y);
}
function func2() external returns (uint256) {
// msg.sender = alice
emit Log(msg.sender, "func2", 2);
return 111;
}
mapping(address => uint256) public balanceOf;
// WARNING: unsafe code when used in combination with multi-delegatecall
// user can mint multiple times for the price of msg.value
function mint() external payable {
balanceOf[msg.sender] += msg.value;
}
}
contract Helper {
function getFunc1Data(uint256 x, uint256 y)
external
pure
returns (bytes memory)
{
return
abi.encodeWithSelector(TestMultiDelegatecall.func1.selector, x, y);
}
function getFunc2Data() external pure returns (bytes memory) {
return abi.encodeWithSelector(TestMultiDelegatecall.func2.selector);
}
function getMintData() external pure returns (bytes memory) {
return abi.encodeWithSelector(TestMultiDelegatecall.mint.selector);
}
}
7.21.1 合约解释
7.21.1.1 MultiDelegatecall
- 功能:这个合约允许用户通过单个交易调用多个函数。
multiDelegatecall:接受一个字节数组data,其中每个元素是调用不同函数的编码数据。它循环遍历这些数据,使用delegatecall调用自身的函数。delegatecall:在调用时,msg.sender保持为调用合约的地址,这意味着调用的函数能够以调用者的上下文(如状态变量等)执行。
7.22 21 时间锁 Time Lock
// SPDX-License-Identifier: MIT pragma solidity ^0.8.26;
contract TimeLock { error NotOwnerError(); error AlreadyQueuedError(bytes32 txId); error TimestampNotInRangeError(uint256 blockTimestamp, uint256 timestamp); error NotQueuedError(bytes32 txId); error TimestampNotPassedError(uint256 blockTimestmap, uint256 timestamp); error TimestampExpiredError(uint256 blockTimestamp, uint256 expiresAt); error TxFailedError();
event Queue(
bytes32 indexed txId,
address indexed target,
uint256 value,
string func,
bytes data,
uint256 timestamp
);
event Execute(
bytes32 indexed txId,
address indexed target,
uint256 value,
string func,
bytes data,
uint256 timestamp
);
event Cancel(bytes32 indexed txId);
uint256 public constant MIN_DELAY = 10; // seconds
uint256 public constant MAX_DELAY = 1000; // seconds
uint256 public constant GRACE_PERIOD = 1000; // seconds
address public owner;
// tx id => queued
mapping(bytes32 => bool) public queued;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
if (msg.sender != owner) {
revert NotOwnerError();
}
_;
}
receive() external payable {}
function getTxId(
address _target,
uint256 _value,
string calldata _func,
bytes calldata _data,
uint256 _timestamp
) public pure returns (bytes32) {
return keccak256(abi.encode(_target, _value, _func, _data, _timestamp));
}
/**
* @param _target Address of contract or account to call
* @param _value Amount of ETH to send
* @param _func Function signature, for example "foo(address,uint256)"
* @param _data ABI encoded data send.
* @param _timestamp Timestamp after which the transaction can be executed.
*/
function queue(
address _target,
uint256 _value,
string calldata _func,
bytes calldata _data,
uint256 _timestamp
) external onlyOwner returns (bytes32 txId) {
txId = getTxId(_target, _value, _func, _data, _timestamp);
if (queued[txId]) {
revert AlreadyQueuedError(txId);
}
// ---|------------|---------------|-------
// block block + min block + max
if (
_timestamp < block.timestamp + MIN_DELAY
|| _timestamp > block.timestamp + MAX_DELAY
) {
revert TimestampNotInRangeError(block.timestamp, _timestamp);
}
queued[txId] = true;
emit Queue(txId, _target, _value, _func, _data, _timestamp);
}
function execute(
address _target,
uint256 _value,
string calldata _func,
bytes calldata _data,
uint256 _timestamp
) external payable onlyOwner returns (bytes memory) {
bytes32 txId = getTxId(_target, _value, _func, _data, _timestamp);
if (!queued[txId]) {
revert NotQueuedError(txId);
}
// ----|-------------------|-------
// timestamp timestamp + grace period
if (block.timestamp < _timestamp) {
revert TimestampNotPassedError(block.timestamp, _timestamp);
}
if (block.timestamp > _timestamp + GRACE_PERIOD) {
revert TimestampExpiredError(
block.timestamp, _timestamp + GRACE_PERIOD
);
}
queued[txId] = false;
// prepare data
bytes memory data;
if (bytes(_func).length > 0) {
// data = func selector + _data
data = abi.encodePacked(bytes4(keccak256(bytes(_func))), _data);
} else {
// call fallback with data
data = _data;
}
// call target
(bool ok, bytes memory res) = _target.call{value: _value}(data);
if (!ok) {
revert TxFailedError();
}
emit Execute(txId, _target, _value, _func, _data, _timestamp);
return res;
}
function cancel(bytes32 _txId) external onlyOwner {
if (!queued[_txId]) {
revert NotQueuedError(_txId);
}
queued[_txId] = false;
emit Cancel(_txId);
}
}
这段代码实现了一个时间锁合约 (TimeLock),用于在未来的某个时间点执行某个交易,常见于去中心化自治组织(DAO)中。
7.22.1 合约结构与功能
7.22.2 主要功能
getTxId:生成唯一的交易 ID,基于目标地址、发送的以太币数量、函数签名、数据和时间戳。queue:- 接受目标地址、发送的以太币数量、函数名、数据和执行时间戳。
- 检查交易 ID 是否已经排队,时间戳是否在允许的范围内。
- 将交易 ID 标记为已排队,并触发
Queue事件。
execute:- 执行排队的交易。
- 检查交易是否已排队,时间戳是否已到达且未过期。
- 准备调用的数据,如果有函数名,构建函数选择器和数据。
- 使用低级
call方法调用目标合约,转账以太币。 - 如果交易成功,触发
Execute事件。
cancel:- 允许合约拥有者取消已排队的交易,并触发
Cancel事件。
- 允许合约拥有者取消已排队的交易,并触发
7.23 22 Assembly Binary Exponentiation
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract AssemblyBinExp {
// Binary exponentiation to calculate x**n
function rpow(uint256 x, uint256 n, uint256 b)
public
pure
returns (uint256 z)
{
assembly {
switch x
// x = 0
case 0 {
switch n
// n = 0 --> x**n = 0**0 --> 1
case 0 { z := b }
// n > 0 --> x**n = 0**n --> 0
default { z := 0 }
}
default {
switch mod(n, 2)
// x > 0 and n is even --> z = 1
case 0 { z := b }
// x > 0 and n is odd --> z = x
default { z := x }
let half := div(b, 2) // for rounding.
// n = n / 2, while n > 0, n = n / 2
for { n := div(n, 2) } n { n := div(n, 2) } {
let xx := mul(x, x)
// Check overflow - revert if xx / x != x
if iszero(eq(div(xx, x), x)) { revert(0, 0) }
// Round (xx + half) / b
let xxRound := add(xx, half)
// Check overflow - revert if xxRound < xx
if lt(xxRound, xx) { revert(0, 0) }
x := div(xxRound, b)
// if n % 2 == 1
if mod(n, 2) {
let zx := mul(z, x)
// revert if x != 0 and zx / x != z
if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) {
revert(0, 0)
}
// Round (zx + half) / b
let zxRound := add(zx, half)
// Check overflow - revert if zxRound < zx
if lt(zxRound, zx) { revert(0, 0) }
z := div(zxRound, b)
}
}
}
}
}
}
这段代码展示了如何在 Solidity 中使用汇编语言实现二进制指数运算(Binary Exponentiation)。该方法可以有效地计算 \(x^n\),并在计算过程中考虑了溢出和舍入的情况。
7.23.2 汇编代码解析
- 处理特殊情况:
- 当 \(x = 0\) 时:
- 如果 \(n = 0\),返回 \(1\)(0 的 0 次方定义为 1)。
- 如果 \(n > 0\),返回 \(0\)(任何数的 0 次方是 0)。
- 当 \(x = 0\) 时:
- 一般情况:
- 当 \(x > 0\) 时,根据 \(n\) 的奇偶性初始化 \(z\):
- 如果 \(n\) 是偶数,初始化 \(z\) 为 \(b\)。
- 如果 \(n\) 是奇数,初始化 \(z\) 为 \(x\)。
- 当 \(x > 0\) 时,根据 \(n\) 的奇偶性初始化 \(z\):
- 循环计算:
- 使用一个
for循环,持续将 \(n\) 除以 \(2\),直到 \(n\) 为 0。 - 在每次循环中:
- 计算 \(x^2\)(即 \(xx\)),并检查是否溢出。
- 将 \(xx\) 舍入到最近的基数 \(b\),并更新 \(x\)。
- 如果 \(n\) 是奇数,则计算 \(z\) 的新值,并检查是否溢出。
- 使用一个
- 溢出检查:
- 使用
revert来处理可能的溢出情况,确保计算的结果是有效的。
- 使用
7.24 23 Merkle Airdrop 空投
7.24.1 Merkle Airdrop
以下是使用 Merkle 树的空投合约示例。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {MerkleProof} from "./MerkleProof.sol";
interface IToken {
function mint(address to, uint256 amount) external;
}
contract Airdrop {
event Claim(address to, uint256 amount);
IToken public immutable token; // 代币合约的实例
bytes32 public immutable root; // Merkle 树的根
mapping(bytes32 => bool) public claimed; // 记录是否已领取的映射
constructor(address _token, bytes32 _root) {
token = IToken(_token);
root = _root;
}
function getLeafHash(address to, uint256 amount)
public
pure
returns (bytes32)
{
return keccak256(abi.encode(to, amount));
}
function claim(bytes32[] memory proof, address to, uint256 amount)
external
{
// 注意: (to, amount) 不能有重复
bytes32 leaf = getLeafHash(to, amount);
require(!claimed[leaf], "airdrop already claimed");
require(MerkleProof.verify(proof, root, leaf), "invalid merkle proof");
claimed[leaf] = true;
token.mint(to, amount); // 领取代币
emit Claim(to, amount); // 触发领取事件
}
}
7.24.2 代币合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// ERC20 + mint + 授权功能
contract Token {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
uint256 public totalSupply; // 总供应量
mapping(address => uint256) public balanceOf; // 每个地址的余额
mapping(address => mapping(address => uint256)) public allowance; // 授权额度
string public name; // 代币名称
string public symbol; // 代币符号
uint8 public decimals; // 小数位数
mapping(address => bool) public authorized; // 授权记录
constructor(string memory _name, string memory _symbol, uint8 _decimals) {
name = _name;
symbol = _symbol;
decimals = _decimals;
authorized[msg.sender] = true; // 部署者默认被授权
}
function setAuthorized(address addr, bool auth) external {
require(authorized[msg.sender], "not authorized");
authorized[addr] = auth; // 设置地址的授权状态
}
function transfer(address recipient, uint256 amount)
external
returns (bool)
{
balanceOf[msg.sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(msg.sender, recipient, amount); // 触发转账事件
return true;
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount; // 设置授权额度
emit Approval(msg.sender, spender, amount); // 触发授权事件
return true;
}
function transferFrom(address sender, address recipient, uint256 amount)
external
returns (bool)
{
allowance[sender][msg.sender] -= amount;
balanceOf[sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(sender, recipient, amount); // 触发转账事件
return true;
}
function _mint(address to, uint256 amount) internal {
balanceOf[to] += amount;
totalSupply += amount; // 更新总供应量
emit Transfer(address(0), to, amount); // 触发铸造事件
}
function mint(address to, uint256 amount) external {
require(authorized[msg.sender], "not authorized");
_mint(to, amount); // 铸造代币
}
}
7.24.3 Merkle 证明库
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts
pragma solidity ^0.8.20;
import {Hashes} from "./Hashes.sol";
library MerkleProof {
function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf)
internal
pure
returns (bool)
{
return processProof(proof, leaf) == root; // 验证 Merkle 证明
}
function processProof(bytes32[] memory proof, bytes32 leaf)
internal
pure
returns (bytes32)
{
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
computedHash = Hashes.commutativeKeccak256(computedHash, proof[i]); // 处理证明
}
return computedHash;
}
}
7.24.4 测试合约
```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.26;
library MerkleHelper { // 冒泡排序 function sort(bytes32[] memory arr) internal pure returns (bytes32[] memory) { uint256 n = arr.length; for (uint256 i = 0; i < n; i++) { for (uint256 j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { (arr[j], arr[j + 1]) = (arr[j + 1], arr[j]); } } }
return arr;
}
function yulKeccak256(bytes32 a, bytes32 b)
internal
pure
returns (bytes32 v)
{
assembly {
mstore(0x00, a)
mstore(0x20, b)
v := keccak256(0x00, 0x40) // 计算哈希
}
}
function calcRoot(bytes32[] memory hashes)
internal
pure
returns (bytes32)
{
uint256 n = hashes.length;
while (n > 1) {
for (uint256 i = 0; i < n; i += 2) {
bytes32 left = hashes[i];
bytes32 right = hashes[i + 1 < n ? i + 1 : i];
(left, right) = left <= right ? (left, right) : (right, left);
hashes[i >> 1] = yulKeccak256(left, right); // 计算根哈希
}
n = (n + (n & 1)) >> 1; // 更新 n 的值
}
return hashes[0]; // 返回根
}
function getProof(bytes32[] memory hashes, uint256 index)
internal
pure
returns (bytes32[] memory)
{
bytes32[] memory proof = new bytes32[](0);
uint256 len = 0;
uint256 n = hashes.length;
uint256 k = index;
while (n > 1) {
// 获取当前层的证明
uint256 j = k & 1 == 1 ? k - 1 : (k + 1 < n ? k + 1 : k);
bytes32 h = hashes[j];
// proof.push(h)
assembly {
len := add(len, 1)
let pos := add(proof, shl(5, len))
mstore(pos, h)
mstore(proof, len)
mstore(0x40, add(pos, 0x20))
}
k >>= 1;
// 计算下一层的哈希
for (uint256 i = 0; i < n; i += 2) {
bytes32 left = hashes[i];
bytes32 right = hashes[i + 1 < n ? i + 1 : i];
(left, right) = left <= right ? (left, right) : (right, left);
hashes[i >> 1] = yulKeccak256(left, right);
}
n = (n + (n & 1)) >> 1;
}
return proof; // 返回证明
}
function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf)
internal
pure
returns (bool)
{
bytes32 h = leaf;
for (uint256 i = 0; i < proof.length; i++) {
(bytes32 left, bytes32 right) =
h <= proof[i] ? (h, proof[i]) : (proof[i], h);
h = yulKeccak256(left, right); // 验证证明
}
return h == root; // 检查是否匹配
}
}
// SPDX-License-Identifier: MIT pragma solidity ^0.8.26;
import {Test, console2} from “forge-std/Test.sol”; import {MerkleHelper} from “./MerkleHelper.sol”; import {Airdrop} from “../../../src/app/airdrop/Airdrop.sol”; import {Token} from “../../../src/app/airdrop/Token.sol”;
contract AirdropTest is Test { Token private token;