【Solidity】合约交互基础

发布于:2024-08-22 ⋅ 阅读:(44) ⋅ 点赞:(0)

数据的编码与解码

数据编码:

  1. abi.encode:每个参数都会被填充为 32 字节的数据,并拼接在一起

  2. abi.encodePacked:类似 abi.encode,但会省略其中填充的零

解码:

  1. abi.decode:接受两个参数:编码后的数据和类型列表
contract ABIExample {
    function encodeData() external pure returns (bytes memory) {
        uint a = 1;
        address b = 0x1234567890123456789012345678901234567890;
        string memory c = "Hello, World!";
        return abi.encode(a, b, c);
    }

    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);
    }
}



函数签名 & 函数选择器

function transfer(address recipient, uint amount) external returns (bool);

函数签名:由函数名称和参数类型组成,中间没有空格 - transfer(address,uint)

函数选择器

  1. 为函数签名通过 Keccak-256 哈希计算得到的前 4 个字节 - bytes4(keccak256("transfer(address,uint)"))
  2. 如果不想手动编写函数签名,可以使用 Solidity 的内置函数 this.functionName.selector 来直接获取函数选择器



函数的编码

  1. abi.encodeWithSignature:第一个参数为函数签名,后面的参数为函数参数

  2. abi.encodeWithSelector:第一个参数为函数选择器,后面的参数为函数参数

contract TargetContract {
    uint public value;
    string public message;

    function setValue(uint _value, string memory _message) public {
        value = _value;
        message = _message;
    }
}

contract CallerContract {
    function callSetValue1(
        address target,
        uint _value,
        string memory _message
    ) public {
        // 获取函数签名
        string memory signature = "setValue(uint256,string)";
        // 通过 encodeWithSignature 编码函数签名及传入的参数
        (bool success, ) = target.call(
            abi.encodeWithSignature(signature, _value, _message)
        );
        require(success, "Call failed");
    }

    function callSetValue2(
        address target,
        uint _value,
        string memory _message
    ) public {
        // 函数选择器 (方法 1)
        bytes4 selector1 = bytes4(keccak256("setValue(uint256,string)"));
        // 函数选择器 (方法 2)
        bytes4 selector2 = TargetContract(target).setValue.selector;
        // 通过 encodeWithSelector 编码函数选择器及传入的参数
        (bool success, ) = target.call(
            abi.encodeWithSelector(selector1, _value, _message)
        );
        require(success, "Call failed");
    }
}



直接调用其他合约的方法

contract Demo1 {
    // 方法 1: 通过地址调用
    function setDemo2X_1(address _demo2, uint _x) public {
        Demo2 demo2 = Demo2(_demo2);
        demo2.setX(_x);
    }

    // 方法 2: 通过合约实例调用
    function setDemo2X_2(Demo2 _demo2, uint _x) public {
        _demo2.setX(_x);
    }
}

contract Demo2 {
    uint public x;

    event Log(address caller, uint x);

    function setX(uint _x) public {
        x = _x;
        emit Log(msg.sender, x);
    }
}
  1. 部署 Demo2 合约

  2. 调用 Demo2 合约的 setX 方法,设置 x 值;查看 Demo2 合约的 x 值,可以看到 x 值被更新;查看 Log 事件,可以看到调用者地址为编辑器地址

  3. 部署 Demo1 合约

  4. 传入 Demo1 合约的地址和新 x 值,调用 Demo1 合约的 setDemo2X_1 方法;查看 Demo2 合约的 x 值,可以看到 x 值被更新;查看 Log 事件,可以看到调用者地址为 Demo1 合约地址

  5. 传入 Demo1 合约的地址和新 x 值,调用 Demo1 合约的 setDemo2X_2 方法;查看 Demo2 合约的 x 值,可以看到 x 值被更新;查看 Log 事件,可以看到调用者地址为 Demo1 合约地址


可以在调用的同时传输以太币:

contract Demo1 {
    function setDemo2X(Demo2 _demo2, uint _x) public payable {
        _demo2.setX{value: msg.value}(_x); // 要求: msg.value >= value 值;  这里设置成一样
    }
}

contract Demo2 {
    uint public x;
    uint public value;
    address public caller;

    function setX(uint _x) public payable {
        value = msg.value;
        caller = msg.sender;
        x = _x;
    }
}
  1. 部署 Demo2 合约

  2. 传入新 x 值,设置以太币数量,调用 Demo2 合约的 setX 方法;查看 Demo2 合约的 x 值、value 值、caller 值,可以看到 x 值被更新、value 值为设置的以太币数量、caller 值为编辑器地址

  3. 部署 Demo1 合约

  4. 传入 Demo2 合约的地址、新 x 值,设置以太币数量,调用 Demo1 合约的 setDemo2X 方法;查看 Demo2 合约的 x 值、value 值、caller 值,可以看到 x 值被更新、value 值为设置的以太币数量、caller 值为 Demo1 合约地址



Interface

接口(Interface)用于定义合约之间的交互标准,确保不同合约之间可以互操作。

  1. 接口不能定义状态变量
  2. 接口只声明函数的签名,而不包含函数的实现;所有函数必须声明为 external;不能包含构造函数
  3. 接口可以继承其他接口,但不能继承合约。

现有如下合约交互:

contract Counter {
    uint public count;

    function increment() public {
        count += 1;
    }
}

contract MyContract {
    // 通过地址调用 Counter 合约的 increment 方法
    function incrementCounter(address _counter) public {
        Counter(_counter).increment();
    }

    // 通过地址获取 Counter 合约的状态变量 count
    function getCount(address _counter) public view returns (uint) {
        return Counter(_counter).count();
    }
}

使用接口:

interface ICounter {
    function increment() external;

    function count() external view returns (uint);
}

contract MyContract {
    function incrementCounter(address _counter) public {
        ICounter(_counter).increment();
    }

    function getCount(address _counter) public view returns (uint) {
        return ICounter(_counter).count();
    }
}



通过 call 方法调用其他合约的方法

call 是一个比较底层的方法,可以用来调用其他合约的函数 同时发送以太。

contract TestCall {
    event Log(string _str, uint _num, uint _value, address _sender);

    function foo(
        string calldata _str,
        uint _num
    ) external payable returns (string memory, uint) {
        emit Log(_str, _num, msg.value ,msg.sender);
        return (_str, _num);
    }
}

contract Call {
    bytes public data;

    function testCall(address _addr) public payable {
        (bool success, bytes memory _data) = _addr.call
        {
            // 传输的以太数量; 若设置的以太数量小于该下限, 会报错
            value: 100,
            // gas 上限; 若消耗的 gas 大于该上限, 会报错
            gas: 500000
        }
        (
            // 传入 encodeWithSignature 包装后的调用数据; 第 1 参数是方法签名, 不能有空格, 不能用简写
            abi.encodeWithSignature("foo(string,uint256)", "call foo", 123)
        );
        require(success, "call failed");
        data = _data; // 返回值 _data 是被调用合约的方法的返回值
    }
}
  1. 部署 TestCall 合约

  2. 传入字符串和数字,设置以太币数量,调用 TestCall 合约的 foo 方法;查看 Log 事件,可以看到传入的字符串、数字、以太币数量、调用者地址 (为编辑器地址)

  3. 部署 Call 合约

  4. 传入 TestCall 合约的地址,设置以太币数量,调用 Call 合约的 testCall 方法;查看 Call 合约的 data 值,可以看到 TestCall 合约的 foo 方法的返回值 (为 bytes 形式);查看 Log 事件,可以看到传入的字符串、数字、以太币数量、调用者地址 (为 Call 合约地址)