Solidity教程之进阶篇

Solidity教程之进阶篇
solidity进阶

全局变量(block.timestamp等)

描述区块链信息的全局变量,包括:

区块和交易的属性

函数含义备注
blockhash(uint blockNumber)(byte32)哈希值
block.coinbase(address) 当前块矿工的地址。
block.difficulty(uint)当前块的难度
block.gaslimit(uint)当前块的gaslimit
block.number(uint)当前区块的块号
block.timestamp(uint)当前块的时间戳常用
msg.data(bytes)完整的调用数据(calldata)常用
gasleft()(uint)当前还剩的gas
msg.sender(address)当前调用发起人的地址常用
msg.sig(bytes4)调用数据的前四个字节(函数标识符)常用
msg.value(uint)这个消息所附带的货币量,单位为wei常用
block.timestamp(uint) 当前时间戳常用
tx.gasprice(uint) 交易的gas价格
tx.origin(address)交易的发送者的地址(EOA)常用
全局变量

msg.sender、msg.value、msg.data

当用户发起一笔交易时,相当于向合约发送一个消息(msg),这笔交易可能会涉及到三个重要的全局变量,具体如下:

  1. msg.sender:表示这笔交易的调用者是谁(地址),同一个交易,不同的用户调用,msg.sender不同;
  2. msg.value:表示调用这笔交易时,携带的ether数量,这些以太坊由msg.sender支付,转入到当前合约(wei单位整数);
  3. 注意:一个函数(或地址)如果想接收ether,需要将其修饰为:payable
  4. msg.data:表示调用这笔交易的信息,由函数签名和函数参数(16进制字符串),组成代理模式时常用msg.data(后续讲解)。

msg.sender

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract MsgSender {
    address public owner;
    uint256 public value;
    address public caller;
    constructor() {
        //在部署合约的时候,设置一个全局唯一的合约所有者,后面可以使用权限控制
        owner = msg.sender;
    }
    //1. 对与合约而言,msg.sender是一个可以改变的值,并不一定是合约的创造者
    //2. 任何人调用了合约的方法,那么这笔交易中的from就是当前合约中的msg.sender
    function setValue(uint256 input) public {
        value = input;
        caller = msg.sender;
    }
}

msg.value

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract MsgValue {
    // uint256 public money;
    mapping(address=> uint256) public personToMoney;
    // 函数里面使用了msg.value,那么函数要修饰为payable
    function play() public payable {
        // 如果转账不是100wei,那么参与失败
        // 否则成功,并且添加到维护的mapping中
        require(msg.value == 100, "should equal to 100!");
        personToMoney[msg.sender] = msg.value;
    }
    // 查询当前合约的余额
    function getBalance() public view returns(uint256) {
        return address(this).balance;
    }
}

msg.data

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract MsgData {
    event Data(bytes data, bytes4 sig);
    // input0: addr: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
    // input1: amt : 1
    function transfer(address addr, uint256 amt) public {
        bytes memory data = msg.data;
        // msg.sig 表示当前方法函数签名(4字节)
        // msg.sig 等价于 this.transfer.selector
        emit Data(data, msg.sig);
    }
    //output: 
    // - data: 0xa9059cbb0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc40000000000000000000000000000000000000000000000000000000000000001
    // - sig: 0xa9059cbb
    // 对data进行分析:
    // 0xa9059cbb //前四字节
    // 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4 //第一个参数占位符(32字节)
    // 0000000000000000000000000000000000000000000000000000000000000001 //第二个参数占位符(32字节)
}

payable

  1. 一个函数(或地址)如果想接收ether,需要将其修饰为:payable
  2. address常用方法:
  3. balance(): 查询当前地址的ether余额
  4. transfer(uint): 合约向当前地址转指定数量的ether,如果失败会回滚
  5. send(uint): 合约向当前地址转指定数量的ether,如果失败会返回false,不回滚(不建议使用send)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Payable {
    // 1. Payable address can receive Ether
    address payable public owner;
    // 2. Payable constructor can receive Ether
    constructor() payable {
        owner = payable(msg.sender);
    }
    // 3. Function to deposit Ether into this contract.
    function deposit() public payable {}
    // 4. Call this function along with some Ether.
    // The function will throw an error since this function is not payable.
    function notPayable() public {}
    // 5. Function to withdraw all Ether from this contract.
    function withdraw() public {
        uint amount = address(this).balance;
        owner.transfer(amount);
    }
    // 6. Function to transfer Ether from this contract to address from input
    function transfer(address payable _to, uint _amount) public {
        _to.transfer(_amount);
    }
}

