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 主要特点

  1. 唯一性:每个 ERC721 代币都有一个唯一的标识符(Token ID),使其在区块链上不可替代。
  2. 所有权管理:ERC721 标准提供了跟踪代币所有权的机制,可以在不同的用户之间转移代币。
  3. 智能合约:ERC721 代币的创建和管理通过智能合约实现,支持在合约中定义代币的特性和功能。
  4. 可扩展性:可以根据需求扩展代币的功能,例如添加元数据(如艺术品的描述、创作者信息等)。

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 主要特点

  1. 多代币支持:ERC1155 合约可以同时管理多种类型的代币,用户可以在同一合约中创建和转移不同种类的代币,降低了合约部署和交易成本。

  2. 批量操作:支持批量转移和批量查询功能,可以一次性处理多个代币的转移,提高了交易效率。例如,可以同时转移多种资产,而不必逐一操作。

  3. 元数据支持:ERC1155 支持为每种代币提供元数据,允许开发者描述代币的属性和功能,从而增强用户体验。

  4. 更低的 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 主要特点

  1. 免交易费用:用户无需直接支付 gas 费用,降低了参与区块链交易的成本,使更多用户能够轻松进入和使用区块链应用。

  2. 用户体验优化:通过简化转账过程,用户可以在不担心 gas 费用的情况下进行频繁的代币交易,提升了应用的可用性。

  3. 代理合约:通常,gasless 转账通过一个代理合约来实现。用户授权该合约处理他们的代币转账,合约在后台承担了手续费。

  4. 补偿机制:一些实现可能通过补偿机制(如通过 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 主要特点

  1. 字节码表示:合约的所有功能和状态都是通过字节码实现的,这种方式强调了智能合约在以太坊虚拟机(EVM)中的执行过程。

  2. 简化结构:Simple Bytecode Contract 通常包含最少的功能,便于开发者理解基本的合约结构和 EVM 操作。

  3. 可部署性:用户可以将字节码直接部署到以太坊网络中,而无需复杂的编程环境。

  4. 低级交互:通过简单的字节码合约,开发者可以学习如何进行底层的操作,比如状态存储、调用和返回值处理。

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 主要特点

  1. 可预测性:使用 Create2,开发者可以根据给定的参数(如盐值和字节码)预先计算出合约地址。这使得合约地址在部署之前是可知的,有助于其他合约或用户进行交互。

  2. 盐值机制:Create2 允许开发者使用一个自定义的盐值(salt),这一机制增加了生成地址的多样性。不同的盐值和字节码组合将生成不同的合约地址。

  3. 安全性增强:由于可以预先计算合约地址,开发者能够在合约交互中验证地址的正确性,从而降低了合约被错误地址攻击的风险。

  4. 无状态合约: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 主要特点

  1. 节省 Gas 成本:最小代理合约的代码量极小,主要只包含必要的转发逻辑。这使得部署代理合约的 Gas 成本显著低于直接部署完整合约。

  2. 灵活的逻辑更新:代理合约可以指向不同的逻辑合约,从而实现逻辑的更新和替换。这种方式允许开发者在不影响用户地址和状态的情况下,更新合约功能。

  3. 标准化模式:根据 EIP-1167 标准,最小代理合约的实现方法得到了广泛认可,确保了合约的可互操作性。

  4. 透明性:虽然使用代理模式,但用户仍然可以清楚地看到与其交互的逻辑合约地址,保证了透明性。

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 主要特点

  1. 逻辑分离:可升级代理将合约的逻辑与状态分离。逻辑合约包含功能实现,而代理合约则负责转发请求并管理状态。这种结构使得逻辑可以独立更新。

  2. 灵活性和可维护性:通过更改代理合约中的指向逻辑合约的地址,可以轻松升级功能,而无需迁移用户数据或更改合约地址。

  3. 透明性:虽然用户与代理合约交互,但它们可以通过代理合约清晰地了解当前逻辑合约的地址,从而保持透明性。

  4. 标准化:根据 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

  1. Deploy any contract by calling Proxy.deploy(bytes memory _code)
  2. 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 英式拍卖

拍卖流程

  1. NFT的卖家部署该合约。
  2. 拍卖持续7天。
  3. 参与者可以通过存入高于当前最高出价者的ETH进行出价。
  4. 所有出价者如果不是当前最高出价,可以撤回他们的出价。

拍卖结束后

  1. 最高出价者成为NFT的新拥有者。
  2. 卖家收到最高出价的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 荷兰式拍卖

拍卖流程

  1. NFT的卖家部署该合约,并设定NFT的起始价格。
  2. 拍卖持续7天。
  3. NFT的价格随时间降低。
  4. 参与者可以通过存入高于智能合约计算的当前价格的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代币

众筹流程

  1. 用户创建一个众筹活动。
  2. 用户可以进行承诺,将他们的代币转移到活动中。
  3. 当活动结束后,如果承诺的总金额超过活动目标,活动创建者可以领取资金。
  4. 否则,若活动未达到目标,用户可以撤回他们的承诺。
// 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.21.1.2 TestMultiDelegatecall

  • 继承自 MultiDelegatecall,包含具体的函数实现。
  • func1:接受两个参数,计算并记录它们的和,发出事件日志,记录调用者的地址。
  • func2:发出事件日志,并返回一个固定值。
  • mint:允许用户根据发送的以太币数量增加他们的余额。这里存在安全隐患,用户可以通过多次调用来铸造多次代币,代价仅为一次的 msg.value

7.21.1.3 Helper

  • 该合约用于生成调用 TestMultiDelegatecall 中函数的编码数据。
  • getFunc1DatagetFunc2DatagetMintData:这些函数返回对应函数调用的数据编码,以便用户在 multiDelegatecall 中使用。

7.21.2 使用场景

  • 为什么使用 delegatecall 而不是普通调用:普通调用 (call) 会将 msg.sender 设置为调用合约的地址,这可能导致状态变量的访问和上下文问题。而 delegatecall 使得调用函数能够在调用者的上下文中执行,适合需要改变调用者状态的场景。

7.21.3 注意事项

  • 使用 delegatecall 时要小心,因为不安全的实现可能导致重入攻击或意外的状态变化。在 mint 函数中,若不加以控制,用户可能会利用多次调用铸造多次代币,导致不公平的结果。

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.1.1 错误处理

合约中定义了一系列错误,以便在执行过程中能够清晰地识别不同的问题: - NotOwnerError:只有合约拥有者可以执行某些操作。 - AlreadyQueuedError:交易已被排队。 - TimestampNotInRangeError:时间戳不在允许的范围内。 - NotQueuedError:交易未被排队。 - TimestampNotPassedError:时间戳尚未到达。 - TimestampExpiredError:时间戳已过期。 - TxFailedError:交易执行失败。

7.22.1.2 事件

合约中定义了三个事件,用于记录交易的状态: - Queue:交易被排队。 - Execute:交易被执行。 - Cancel:交易被取消。

7.22.1.3 常量

  • MIN_DELAY:最小延迟(10秒)。
  • MAX_DELAY:最大延迟(1000秒)。
  • GRACE_PERIOD:宽限期(1000秒)。

7.22.1.4 状态变量

  • owner:合约的拥有者。
  • queued:一个映射,记录每个交易 ID 是否已被排队。

7.22.1.5 构造函数

在合约创建时,合约拥有者被设定为调用合约的地址。

7.22.2 主要功能

  1. getTxId:生成唯一的交易 ID,基于目标地址、发送的以太币数量、函数签名、数据和时间戳。

  2. queue

    • 接受目标地址、发送的以太币数量、函数名、数据和执行时间戳。
    • 检查交易 ID 是否已经排队,时间戳是否在允许的范围内。
    • 将交易 ID 标记为已排队,并触发 Queue 事件。
  3. execute

    • 执行排队的交易。
    • 检查交易是否已排队,时间戳是否已到达且未过期。
    • 准备调用的数据,如果有函数名,构建函数选择器和数据。
    • 使用低级 call 方法调用目标合约,转账以太币。
    • 如果交易成功,触发 Execute 事件。
  4. cancel

    • 允许合约拥有者取消已排队的交易,并触发 Cancel 事件。

7.22.3 使用场景

时间锁合约通常用于需要推迟交易执行的场景,增加透明度和安全性。通过要求在一定的延迟后才能执行交易,给与社区成员足够的时间进行审查和响应。这在 DAO 和其他去中心化应用中特别有用,以防止恶意操作或错误的操作。

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.1 合约结构

7.23.1.1 合约名称

AssemblyBinExp

7.23.1.2 函数 rpow

  • 参数
    • uint256 x:底数。
    • uint256 n:指数。
    • uint256 b:舍入基数,用于避免整数除法时的精度丢失。
  • 返回
    • uint256 z:计算结果。

7.23.2 汇编代码解析

  1. 处理特殊情况
    • \(x = 0\) 时:
      • 如果 \(n = 0\),返回 \(1\)(0 的 0 次方定义为 1)。
      • 如果 \(n > 0\),返回 \(0\)(任何数的 0 次方是 0)。
  2. 一般情况
    • \(x > 0\) 时,根据 \(n\) 的奇偶性初始化 \(z\)
      • 如果 \(n\) 是偶数,初始化 \(z\)\(b\)
      • 如果 \(n\) 是奇数,初始化 \(z\)\(x\)
  3. 循环计算
    • 使用一个 for 循环,持续将 \(n\) 除以 \(2\),直到 \(n\) 为 0。
    • 在每次循环中:
      • 计算 \(x^2\)(即 \(xx\)),并检查是否溢出。
      • \(xx\) 舍入到最近的基数 \(b\),并更新 \(x\)
      • 如果 \(n\) 是奇数,则计算 \(z\) 的新值,并检查是否溢出。
  4. 溢出检查
    • 使用 revert 来处理可能的溢出情况,确保计算的结果是有效的。

7.23.3 总结

这段代码展示了如何利用汇编语言实现高效的二进制指数运算,同时关注溢出和舍入的问题。使用汇编可以获得更高的性能和更精细的控制,适用于对资源使用有严格要求的区块链应用场景。

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;