【Solidity】合约交互基础

数据的编码与解码

  1. abi.encode:将给定的参数按照 ABI(应用二进制接口)规则编码成字节数组。编码后的数据总是 32 字节的倍数,不足 32 字节的部分会自动填充。适用于合约调用和数据传输。
  2. abi.encodePacked:与 abi.encode 类似。不同的是,它生成的字节数组是压缩过的,不会自动填充到 32 字节的倍数。适用于生成紧凑数据和哈希计算。
  3. abi.decode:将 abi.encode 返回的字节数组解码成原始数据。需要提供数据的类型信息。适用于解析从其他合约接收到的数据。
contract ABIExample {
    uint num = 1;
    address addr = 0x1234567890123456789012345678901234567890;
    string str = "Hello, World!";

    function encodeData() external view returns (bytes memory) {
        return abi.encode(num, addr, str);
    }

    function decodePackedData() external view returns (bytes memory) {
        return abi.encodePacked(num, addr, str);
    }

    function decodeData(
        bytes memory data
    ) external pure returns (uint, address, string memory) {
        (uint a, address b, string memory c) = abi.decode(
            data,
            (uint, address, string)
        );
        return (a, b, c);
    }
}
  1. 部署 ABIExample 合约

  2. 调用 encodeData 方法,获取 num、addr、str 的编码数据

  3. 调用 decodePackedData 方法,获取 num、addr、str 的压缩编码数据,可以看到数据是紧凑的

  4. 传入步骤 2 中获取的编码数据,调用 decodeData 方法,解码数据,可以看到 num、addr、str 的原始数据



函数指针、函数签名、函数选择器

函数指针、函数签名、函数选择器均用于标识函数。


函数指针:指向函数的指针变量,可以用来调用函数

contract Demo {
    uint public num;
    string public str;

    function update(
        uint _num,
        string calldata _str
    ) public returns (uint, string memory) {
        num += _num;
        str = _str;
        return (num, str);
    }
}

contract Test {
    event Log(uint num, string str);

    function callDemoUpdate(
        address _demo,
        uint _num,
        string calldata _str
    ) public {
        // 通过函数指针调用 Demo 合约的 update 函数
        (uint num, string memory str) = Demo(_demo).update(_num, _str);
        emit Log(num, str);
    }
}
  1. 部署 Demo 合约、Test 合约

  2. 传入 Demo 合约的地址、数字、字符串(这里设置为 10 和 “Hello”),调用 Test 合约的 callDemoUpdate 方法

  3. 查看 Demo 合约的 num、str 值,可以看到 num 值加 10、str 值为 “Hello”

  4. 查看 Log 事件,可以看到 Demo 合约的 num、str 值


函数签名:由函数名称和参数类型组成的字符串

上例的函数签名为 update(uint256,string),不能有空格、不能用简写。


函数选择器:函数签名的前 4 个字节

获取方法 1:bytes4(keccak256("update(uint256,string)"))
获取方法 2:Demo(_demo).update.selector



call 方法 & 函数调用数据

call 方法是一个低级函数,能通过 “函数调用数据” 与其他合约进行交互。

有 3 个方法获取 “函数调用数据”:

  1. abi.encodeCall函数指针和参数编码成字节数组,并进行参数类型检查
  2. abi.encodeWithSignature:将函数签名和参数编码成字节数组
  3. abi.encodeWithSelector:将函数选择器和参数编码成字节数组

以上 3 种方法返回的字节数组即 “函数调用数据”。

contract Demo {
    uint public num;
    string public str;

    function update(
        uint _count,
        string calldata _str
    ) public returns (uint, string memory) {
        num += _count;
        str = _str;
        return (num, str);
    }
}