Try on Remix

abi.encode、abi.decode、abi.encodePacked

abi.encode:可以将data编码成bytes,生成的bytes总是32字节的倍数,不足32为会自动填充(用于给合约调用);

abi.decode:可以将bytes解码成data(可以只解析部分字段)

abi.encodePacked:与abi.encode类似,但是生成的bytes是压缩过的(有些类型不会自动填充,无法传递给合约调用)。

手册:https://docs.soliditylang.org/en/v0.8.13/abi-spec.html?highlight=abi.encodePacked#non-standard-packed-mode

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract AbiDecode {
    struct MyStruct {
        string name;
        uint[2] nums;
    }
    // input: 10, 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, [1,"2",3], ["duke", [10,20]]
    // output: 0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc40000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000064756b6500000000000000000000000000000000000000000000000000000000
    //  output长度:832位16进制字符(去除0x),832 / 32 = 26 (一定是32字节的整数倍,不足填0)
    function encode(
        uint x,
        address addr,
        uint[] calldata arr,
        MyStruct calldata myStruct
    ) external pure returns (bytes memory) {
        return abi.encode(x, addr, arr, myStruct);
    }
    function decode(bytes calldata data)
        external
        pure
        returns (
            uint x,
            address addr,
            uint[] memory arr,
            MyStruct memory myStruct
        )
    {
        (x, addr, arr, myStruct) = abi.decode(data, (uint, address, uint[], MyStruct));
        /* decode output: 
            0: uint256: x 10
            1: address: addr 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
            2: uint256[]: arr 1,2,3
            3: tuple(string,uint256[2]): myStruct ,10,20
        */
    }
    // 可以只decode其中部分字段,而不用全部decode,当前案例中,只有第一个字段被解析了,其余为默认值
    function decodeLess(bytes calldata data)
        external
        pure
        returns (
            uint x,
            address addr,
            uint[] memory arr,
            MyStruct memory myStruct
        )
    {
        (x) = abi.decode(data, (uint));
        /* decode output: 
            0: uint256: x 10
            1: address: addr 0x0000000000000000000000000000000000000000
            2: uint256[]: arr
            3: tuple(string,uint256[2]): myStruct ,0,0
        */
    }
    // input: -1, 0x42, 0x03, "Hello, world!"
    function encodePacked(
        int16 x,
        bytes1 y,
        uint16 z,
        string memory s
    ) external view returns (bytes memory) {
        // encodePacked 不支持struct和mapping
        return abi.encodePacked(x, y, z, s);
        /*
        0xffff42000348656c6c6f2c20776f726c6421
          ^^^^                                 int16(-1)
              ^^                               bytes1(0x42)
                ^^^^                           uint16(0x03)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^ string("Hello, world!") without a length field
        */
    }
}

使用三方库:

web3js编码:

// encodeFunctionCall( abi ,参数 ) 得到编码
web3.eth.abi.encodeFunctionCall({
    name: 'myMethod',
    type: 'function',
    inputs: [{
        type: 'uint256',
        name: 'myNumber'
    },{
        type: 'string',
        name: 'myString'
    }]
}, ['2345675643', 'Hello!%']);
> "0x24ee0097000000000000000000000000000000000000000000000000000000008bd02b7b0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000748656c6c6f212500000000000000000000000000000000000000000000000000"

web3js解码:

解码时,要将得到的calldata的前四字节去掉,那是函数的selector,不应该参与参数解析。

//  let res = web3.eth.abi.decodeParameters(abi, calldata)
async function main() {
  let calldata = '' //太大了,删除,去下面线上链接中获取
  let abi = [{ "internalType": "string", "name": "_id", "type": "string" }, { "internalType": "string", "name": "_uniqueId", "type": "string" }, { "internalType": "uint8", "name": "_assetFrom", "type": "uint8" }, { "internalType": "uint8", "name": "_action", "type": "uint8" }, { "internalType": "address", "name": "_srcToken", "type": "address" }, { "internalType": "address", "name": "_dstToken", "type": "address" }, { "internalType": "uint256", "name": "_srcAmount", "type": "uint256" }, { "internalType": "uint256", "name": "_srcFeeAmount", "type": "uint256" }, { "internalType": "bytes", "name": "_data", "type": "bytes" }]

  let res = web3.eth.abi.decodeParameters(abi, calldata)
  console.log('res:', res)
}

在线案例:https://web3playground.io/QmSeHtJPLFxweiGB8ocFDXkCZao6Mt5oEJ4Ej66iY3RL1R

ethersjs

