Solidity基本语法学习5

发布时间 2023-12-13 19:05:07作者: 芜光

文档: https://solidity-by-example.org/
视频教程: https://www.youtube.com/watch?v=xv9OmztShIw&list=PLO5VPQH6OWdVQwpQfw9rZ67O6Pjfo6q-p

说明

Payable

声明payablefunctionaddress可以接收以太进入智能合约。
不要太纠结代码的transfer, call那些.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// Payable合约,允许接收和处理以太币
contract Payable {
    // Payable地址可以通过transfer或send发送以太币
    address payable public owner;

    // Payable构造函数,可以接收以太币
    constructor() payable {
        owner = payable(msg.sender);
    }

    // 函数用于向合约存入以太币。
    // 调用此函数时,请携带一些以太币。
    // 合约的余额将自动更新。
    function deposit() public payable {}

    // 调用此函数时,请携带一些以太币。
    // 由于此函数不可支付,将引发错误。
    function notPayable() public {}

    // 从合约中提取所有以太币的函数。
    function withdraw() public {
        // 获取存储在合约中的以太币数量
        uint amount = address(this).balance;

        // 将所有以太币发送到所有者地址
        (bool success, ) = owner.call{value: amount}("");
        require(success, "发送以太币失败");
    }

    // 从此合约向输入地址转移以太币的函数
    function transfer(address payable _to, uint _amount) public {
        // 请注意,“to”被声明为payable
        (bool success, ) = _to.call{value: _amount}("");
        require(success, "发送以太币失败");
    }
}

由于这个合约没做ether的存储, 所以transfer函数第二个参数是0才能执行.

Sending Ether (transfer, send, call)

发送

发送Ether到其它合约, 可以采取:

  • transfer (2300 gas, throws error)
  • send (2300 gas, returns bool)
  • call (forward all gas or set gas, returns bool)

接收

一个能接收Ether的合约必须要有以下其中一个函数:

  • receive() external payable
  • fallback() external payable
    如果msg.data为空则会调用receive(), 否则调用fallback()

细节