contract Test {
    event Log(uint num, string str);

    function callDemoUpdateByPointer(
        address _demo,
        uint _count,
        string calldata _str
    ) public {
    	// call 搭配 encodeCall 调用 Demo 合约的 update 函数
        (bool success, bytes memory data) = _demo.call(
            abi.encodeCall(Demo(_demo).update, (_count, _str))
        );
        require(success, "call failed");
        (uint num, string memory str) = abi.decode(data, (uint, string));
        emit Log(num, str);
    }

    function callDemoUpdateBySignature(
        address _demo,
        uint _count,
        string calldata _str
    ) public {
        // call 搭配 encodeWithSignature 调用 Demo 合约的 update 函数
        (bool success, bytes memory data) = _demo.call(
            abi.encodeWithSignature("update(uint256,string)", _count, _str)
        );
        require(success, "call failed");
        (uint num, string memory str) = abi.decode(data, (uint, string));
        emit Log(num, str);
    }

    function callDemoUpdateBySelector(
        address _demo,
        uint _count,
        string calldata _str
    ) public {
        bytes4 selector1 = bytes4(keccak256("update(uint256,string)"));
        bytes4 selector2 = Demo(_demo).update.selector;
        // call 搭配 encodeWithSelector 调用 Demo 合约的 update 函数
        (bool success, bytes memory data) = _demo.call(
            abi.encodeWithSelector(selector1, _count, _str)
        );
        require(success, "call failed");
        (uint num, string memory str) = abi.decode(data, (uint, string));
        emit Log(num, str);
    }
}
  1. 部署 Demo 合约、Test 合约

  2. 传入 Demo 合约的地址、数字、字符串(这里设置为 10 和 “Hello”),调用 Test 合约的 callDemoUpdateByPointer / callDemoUpdateBySignature / callDemoUpdateBySelector 方法

  3. 查看 Demo 合约的 num、str 值,可以看到 num 值加 10、str 值为 “Hello”

  4. 查看 Log 事件,可以看到 Demo 合约的 num、str 值



调用函数并传输以太

现有如下合约:

contract Demo {
    uint public balance;
    uint public num;
    string public str;

    function update(
        uint _num,
        string calldata _str
    ) public payable returns (uint, string memory) {
        balance += msg.value;
        num += _num;
        str = _str;
        return (num, str);
    }
}

函数指针调用方法时:

contract Test {
    event Log(uint num, string str);

    function callDemoUpdate(
        address _demo,
        uint _num,
        string calldata _str
    ) public payable {
        // 通过函数指针调用 Demo 合约的 update 方法并传输以太币
        // 配置项 value 为传输的以太币数量下限, 这里设置为 msg.value
        (uint num, string memory str) = Demo(_demo).update{value: msg.value}(
            _num,
            _str
        );
        emit Log(num, str);
    }
}
  1. 部署 Demo 合约、Test 合约

  2. 传入 Demo 合约的地址、数字、字符串(这里设置为 10 和 “Hello”),设置以太币数量(这里设置为 100),调用 Test 合约的 callDemoUpdate 方法

  3. 查看 Demo 合约的 balance、num、str 值,可以看到 balance 值加 100、num 值加 10、str 值为 “Hello”

  4. 查看 Log 事件,可以看到 Demo 合约的 num、str 值


call 调用方法时:

contract Test {
    event Log(uint num, string str);

    function callDemoUpdateBySignature(
        address _demo,
        uint _num,
        string calldata _str
    ) public payable {
        // 通过 call 调用 Demo 合约的 update 函数并传输以太币
        (bool success, bytes memory data) = _demo.call{
            value: 200, // 配置项 value 为传输的以太币数量下限, 这里设置为 200
            gas: 500000 // 配置项 gas 为消耗的 gas 上限, 这里设置为 500000
        }(abi.encodeWithSignature("update(uint256,string)", _num, _str));
        require(success, "call failed");
        (uint num, string memory str) = abi.decode(data, (uint, string));
        emit Log(num, str);
    }
}
  1. 部署 Demo 合约、Test 合约

  2. 传入 Demo 合约的地址、数字、字符串(这里设置为 10 和 “Hello”),设置以太币数量(这里设置为 200),调用 Test 合约的 callDemoUpdateBySignature 方法

  3. 查看 Demo 合约的 balance、num、str 值,可以看到 balance 值加 200、num 值加 10、str 值为 “Hello”

  4. 查看 Log 事件,可以看到 Demo 合约的 num、str 值



Abstract & Interface