let bytes2 = mock1Inch.interface.encodeFunctionData("swap", [ETH_ADDR, daiToken.address, 90])

call

call是一种底层调用合约的方式,可以在合约内调用其他合约,call语法为:

//(bool success, bytes memory data) = addr.call{value: valueAmt, gas: gasAmt}(abi.encodeWithSignature("foo(string,uint256)", 参数1, 参数2)
其中:
1. success:执行结果,一定要校验success是否成功,失败务必要回滚
2. data:执行调用的返回值,是打包的字节序,需要解析才能得到调用函数的返回值(后续encode_decode详解)

当调用fallback方式给合约转ether的时候,建议使用call,而不是使用transfer或send方法

(bool success, bytes memory data) = addr.call{value: 10}("")

对于存在的方法,不建议使用call方式调用。

(bool success, bytes memory data) = _addr.call(abi.encodeWithSignature("doesNotExist()"));

调用不存在的方法(又不存在fallback)时,交易会调用成功,但是第一个参数为:false。

完整demo:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
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);
    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 _addr) public {
        (bool success, bytes memory data) = _addr.call(
            abi.encodeWithSignature("doesNotExist()")
        );
        emit Response(success, data);
    }
}

Try on Remix

keccak256哈希算法

keccak256用于计算哈希,属于sha3算法,与sha256(属于sha2算法不同),keccak256使用场景如下:

  1. 用于生成唯一id;
  2. 生成数据指纹;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract HashFunction {
    function hash(
        string memory _text,
        uint _num,
        address _addr
    ) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(_text, _num, _addr));
    }
    // Example of hash collision
    // Hash collision can occur when you pass more than one dynamic data type
    // to abi.encodePacked. In such case, you should use abi.encode instead.
    function collision(string memory _text, string memory _anotherText)
        public
        pure
        returns (bytes32)
    {
        // encodePacked(AAA, BBB) -> AAABBB
        // encodePacked(AA, ABBB) -> AAABBB
        return keccak256(abi.encodePacked(_text, _anotherText));
    }
}
contract GuessTheMagicWord {
    bytes32 public answer =
        0x60298f78cc0b47170ba79c10aa3851d7648bd96f2f8e46a19dbc777c36fb0c00;
    // Magic word is "Solidity"
    function guess(string memory _word) public view returns (bool) {
        return keccak256(abi.encodePacked(_word)) == answer;
    }
}

Try on Remix

selector

  1. 当调用某个function时,具体调用function的信息会被拼装成calldata,calldata的前4个字节就是这个function的selector,其中:
  2. 通过selector可以知道调用的是哪个function;
  3. calldata 可以通过msg.data获取。
  4. 通过拼装selector和函数参数,我们可以在A合约中得到calldata,并在A合约中通过call方法去调用B合约中的方法,从而实现合约间的调用。举例,下面的代码功能是:在当前合约中使用call调用addr地址中的transfer方法:
   // 在合约中,一个function的4字节selector可以通过abi.encodeWithSignature(...)来获取
   // "0xa9059cbb"
   bytes memory transferSelector = abi.encodeWithSignature("transfer(address,uint256)");
   // 调用合约
   addr.call(transferSelector, 0xSomeAddress, 100); 
   // 一般会写成一行
   // addr.call(abi.encodeWithSignature("transfer(address,uint256)"), 0xSomeAddress, 100);
  1. 另外一种计算selector的方式为:(keccak256即sha3哈希算法)
   bytes4(keccak256(bytes("transfer(address,uint256)"))) //不用关心返回值,不用放在这里面计算。
  1. 在合约内部也可以直接获取selector
   // 假设当前合约内有transfer函数
   this.transfer.selector
  1. 也可以使用abi.encodeCall方式获取
   interface IERC20 {
           function transfer(address, uint) external;
   }
   function encodeCall(address to, uint amount) external pure returns (bytes memory) {
         // Typo and type errors will not compile
         return abi.encodeCall(IERC20.transfer, (to, amount));
   }