2019年12月之后使用call与重入保护(re-entrancy guard)相结合的方法。
通过在使用可重入保护修饰符调用其他契约之前更改所有状态来防止可重入
重入保护是通过以下措施:

  • 在调用其他合约之前更改所有状态
  • 使用重入保护修饰符(这里我猜测是之前提到的自定义一个locked变量的形式)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract ReceiveEther {
    /*
    哪个函数会被调用,fallback() 还是 receive()?

           发送以太币
               |
         msg.data 是否为空?
              / \
            是  否
            /     \
receive() 是否存在?  fallback()
         /   \
        是   否
        /      \
    receive()   fallback()
    */

    // 函数用于接收以太币。msg.data 必须为空
    receive() external payable {}

    // 当 msg.data 不为空时调用的回退函数
    fallback() external payable {}

    // 获取合约的余额
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

contract SendEther {
    // 通过 transfer 发送以太币的函数
    function sendViaTransfer(address payable _to) public payable {
        // 不再推荐使用此函数发送以太币。
        _to.transfer(msg.value);
    }

    // 通过 send 发送以太币的函数
    function sendViaSend(address payable _to) public payable {
        // send 返回一个布尔值,表示成功或失败。
        // 不推荐使用此函数发送以太币。
        bool sent = _to.send(msg.value);
        require(sent, "Failed to send Ether (sendViaSend func.)");
    }

    // ⭐通过 call 发送以太币的函数
    function sendViaCall(address payable _to) public payable {
        // call 返回一个布尔值,表示成功或失败。
        // 这是当前推荐的使用方法。
        (bool sent, bytes memory data) = _to.call{value: msg.value}("");
        require(sent, "Failed to send Ether (sendViaCall func.)");
    }
}

Fallback

fallback是一个特殊的函数,在以下情况下会被调用:

  • 当一个不存在的函数被调用
  • 以太币被直接发送到一个合约,但receive()不存在或msg.data非空.

当通过transfersend调用时,fallback有2300 gas限制。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Fallback {
    event Log(string func, uint gas);

    // 回退函数必须声明为 external。
    fallback() external payable {
        // send / transfer(将 2300 gas 转发到此回退函数)
        // call(转发所有的 gas)
        emit Log("fallback", gasleft());
    }

    // receive 是回退函数的一种变体,当 msg.data 为空时触发
    receive() external payable {
        emit Log("receive", gasleft());
    }

    // 辅助函数,用于检查合约的余额
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

contract SendToFallback {
    // 通过 transfer 发送以太币到回退函数的函数
    function transferToFallback(address payable _to) public payable {
        _to.transfer(msg.value);
    }

    // 通过 call 发送以太币到回退函数的函数
    function callFallback(address payable _to) public payable {
        (bool sent, ) = _to.call{value: msg.value}("");
        require(sent, "Failed to send Ether. ");
    }
}

fallback可以选择使用bytes作为输入和输出

// SPDX-License-Identifier: MIT
pragma solidity ^0.8;

// TestFallbackInputOutput -> FallbackInputOutput -> Counter
contract FallbackInputOutput {
    address immutable target;

    // 构造函数,接收目标合约地址作为参数
    constructor(address _target) {
        target = _target;
    }

    // 回退函数,接收输入数据并调用目标合约
    fallback(bytes calldata data) external payable returns (bytes memory) {
        // 使用call函数调用目标合约,转发以太币和数据
        (bool ok, bytes memory res) = target.call{value: msg.value}(data);
        require(ok, "failed to call.");
        return res;
    }
}

contract Counter {
    uint public count;

    // 获取计数值的函数
    function get() external view returns (uint) {
        return count;
    }

    // 增加计数值的函数
    function inc() external returns (uint) {
        count += 1;
        return count;
    }
}

contract TestFallbackInputOutput {
    event Log(bytes res);

    // 测试函数,调用传入的回退合约并记录返回结果
    function test(address _fallback, bytes calldata data) external {
        (bool ok, bytes memory res) = _fallback.call(data);
        require(ok, "failed to call");
        emit Log(res);
    }

    // 获取用于测试的数据,分别是Counter合约的get和inc函数的ABI编码
    function getTestData() external pure returns (bytes memory, bytes memory) {
        return (abi.encodeWithSignature("get()"), abi.encodeWithSignature("inc()"));
    }
}

要测试的时候,

  1. 部署Counter合约,
  2. Counter合约的地址作为FallbackInputOutput 合约的构造函数参数
  3. 部署FallbackInputOutput合约
  4. 部署TestFallbackInputOutput合约
  5. 执行getTestData()获取测试数据, 是由Countercount值的Bytes数据.
    测试结果

Call

Call是与其他合约交互的低级函数。
当你只是通过调用fallback函数发送ether时,这是推荐使用的方法。
然而,这不是调用现有函数的推荐方式
不建议使用低级调用的几个原因是:

  1. 不生成回显
  2. 忽略类型检查
  3. 忽略函数存在检查
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// 注意不要把地址弄错, 不然看不到输出call foo的. 
contract Receiver {
    event Received(address caller, uint amount, string message);

    fallback() external payable {
        emit Received(msg.sender, msg.value, "Fallback was called");
    }

    function foo(string memory _message, uint _x) public payable returns (uint) {
        emit Received(msg.sender, msg.value, _message);

        return _x + 1;
    }
}

contract Caller {
    event Response(bool success, bytes data);

    // Let's imagine that contract Caller does not have the source code for the
    // contract Receiver, but we do know the address of contract Receiver and the function to call.
    function testCallFoo(address payable _addr) public payable {
        // You can send ether and specify a custom gas amount
        (bool success, bytes memory data) = _addr.call{value: msg.value, gas: 5000}(
            abi.encodeWithSignature("foo(string,uint256)", "call foo", 123)
        );

        emit Response(success, data);
    }

    // Calling a function that does not exist triggers the fallback function.
    function testCallDoesNotExist(address payable _addr) public payable {
        (bool success, bytes memory data) = _addr.call{value: msg.value}(
            abi.encodeWithSignature("doesNotExist()")
        );

        emit Response(success, data);
    }
}

Delegatecall

delegatecall是一个低级函数,类似于call
当合约A对合约B执行delegatcall时,B的代码将调用合约A的存储storage, msg.sendermsg.value作为参数.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// NOTE: Deploy this contract first
// 就结果来说A和B的deploy顺序没太大区别. 
contract B {
    // NOTE: storage layout must be the same as contract A
    uint public num;
    address public sender;
    uint public value;

    function setVars(uint _num) public payable {
        num = _num;
        sender = msg.sender;
        value = msg.value;
    }
}

contract A {
    uint public num;
    address public sender;
    uint public value;

    function setVars(address _contract, uint _num) public payable {
        // A's storage is set, B is not modified.
        (bool success, bytes memory data) = _contract.delegatecall(
            abi.encodeWithSignature("setVars(uint256)", _num)
        );
    }
}

意思是说A调用了B的函数setVars用来设置自己的状态变量state variable.

Function Selector

当调用函数时,calldata的前4个字节指定要调用哪个函数。
这4个字节称为函数选择器function selector.
以下面的代码为例。它使用call在地址addr的合约上执行transfer
addr.call(abi.encodeWithSignature("transfer(address,uint256)", 0xSomeAddress, 123))
abi.encodeWithSignature(....)返回的前4个字节是函数选择器。
如果在代码中预先计算并内联函数选择器,也许可以节省少量的gas?
下面是函数选择器的计算方式。

视频案例: https://www.youtube.com/watch?v=Mn4e4w8h6n8

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract FunctionSelector {
    /*
    "transfer(address,uint256)"
    0xa9059cbb
    "transferFrom(address,address,uint256)"
    0x23b872dd
    */
    function getSelector(string calldata _func) external pure returns (bytes4) {
        return bytes4(keccak256(bytes(_func)));
    }
}

contract Receiver {
    event Log(bytes data);
    function transfer(address _to, uint amount) external {
        emit Log(msg.data);
        // data: 
        // 前4个字节: 0xa9059cbb
        // 地址: 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4
        // 数据(我输入的是11, 所以最后是b): 
        // 000000000000000000000000000000000000000000000000000000000000000b
    }
}

"transfer(address,uint256)"必须完整, 多一个空格都不行, 不然前4个字节结果就不是0xa9059cbb. 这是给函数编码(encode)后的形式.

Calling Other Contract

合约可以通过两种方式调用其他合约。
最简单的方法是直接调用它,就像A.foo(x, y, z)一样。
另一种调用其他契约的方法是使用低级调用(call)。不推荐使用该方法。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8;

contract Callee{
    uint public x;
    uint public value;
    function setX(uint _x) public returns(uint){
        x = _x;
        return x;
    }
    function getX() external view returns(uint){
        return x;
    }
    function setXandSendEther(uint _x) public payable returns (uint, uint){
        x = _x;
        value = msg.value;
        return (x,value);
    }
}

contract Caller{
    function setX(Callee _callee, uint _x) public {
        uint x = _callee.setX(_x);
    }
    function getX(address _callee, Callee _c) public view returns(uint x){
        uint i = Callee(_callee).getX();
        uint j = _c.getX();
        return i == j ? 1 : 0;
    }
    function setXFromAddress(address _addr, uint _x) public {
        Callee callee = Callee(_addr);
        callee.setX(_x);
    }
    function setXandSendEther(Callee _callee, uint _x) public payable {
        (uint x, uint value) = _callee.setXandSendEther{value: msg.value}(_x);
    }
}

Contract that Creates other Contracts

合约可以通过new关键字由其他合约创建。从0.8.0开始,new关键字通过指定salt选项来支持create2特性。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Car {
    address public owner;
    string public model;
    address public carAddr;

    constructor(address _owner, string memory _model) payable {
        owner = _owner;
        model = _model;
        carAddr = address(this);
    }
}

contract CarFactory {
    Car[] public cars;

    function create(address _owner, string memory _model) public {
        Car car = new Car(_owner, _model);
        cars.push(car);
    }

    function createAndSendEther(address _owner, string memory _model) public payable {
        Car car = (new Car){value: msg.value}(_owner, _model);
        cars.push(car);
    }

    function create2(address _owner, string memory _model, bytes32 _salt) public {
        Car car = (new Car){salt: _salt}(_owner, _model);
        cars.push(car);
    }

    function create2AndSendEther(
        address _owner,
        string memory _model,
        bytes32 _salt
    ) public payable {
        Car car = (new Car){value: msg.value, salt: _salt}(_owner, _model);
        cars.push(car);
    }

    function getCar(
        uint _index
    )
        public
        view
        returns (address owner, string memory model, address carAddr, uint balance)
    {
        Car car = cars[_index];

        return (car.owner(), car.model(), car.carAddr(), address(car).balance);
    }
}