abstract contract (抽象合约):如果一个智能合约里面有未实现的函数 (缺少主体 { ... }),则必须将该合约标为 abstract 。另外,未实现的函数需要标为 virtual,以便子合约重写。

interface (接口):类似于抽象合约,但它不实现任何功能。

接口的规则:

  1. 不能包含状态变量

  2. 不能包含构造函数

  3. 不能继承除接口外的其他合约

  4. 所有函数都必须是 external 且不能有函数体

  5. 继承接口的非抽象合约必须实现接口定义的所有功能

我们以 ERC721 的接口合约 IERC721 为例,它定义了 3 个 event 和 9 个 function:

interface IERC721 is IERC165 {
    // 在转账时被释放, 记录代币的发出地址 from、接收地址 to 和 tokenId
    event Transfer(
        address indexed from,
        address indexed to,
        uint256 indexed tokenId
    );

    // 在授权时被释放, 记录授权地址 owner、被授权地址 approved 和 tokenId
    event Approval(
        address indexed owner,
        address indexed approved,
        uint256 indexed tokenId
    );

    // 在批量授权时被释放, 记录批量授权的发出地址 owner、被授权地址 operator 和授权与否的 approved
    event ApprovalForAll(
        address indexed owner,
        address indexed operator,
        bool approved
    );

    // 返回某地址的 NFT 持有量 balance
    function balanceOf(address owner) external view returns (uint256 balance);

    // 返回某 tokenId 的主人 owner
    function ownerOf(uint256 tokenId) external view returns (address owner);

    // 授权另一个地址使用你的 NFT, 参数为被授权地址 approve 和 tokenId
    function approve(address to, uint256 tokenId) external;

    // 查询 tokenId 被批准给了哪个地址
    function getApproved(
        uint256 tokenId
    ) external view returns (address operator);

    // 将自己持有的该系列 NFT 批量授权给某个地址 operator
    function setApprovalForAll(address operator, bool _approved) external;

    // 查询某地址的 NFT 是否批量授权给了另一个 operator 地址
    function isApprovedForAll(
        address owner,
        address operator
    ) external view returns (bool);

    // 普通转账, 参数为转出地址 from、接收地址 to 和 tokenId
    function transferFrom(address from, address to, uint256 tokenId) external;

    // 安全转账 (如果接收方是合约地址, 会要求实现 ERC721Receiver 接口), 参数为转出地址 from、接收地址 to 和 tokenId
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    // 安全转账的重载函数, 参数里面包含了 data
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes calldata data
    ) external;
}

如果我们知道一个合约实现了 IERC721 接口,我们不需要知道它具体代码实现,就可以与它交互。

无聊猿 BAYC 属于 ERC721 代币,实现了 IERC721 接口的功能。我们不需要知道它的源代码,只需知道它的合约地址,用 IERC721 接口就可以与它交互,比如用 balanceOf 来查询某个地址的 BAYC 余额,用 safeTransferFrom 来转账 BAYC 。

contract interactBAYC {
    // 利用 BAYC 地址创建接口合约变量 (ETH 主网)
    IERC721 BAYC = IERC721(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D);

    // 通过接口调用 BAYC 的 balanceOf 查询持仓量
    function balanceOfBAYC(
        address owner
    ) external view returns (uint256 balance) {
        return BAYC.balanceOf(owner);
    }

    // 通过接口调用 BAYC 的 safeTransferFrom 安全转账
    function safeTransferFromBAYC(
        address from,
        address to,
        uint256 tokenId
    ) external {
        BAYC.safeTransferFrom(from, to, tokenId);
    }
}

接口 ID:用于唯一标识接口。

接口 ID 是接口中所有函数选择器的异或(XOR)运算结果。具体步骤如下:

  1. 计算接口中每个函数的选择器(函数签名的 Keccak-256 哈希的前 4 字节)

  2. 对所有函数选择器执行异或运算

计算示例:

interface Solidity101 {
    function hello() external pure;
    function world(int) external pure;
}

contract Selector {
    function calculateSelector() public pure returns (bytes4) {
    Solidity101 i;
    return i.hello.selector ^ i.world.selector;
    }
}



猜你喜欢

转载自blog.csdn.net/Superman_H/article/details/141402854