完整demo:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
interface IERC20 {
    function transfer(address, uint) external;
}
contract FunctionSelector {
    event Selector(bytes4 s1, bytes4 s2);
    //_func示例值: "transfer(address,uint256)"
    function getSelector(string calldata _func) external pure returns (bytes4, bytes memory) {
        bytes4 selector1 = bytes4(keccak256(bytes(_func)));
        bytes memory selector2 = abi.encodeWithSignature(_func);
        // 两者相同
          // 0: bytes4: 0xa9059cbb
        // 1: bytes: 0xa9059cbb
        return (selector1, selector2);
    }
       function encodeWithSignature(address to, uint amount)
        external
        pure
        returns (bytes memory)
    {
        // Typo is not checked - "transfer(address, uint)",不会检查参数类型
        return abi.encodeWithSignature("transfer(address,uint256)", to, amount);
    }
    function encodeWithSelector(address to, uint amount)
        external
        pure
        returns (bytes memory)
    {
        // Type is not checked - (IERC20.transfer.selector, true, amount) ,不会检查to, amount类型
        return abi.encodeWithSelector(IERC20.transfer.selector, to, amount);
    }
    function encodeCall(address to, uint amount) external pure returns (bytes memory) {
        // Typo and type errors will not compile,校验最严格
        return abi.encodeCall(IERC20.transfer, (to, amount));
    }
}

补充知识点

  1. 一般提到signature的方法指的是函数原型:
   "transfer(address,uint256)"
  1. 一般提到selector指的是前4字节
   // bytes4(keccak256(bytes("transfer(address,uint256)")))
   "Function sig:" "0xa9059cbb"
  1. 链下方式计算selector(详见在web3.js和ether.js章节),点击执行demo
   async function main() {
     let transferEvent = "Transfer(address,address,uint256)"
     let sig1 = web3.eth.abi.encodeEventSignature(transferEvent)
     let sig2 = web3.eth.abi.encodeFunctionSignature(transferEvent)
     // 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
     console.log('event sig1:', sig1)
     // 0xddf252ad
     console.log('event sig2:', sig2)
     let transferFun = "transfer(address,uint256)"
     let sig3 = web3.eth.abi.encodeFunctionSignature(transferFun)
     // 0xa9059cbb,差一个字母千差万别
     console.log('Function sig:', sig3)
   }
  1. 哈希算法:
   // keccak256与sha3和算法相同  ==》  brew install sha3sum
   // sha256属于sha2系列(与sha3不同)。

Try on Remix

Send Ether(transfer、send、call)

https://docs.soliditylang.org/en/latest/security-considerations.html#sending-and-receiving-ether

如何发送ether?

有三种方式可以向合约地址转ether:

  1. transfer(2300 gas, throw error)
  2. send(2300 gas,return bool)
  3. call(传递交易剩余的gas或设置gas,不限定2300gas,return bool)(推荐使用)

如何接收ether?

想接收ether的合约至少包含以下方法中的一个:

  1. receive() external payable:msg.data为空时调用
  2. fallback() external payable:msg.data非空时调用
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract ReceiveEther {
    /*
    Which function is called, fallback() or receive()?
                sender ether
                    |
             msg.data is empty?
                /       \
            yes          no
             /             \
      receive() exist?     fallback()
          /    \
        yes     no
       /          \
  receive()     fallback()
  */
    string public message;
    // Function to receive Ether. msg.data must be empty
    receive() external payable {
        message = "receive called!";
    }
    // Fallback function is called when msg.data is not empty
    fallback() external payable {
        message = "fallback called!";
    }
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
    function setMsg(string memory _msg) public {
        message = _msg;
    }
}
contract SendEther {
    function sendViaTransfer(address payable _to) public payable {
        // This function is no longer recommended for sending Ether. (不建议使用)
        _to.transfer(msg.value);
    }
    function sendViaSend(address payable _to) public payable {
        // Send returns a boolean value indicating success or failure.
        // This function is not recommended for sending Ether. (不建议使用)
        bool sent = _to.send(msg.value);
        require(sent, "Failed to send Ether");
    }
    function sendViaCallFallback(address payable _to) public payable {
        // Call returns a boolean value indicating success or failure.
        // This is the current recommended method to use. (推荐使用)
        (bool sent, bytes memory data) = _to.call{value: msg.value}(abi.encodeWithSignature("noExistFuncTest()"));
        require(sent, "Failed to send Ether");
    }
    function sendViaCallReceive(address payable _to) public payable {
        // Call returns a boolean value indicating success or failure.
        // This is the current recommended method to use.(推荐使用)
        (bool sent, bytes memory data) = _to.call{value: msg.value}("");
        require(sent, "Failed to send Ether");
    }
}

Try on Remix

delegatecall

delegatecall与call相似,也是底层调用合约方式,特点是:

  1. 当A合约使用delegatecall调用B合约的方法时,B合约的代码被执行,但是使用的是A合约的上下文,包括A合约的状态变量,msg.sender,msg.value等;
  2. 使用delegatecall的前提是:A合约和B合约有相同的状态变量。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
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;
    }
}
// 注意:执行后,sender值为EOA的地址,而不是A合约的地址  (调用链EOA-> A::setVars -> B::setVars)
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)
        );
    }
}

Try on Remix

fallback

  1. fallback是特殊的函数,无参数,无返回值;
  2. 何时会被调用:
  3. 当被调用的方法不存在时,fallback会被调用,属于default函数;
  4. 当向合约转ether但是合约不存在receive函数时;
  5. 当向合约转ether但是msg.data不为空时。(即使receive存在)
  6. 当使用transfer或者send对合约进行转账时,fallback函数的gaslimit限定为2300 gas
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Fallback {
    event Log(uint gas);
    // Fallback function must be declared as external.
    fallback() external payable {
        // send / transfer (forwards 2300 gas to this fallback function)
        // call (forwards all of the gas)
        emit Log(gasleft());
    }
    // Helper function to check the balance of this contract
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}
contract SendToFallback {
    function transferToFallback(address payable _to) public payable {
          // Log event:  "gas": "2254"
        _to.transfer(msg.value);
    }
    function callFallback(address payable _to) public payable {
        // Log event:  "gas": "6110"
        (bool sent, ) = _to.call{value: msg.value}("");
        require(sent, "Failed to send Ether");
    }
     function callNoExistFunc(address payable _to) public payable {
        // call no exist funtion will call fallback by default 
        // Log event:  "gas": "5146"
        (bool sent, ) = _to.call{value: msg.value}(abi.encodeWithSignature("noExistFunc()"));
        require(sent, "Failed to call");
    }
}

Try on Remix

合约间调用

普通的交易,相当于在世界状态中修改原有的账户数据,更新到新状态。

一共有三种方式调用合约:

  1. 使用合约实例调用合约(常规):A.foo(argument)
  2. 使用call调用合约: A.call(calldata)
  3. 使用delegate调用合约:A.delegatecall(calldata)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Callee {
    uint public x;
    uint public value;
    function setX(uint _x) public returns (uint) {
        x = _x;
        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);
    }
    // 传递地址,在内部实例化callee合约
    function setXFromAddress(address _addr, uint _x) public {
        Callee callee = Callee(_addr);
        callee.setX(_x);
    }
    // 调用方法,并转ether
    function setXandSendEther(Callee _callee, uint _x) public payable {
        (uint x, uint value) = _callee.setXandSendEther{value: msg.value}(_x);
    }
}

new合约

创建合约时,在世界状态中,增加一个地址与账户的信息。

在合约内部可以使用关键字new创建新的合约;

在0.8.0版本之后,new增加了salt选项,从而支持了create2的特性(通过salt可以计算出创建合约的地址)。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
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);
    }
}

delete

  1. delete操作符可以用于任何变量(map除外),将其设置成默认值;
  2. 如果对动态数组使用delete,则删除所有元素,其长度变为0: uint[ ] array0 ; arry0 = new uint
  3. 如果对静态数组使用delete,则重置所有索引的值,数组长度不变: uint[10] array1 = [1,2,3,4,5,6];
  4. 如果对map类型使用delete,什么都不会发生;
  5. 但如果对map类型中的一个键使用delete,则会删除与该键相关的值。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract  Delete {
    //01. string 
    string public str1 = "hello";
    function deleteStr() public {
        delete str1;
    }
    function setStr(string memory input) public {
        str1 = input;
    }
    //02. array 对于固定长度的数组,会删除每个元素的值,但是数组长度不变
    uint256[10] public arry1 = [1,2,3,4,5];
    function deleteFiexedArry() public {
        delete arry1;
    }
    //03. array new
    uint256[] arry2 ;
    function setArray2() public {
        arry2 = new uint256[](10);
        for (uint256 i = 0; i< arry2.length; i++) {
            arry2[i] = i;
        }
    }
    function getArray2() public view returns(uint256[] memory) {
        return arry2;
    }
    function deleteArray2() public {
        delete arry2;
    }
    //04. mapping
    mapping(uint256 => string) public m1;
    function setMap() public {
        m1[0] = "hello";
        m1[1] = "world";
    }
    //Mapping不允许直接使用delete,但是可以对mapping的元素进行指定删除
    // function deleteM1() public {
    //     delete m1;
    // }    
    function deleteMapping(uint256 i) public {
        delete m1[i];
    }
}

try/catch

try/catch仅可以捕捉在调用external函数或创建合约中抛出的异常信息。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// External contract used for try / catch examples
contract Foo {
    address public owner;
    constructor(address _owner) {
        require(_owner != address(0), "invalid address");
        assert(_owner != 0x0000000000000000000000000000000000000001);
        owner = _owner;
    }
    function myFunc(uint x) public pure returns (string memory) {
        require(x != 0, "require failed");
        return "my func was called";
    }
}
contract Bar {
    event Log(string message);
    event LogBytes(bytes data);
    Foo public foo;
    constructor() {
        // This Foo contract is used for example of try catch with external call
        foo = new Foo(msg.sender);
    }
    // Example of try / catch with external call
    // tryCatchExternalCall(0) => Log("external call failed")
    // tryCatchExternalCall(1) => Log("my func was called")
    function tryCatchExternalCall(uint _i) public {
        try foo.myFunc(_i) returns (string memory result) {
            emit Log(result);
        } catch {
            emit Log("external call failed");
        }
    }
    // Example of try / catch with contract creation
    // tryCatchNewContract(0x0000000000000000000000000000000000000000) => Log("invalid address")
    // tryCatchNewContract(0x0000000000000000000000000000000000000001) => LogBytes("")
    // tryCatchNewContract(0x0000000000000000000000000000000000000002) => Log("Foo created")
    function tryCatchNewContract(address _owner) public {
        try new Foo(_owner) returns (Foo foo) {
            // you can use variable foo here
            emit Log("Foo created");
        } catch Error(string memory reason) {
            // catch failing revert() and require()
            emit Log(reason);
        } catch (bytes memory reason) {
            // catch failing assert()
            emit LogBytes(reason);
        }
    }
}

没有错误信息的时候,直接使用catch比较稳妥,如果使用reason,会捕捉require发出的信息。

import

我们可以使用import将本地的.sol文件或外部(github或openzeppelin等).sol导入进来

├── Import.sol
└── Foo.sol

Fool.sol

常量、函数、枚举、结构体、Error可以定义在合约之外;事件、变量不允许定义在合约之外。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
struct Point {
    uint x;
    uint y;
}
// 事件不允许定义在合约之外
// event Greeting(string);
error Unauthorized(address caller);
string constant greeting = "hell world";
function add(uint x, uint y) pure returns (uint) {
    return x + y;
}
contract Foo {
    string public name = "Foo";
}

Import.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// import Foo.sol from current directory
import "./Foo.sol";
// import {symbol1 as alias, symbol2} from "filename";
import {Unauthorized, add as func, Point} from "./Foo.sol";
contract Import {
    // Initialize Foo.sol
    Foo public foo = new Foo();
    // Test Foo.sol by getting it's name.
    function getFooName() public view returns (string memory) {
        return foo.name();
    }
    function myAdd() public pure returns(uint) {
        return func(1,2);
    }
    function greetingCall() public pure returns(string memory) {
        return greeting;
    }
}

导入外部文件:

// https://github.com/owner/repo/blob/branch/path/to/Contract.sol
import "https://github.com/owner/repo/blob/branch/path/to/Contract.sol";
// Example import ECDSA.sol from openzeppelin-contract repo, release-v4.5 branch
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol";

Try on Remix

节约gas

  1. 使用calldata替换memory
  2. 将状态变量加载到memory中
  3. 使用++i替换i++
  4. 对变量进行缓存
  5. 短路效应
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// gas golf
contract GasGolf {
    // start - 50908 gas
    // use calldata - 49163 gas
    // load state variables to memory - 48952 gas
    // short circuit - 48634 gas
    // loop increments - 48244 gas
    // cache array length - 48209 gas
    // load array elements to memory - 48047 gas
    uint public total;
    // start - not gas optimized
    // function sumIfEvenAndLessThan99(uint[] memory nums) external {
    //     for (uint i = 0; i < nums.length; i += 1) {
    //         bool isEven = nums[i] % 2 == 0;
    //         bool isLessThan99 = nums[i] < 99;
    //         if (isEven && isLessThan99) {
    //             total += nums[i];
    //         }
    //     }
    // }
    // gas optimized
    // [1, 2, 3, 4, 5, 100]
    function sumIfEvenAndLessThan99(uint[] calldata nums) external {
        uint _total = total;
        uint len = nums.length;
        for (uint i = 0; i < len; ++i) {
            uint num = nums[i];
            if (num % 2 == 0 && num < 99) {
                _total += num;
            }
        }
        total = _total;
    }
}

十种节约方法

一共两种类型的Gas需要消耗,他们是:

  1. 调用合约
  2. 部署合约

有时候,减少了一种类型的gas会导致另一种类型gas的增加,我们需要进行权衡(Tradeoff)利弊,主要优化方向:

  1. Minimize on-chain data (events, IPFS, stateless contracts, merkle proofs) -> 优化链上存储
  2. Minimize on-chain operations (strings, return storage value, looping, local storage, batching) -> 优化链上操作
  3. Memory Locations (calldata, stack, memory, storage) -> 数据位置的选择
  4. Variables ordering -> 变量的定义顺序很重要
  5. Preferred data types -> 数据类型的选择
  6. Libraries (embedded, deploy) -> 尽量使用库来减少部署gas
  7. Minimal Proxy -> 使用clone方式创建新合约
  8. Constructor -> 优化构造函数(尽量使用constant)
  9. Contract size (messages, modifiers, functions) -> 优化合约size
  10. Solidity compiler optimizer -> 开启优化中

1. Minimize on-chain data

  • Event:如果链上合约不需要调用的数据,可以使用event,由链下监听,提供只读操作;
  • IPFS:大数据可以上传到ipfs,然后将对应的id存储在链上;
  • 无状态合约:如果只是为了存储key-value,那么在合约中不需要状态变量存储,而是仅仅通过参数记录,让链下程序去解析交易,读取参数,从而读取到key-value数据;
  • 默克尔根(Merkle Proofs):快速验证数据,合约不用存储太多内容。

2. Minimize on-chain operations

  • string:string内在也是bytes,尽量使用bytes替代,可以减少EVM计算,减少gas消耗;
  • 返回storage值:直接返回storage如果有必要的话,具体内部数据,让链下程序解析;
  • Local Storage:使用local storage变量,可节约开销,不要使用memory进行copy一遍操作;
  • Batching(批量操作):如果有批量操作需要,可以提供相应接口,避免用户发起相同交易。

3. Memory locations

四种存储位置gas消耗(由低到高):calldata -> stack -> memory -> storage.

  • Calldata:一般用在参数中,修饰引用数据类型(array、string),限定external function,尽量使用,便宜;
  • Stack:函数体中值类型的数据,自动修饰为stack类型;
  • Memory:对于存储引用类型的数据时,完全拷贝(你没有看错,反而更便宜)比storage便宜;
  • Storage:最贵,非必要,不使用。

4. Variables ordering

  • Storage slots(槽)大小是32字节,但并不是所有的类型都能填满(bool,int8等);
  • 调整顺序,可以优化storage使用空间:
  • uint128、uint128、uint256,一共使用两个槽位(good)✅
  • uint128、uint256、uint128,一共使用三个槽位(bad)❌

5. Preferred data types

  • 如果定义变量的类型原本可以填满整个槽位,那么就填满ta,而不要使用更短的数据类型。
  • 例如:如果定义数据类型:datatype:uint8,但是opcode原则上是处理:uint256的,那么会对空余部分填充:0,这反而会增加evm对gas的开销,所以更好的方法是:直接定义datatype为:uint256。

6. Libraries

库有两种表现形式:

  • Embedded Libraries:当lib中的方法都是internal的时候,会自动内联到合约中,此时对节约gas不起作用;
  • Deployed Libraries:当lib中有public或external方法时,此时会单独部署lib合约,我们可以使用一个lib地址关联到不同合约来达到节约gas的目的。

7. Minimal Proxies (ERC 1167)

  • 这是一个标准,用于clone合约
  • openzeppelin合约中clone就源于此

8. Constructor

  • 构造函数中可以传递immutable数据,如果可能,尽量使用constant,这样开销更小。

9. Contract Size

  • 合约最大支持24K
  • 减少Logs/ Message:require后面的des,event的使用,都影响合约size
  • 使用opcode:这个看情况而定,opcode可能减少部署开销,却引来调用开销的增加。
  • 修饰器Modifier:modifer中wrapped一个函数,在函数中实现具体逻辑 // TODO

10. Solidity compiler optimizer

  • 开启编译器optimize,这个是有双面性的,一定会使得合约size变小;
  • 但是可能会使部分函数的逻辑变复杂(code bigger),增加函数的执行开销。

参考链接:https://medium.com/coinmonks/smart-contracts-gas-optimization-techniques-2bd07add0e86

type

https://docs.soliditylang.org/en/v0.6.5/units-and-global-variables.html#meta-type

type(x) 可以返回x类型的对象信息,例如:

  • type(x).name: 合约的名字;
  • type(x).creattionCode: 合约部署时的bytecode;
  • type(x).runtimeCode: 合约运行时的bytecode,一般是构造函数数据,但是当constructor中有汇编时会有不同(没有仔细了解)。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/**
 * @title Storage
 * @dev Store & retrieve value in a variable
 */
contract Storage {
    string public str;
    constructor(string memory _str) {
        str = _str;
    }
    uint256 number;

    /**
     * @dev Store value in variable
     * @param num value to store
     */
    function store(uint256 num) public {
        number = num;
    }
    /**
     * @dev Return value 
     * @return value of 'number'
     */
    function retrieve() public view returns (uint256){
        return number;
    }
    function getInfo() public pure returns(string memory name) {
        name = type(Storage).name;
        // creationCode 和runtimeCode不能在这个合约自己内部使用,防止会出现循环调用问题
        // creationCode = type(Storage).creationCode;
        // runtimeCode = new type(Storage).runtimeCode;
    }
}
contract TestStorage {
    Storage s;
    constructor(Storage _address) {
        s = _address;
    }
    function getInfo() public view returns(bytes memory creationCode, bytes memory runtimeCode) {
        creationCode = type(Storage).creationCode;
        runtimeCode = type(Storage).runtimeCode;
    }
}

汇编

  1. 汇编在写库的时候很实用,提供更加细颗粒度的编写
  2. 汇编语言为Yul语法
  3. 使用assembly { … }包裹

节约gas

测试:input: [1,2,3]

  • 23374 gas -> sumSolidity(最低效)
  • 23082 gas -> sumAsm(次之)
  • 22895 gas -> sumPureAsm(最高效)
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
library VectorSum {
    // This function is less efficient because the optimizer currently fails to
    // remove the bounds checks in array access.
    function sumSolidity(uint[] memory data) public pure returns (uint sum) {
        for (uint i = 0; i < data.length; ++i)
            sum += data[i];
    }
    // We know that we only access the array in bounds, so we can avoid the check.
    // 0x20 needs to be added to an array because the first slot contains the
    // array length.
    function sumAsm(uint[] memory data) public pure returns (uint sum) {
        for (uint i = 0; i < data.length; ++i) {
            assembly {
                sum := add(sum, mload(add(add(data, 0x20), mul(i, 0x20))))
            }
        }
    }
    // Same as above, but accomplish the entire code within inline assembly.
    function sumPureAsm(uint[] memory data) public pure returns (uint sum) {
        assembly {
            // Load the length (first 32 bytes)
            let len := mload(data)
            // Skip over the length field.
            //
            // Keep temporary variable so it can be incremented in place.
            //
            // NOTE: incrementing data would result in an unusable
            //       data variable after this assembly block
            let dataElementLocation := add(data, 0x20)
            // Iterate until the bound is not met.
            for
                { let end := add(dataElementLocation, mul(len, 0x20)) }
                lt(dataElementLocation, end)
                { dataElementLocation := add(dataElementLocation, 0x20) }
            {
                sum := add(sum, mload(dataElementLocation))
            }
        }
    }
}

其他文章

  1. https://jeancvllr.medium.com/solidity-tutorial-all-about-assembly-5acdfefde05c

Event Log

概述

https://ethereum.stackexchange.com/questions/3418/how-does-ethereum-make-use-of-bloom-filters/3426

  • Log也在区块链账本中,但是它不是链上数据;
  • Log是由交易执行产生的数据,它是不需要共识的,可以通过重新执行交易生成;
  • Log是经由链上校验的,无法造假,因为一笔交易的ReceiptHash是存在链上的(Header中)
  1. 日志在账本中,和合约存储在不同的结构中;
  2. log不能被合约读取,可以被链下读取;
  3. 一部分log(使用indexed修饰的字段)存储在bloom filters中,可以由轻节点随意访问;
  4. 日志是账本的一部分:https://docs.soliditylang.org/en/develop/contracts.html?highlight=events#events

Event

  • indexed:方便索引,加了inexed是topics
  • non-indexed:没有解码,需要使用abi解码后才知道内容,不加indexd是data

描述

发送一笔交易之后,我们可以得到tx交易的返回值,在返回值中会包含TransactionRecept字段,进一步会发现Event,指定id可以获取这个事件的详情,进一步会获得每个字段的具体数值。

console.log(transactionReceipt.events[0].args.oldNumber.toString())

Bloom

以太坊的Header中有布隆过滤器,这里面存放的是什么?(是所有事件生成的信息),当有索引过来的时候,我们会去重新生成事件。

  • 只有indexed的field才会添加到bloom filter中
  • 有请求的时候会快速在bloom filter中寻找,如果发现存在,则重新执行交易,并且返回log结果

英文参考:https://solidity-by-example.org/

本教程仅供学习,如有疑问,请移步Telegram
post-qrcode
gason
版权声明:本文于2022-10-18转载自参考文章,共计28061字。
转载提示:此文章非本站原创文章,若需转载请联系原作者获得转载授权。
评论(没有评论)
验证码