Solidity进阶学习笔记

hloong 于 2022-11-19 发布

Solidity进阶学习笔记

这段时间学习了solidity的基础知识,做个笔记参考以免很容易遗忘,学习网站: https://wtf.academy/solidity-start/

函数重载

solidity中允许函数进行重载(overloading),即名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。注意,solidity不允许修饰器(modifier)重载。 我们可以定义两个都叫saySomething()的函数,一个没有任何参数,输出”Nothing”;另一个接收一个string参数,输出这个string。

function saySomething() public pure returns(string memory){
    return("Nothing");
}

function saySomething(string memory something) public pure returns(string memory){
    return(something);
}

在调用重载函数时,会把输入的实际参数和函数参数的变量类型做匹配。 如果出现多个匹配的重载函数,则会报错。下面这个例子有两个叫f()的函数,一个参数为uint8,另一个为uint256:

    function f(uint8 _in) public pure returns (uint8 out) {
        out = _in;
    }

    function f(uint256 _in) public pure returns (uint256 out) {
        out = _in;
    }

我们调用f(50),因为50既可以被转换为uint8,也可以被转换为uint256,因此会报错。

库合约

库函数是一种特殊的合约,为了提升solidity代码的复用性和减少gas而存在。库合约一般都是一些好用的函数合集(库函数)

他和普通合约主要有以下几点不同:

String库合约

String库合约是将uint256类型转换为相应的string类型的代码库,他主要包含两个函数,toString()将uint256转为string,toHexString()将uint256转换为16进制,在转换为string。 样例代码如下:

library Strings {
    bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";

    /**
     * @dev Converts a `uint256` to its ASCII `string` decimal representation.
     */
    function toString(uint256 value) public pure returns (string memory) {
        // Inspired by OraclizeAPI's implementation - MIT licence
        // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol

        if (value == 0) {
            return "0";
        }
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
            value /= 10;
        }
        return string(buffer);
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
     */
    function toHexString(uint256 value) public pure returns (string memory) {
        if (value == 0) {
            return "0x00";
        }
        uint256 temp = value;
        uint256 length = 0;
        while (temp != 0) {
            length++;
            temp >>= 8;
        }
        return toHexString(value, length);
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
     */
    function toHexString(uint256 value, uint256 length) public pure returns (string memory) {
        bytes memory buffer = new bytes(2 * length + 2);
        buffer[0] = "0";
        buffer[1] = "x";
        for (uint256 i = 2 * length + 1; i > 1; --i) {
            buffer[i] = _HEX_SYMBOLS[value & 0xf];
            value >>= 4;
        }
        require(value == 0, "Strings: hex length insufficient");
        return string(buffer);
    }
}

如何使用库合约​

我们用String库函数的toHexString()来演示两种使用库合约中函数的办法 1,利用using for指令 指令using A for B;可用于附加库函数(从库 A)到任何类型(B)。添加完指令后,库A中的函数会自动添加为B类型变量的成员,可以直接调用。注意:在调用的时候,这个变量会被当作第一个参数传递给函数:

    // 利用using for指令
    using Strings for uint256;
    function getString1(uint256 _number) public pure returns(string memory){
        // 库函数会自动添加为uint256型变量的成员
        return _number.toHexString();
    }

2,通过库合约名称调用库函数

    // 直接通过库合约名调用
    function getString2(uint256 _number) public pure returns(string memory){
        return Strings.toHexString(_number);
    }

常用的库函数有:

String:将uint256转换为String Address:判断某个地址是否为合约地址 Create2:更安全的使用Create2 EVM opcode Arrays:跟数组相关的库函数

Import

通过源文件相对位置导入 文件结构

├── Import.sol
└── Yeye.sol

// 通过文件相对位置import import './Yeye.sol';

通过源文件网址导入网上的合约 // 通过网址引用 import ‘https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol’;

通过npm的目录导入 import '@openzeppelin/contracts/access/Ownable.sol';

通过全局符号导入特定的合约 import {Yeye} from './Yeye.sol';

引用(import)在代码中的位置为:在声明版本号之后,在其余代码之前。

接收和发送ETH

接收ETH

Solidity支持两种特殊的回调函数,receive()和fallback(),他们主要在两种情况下被使用: 接收ETH 处理合约中不存在的函数调用(代理合约proxy contract) 在solidity 0.6.x版本之前,语法上只有 fallback() 函数,用来接收用户发送的ETH时调用以及在被调用函数签名没有匹配到时,来调用。 0.6版本之后,solidity才将 fallback() 函数拆分成 receive() 和 fallback() 两个函数。 接收ETH函数 receive​ receive()只用于处理接收ETH。一个合约最多有一个receive()函数,声明方式与一般函数不一样,不需要function关键字:receive() external payable { … }。receive()函数不能有任何的参数,不能返回任何值,必须包含external和payable。 当合约接收ETH的时候,receive()会被触发。receive()最好不要执行太多的逻辑因为如果别人用send和transfer方法发送ETH的话,gas会限制在2300,receive()太复杂可能会触发Out of Gas报错;如果用call就可以自定义gas执行更复杂的逻辑(这三种发送ETH的方法我们之后会讲到)。 我们可以在receive()里发送一个event,例如:

    // 定义事件
    event Received(address Sender, uint Value);
    // 接收ETH时释放Received事件
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }

有些恶意合约,会在receive() 函数(老版本的话,就是 fallback() 函数)嵌入恶意消耗gas的内容或者使得执行故意失败的代码,导致一些包含退款和转账逻辑的合约不能正常工作,因此写包含退款等逻辑的合约时候,一定要注意这种情况。

回退函数 fallback​

fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contract。fallback()声明时不需要function关键字,必须由external修饰,一般也会用payable修饰,用于接收ETH:fallback() external payable { … }。 我们定义一个fallback()函数,被触发时候会释放fallbackCalled事件,并输出msg.sender,msg.value和msg.data:

    // fallback
    fallback() external payable{
        emit fallbackCalled(msg.sender, msg.value, msg.data);
    }

receive和fallback的区别​

receive和fallback都能够用于接收ETH,他们触发的规则如下:

触发fallback() 还是 receive()?
           接收ETH
              |
         msg.data是空?
            /  \
          是    否
          /      \
receive()存在?   fallback()
        / \
       是  否
      /     \
receive()   fallback()

合约接收ETH时,msg.data为空且存在receive()时,会触发receive();msg.data不为空或不存在receive()时,会触发fallback(),此时fallback()必须为payable。 receive()和payable fallback()均不存在的时候,向合约发送ETH将会报错。

发送ETH

Solidity有三种方法向其他合约发送ETH,他们是:transfer(),send()和call(),其中call()是被鼓励的用法。

transfer

用法是接收方地址.transfer(发送ETH数额)。 transfer()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。 transfer()如果转账失败,会自动revert(回滚交易)。 代码样例,注意里面的_to填ReceiveETH合约的地址,amount是ETH转账金额:

// 用transfer()发送ETH
function transferETH(address payable _to, uint256 amount) external payable{
    _to.transfer(amount);
}

send ​ 用法是接收方地址.send(发送ETH数额)。 send()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。 send()如果转账失败,不会revert。 send()的返回值是bool,代表着转账成功或失败,需要额外代码处理一下。

// send()发送ETH
function sendETH(address payable _to, uint256 amount) external payable{
    // 处理下send的返回值,如果失败,revert交易并发送error
    bool success = _to.send(amount);
    if(!success){
        revert SendFailed();
    }
}

call

用法是接收方地址.call{value: 发送ETH数额}(“”)。 call()没有gas限制,可以支持对方合约fallback()或receive()函数实现复杂逻辑。 call()如果转账失败,不会revert。 call()的返回值是(bool, data),其中bool代表着转账成功或失败,需要额外代码处理一下。

// call()发送ETH
function callETH(address payable _to, uint256 amount) external payable{
    // 处理下call的返回值,如果失败,revert交易并发送error
    (bool success,) = _to.call{value: amount}("");
    if(!success){
        revert CallFailed();
    }
}

调用其他合约

我们先写一个简单的合约OtherContract来调用。

contract OtherContract {
    uint256 private _x = 0; // 状态变量_x
    // 收到eth的事件,记录amount和gas
    event Log(uint amount, uint gas);
    
    // 返回合约ETH余额
    function getBalance() view public returns(uint) {
        return address(this).balance;
    }

    // 可以调整状态变量_x的函数,并且可以往合约转ETH (payable)
    function setX(uint256 x) external payable{
        _x = x;
        // 如果转入ETH,则释放Log事件
        if(msg.value > 0){
            emit Log(msg.value, gasleft());
        }
    }

    // 读取_x
    function getX() external view returns(uint x){
        x = _x;
    }
}

调用上面的OtherContract合约

contract CallContract{
    //我们可以在函数里传入目标合约地址,生成目标合约的引用,然后调用目标函数。
    //以调用OtherContract合约的setX函数为例,我们在新合约中写一个callSetX函数,
    //传入已部署好的OtherContract合约地址_Address和setX的参数x:
    function callSetX(address _address,uint256 x) external {
        OtherContract(_address).setX(x);
    }
    //我们可以直接在函数里传入合约的引用,只需要把上面参数的address类型改为目标合约名
    function callGetX(OtherContract _address) external view returns(uint x){
        x = _address.getX();
    }
    //我们可以创建合约变量,然后通过它来调用目标函数
    function callGetX2(address _address) external view returns(uint x){
        OtherContract oc = OtherContract(_address);
        x = oc.getX();
    }
    //OtherContract合约的setX函数是payable的,在下面这个例子中我们通过调用setX来往目标合约转账
    function setXTransferETH(address otherContract, uint256 x) payable external{
        OtherContract(otherContract).setX{value: msg.value}(x);
    }
}

Call 和Delegatecall

Call 参考:https://wtf.academy/solidity-advanced/Call call 是address类型的低级成员函数,它用来与其他合约交互。它的返回值为(bool, data),分别对应call是否成功以及目标函数的返回值。 call是solidity官方推荐的通过触发fallback或receive函数发送ETH的方法。 不推荐用call来调用另一个合约,因为当你调用不安全合约的函数时,你就把主动权交给了它。推荐的方法仍是声明合约变量后调用函数 当我们不知道对方合约的源代码或ABI,就没法生成合约变量;这时,我们仍可以通过call调用对方合约的函数。

Call的使用规则:

目标合约地址.call(二进制编码); 其中二进制编码利用结构化编码函数abi.encodeWithSignature获得: abi.encodeWithSignature(“函数签名”, 逗号分隔的具体参数)

函数签名为”函数名(逗号分隔的参数类型)”。例如abi.encodeWithSignature(“f(uint256,address)”, _x, _addr)。 另外call在调用合约时可以指定交易发送的ETH数额和gas: 目标合约地址.call{value:发送数额, gas:gas数额}(二进制编码);

利用Call 调用目标合约

  1. Response事件 我们写一个Call合约来调用目标合约函数。首先定义一个Response事件,输出call返回的success和data,方便我们观察返回值。 // 定义Response事件,输出call返回的结果success和data event Response(bool success, bytes data);

  2. 调用setX函数 我们定义callSetX函数来调用目标合约的setX(),转入msg.value数额的ETH,并释放Response事件输出success和data:

    function callSetX(address payable _addr, uint256 x) public payable {
     // call setX(),同时可以发送ETH
     (bool success, bytes memory data) = _addr.call{value: msg.value}(
         abi.encodeWithSignature("setX(uint256)", x)
     );
    
     emit Response(success, data); //释放事件
    }
    

接下来我们调用callSetX把状态变量_x改为5,参数为OtherContract地址和5,由于目标函数setX()没有返回值, 因此Response事件输出的data为0x,也就是空。 3,调用getX函数 下面我们调用getX()函数,它将返回目标合约_x的值,类型为uint256。我们可以利用abi.decode来解码call的返回值data,并读出数值。 function callGetX(address _addr) external returns(uint256){ // call getX() (bool success, bytes memory data) = _addr.call( abi.encodeWithSignature(“getX()”) );

emit Response(success, data); //释放事件
return abi.decode(data, (uint256)); }

从Response事件的输出,我们可以看到data为0x0000000000000000000000000000000000000000000000000000000000000005。而经过abi.decode,最终返回值为5。 4,调用不存在的函数 如果我们给call输入的函数不存在于目标合约,那么目标合约的fallback函数会被触发。

function callNonExist(address _addr) external{
    // call getX()
    (bool success, bytes memory data) = _addr.call(
        abi.encodeWithSignature("foo(uint256)")
    );

    emit Response(success, data); //释放事件
}

上面例子中,我们call了不存在的foo函数。call仍能执行成功,并返回success,但其实调用的目标合约fallback函数。

Delegatecall

delegatecall与call类似,是solidity中地址类型的低级成员函数。delegate中是委托/代表的意思,那么delegatecall委托了什么? 当用户A通过合约B来call合约C的时候,执行的是合约C的函数,语境(Context,可以理解为包含变量和状态的环境)也是合约C的:msg.sender是B的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约C的变量上。

而当用户A通过合约B来delegatecall合约C的时候,执行的是合约C的函数,但是语境仍是合约B的:msg.sender是A的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约B的变量上。

delegatecall语法和call类似,也是: 目标合约地址.delegatecall(二进制编码);

其中二进制编码利用结构化编码函数abi.encodeWithSignature获得: abi.encodeWithSignature(“函数签名”, 逗号分隔的具体参数)

函数签名为”函数名(逗号分隔的参数类型)”。例如abi.encodeWithSignature(“f(uint256,address)”, _x, _addr)。 和call不一样,delegatecall在调用合约时可以指定交易发送的gas,但不能指定发送的ETH数额 注意:delegatecall有安全隐患,使用时要保证当前合约和目标合约的状态变量存储结构相同,并且目标合约安全,不然会造成资产损失。

什么情况下会用到delegatecall? 目前delegatecall主要有两个应用场景: 代理合约(Proxy Contract):将智能合约的存储合约和逻辑合约分开:代理合约(Proxy Contract)存储所有相关的变量,并且保存逻辑合约的地址;所有函数存在逻辑合约(Logic Contract)里,通过delegatecall执行。当升级时,只需要将代理合约指向新的逻辑合约即可。 EIP-2535 Diamonds(钻石):钻石是一个支持构建可在生产中扩展的模块化智能合约系统的标准。钻石是具有多个实施合同的代理合同。 更多信息请查看:钻石标准简介。 举个例子:

// 被调用的合约C
contract C {
    uint public num;
    address public sender;

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

contract B {
    uint public num;
    address public sender;
        // 通过call来调用C的setVars()函数,将改变合约C里的状态变量
    function callSetVars(address _addr, uint _num) external payable{
        // call setVars()
        (bool success, bytes memory data) = _addr.call(
            abi.encodeWithSignature("setVars(uint256)", _num)
        );
    }
        // 通过delegatecall来调用C的setVars()函数,将改变合约B里的状态变量
    function delegatecallSetVars(address _addr, uint _num) external payable{
        // delegatecall setVars()
        (bool success, bytes memory data) = _addr.delegatecall(
            abi.encodeWithSignature("setVars(uint256)", _num)
        );
    }
}

在合约中创建新合约

在以太坊链上,用户(外部账户,EOA)可以创建智能合约,智能合约同样也可以创建新的智能合约。去中心化交易所uniswap就是利用工厂合约(Factory)创建了无数个币对合约(Pair)。这一讲,我会用简化版的uniswap讲如何通过合约创建合约。 有两种方法可以在合约中创建新合约,create和create2

Create

create的用法很简单,就是new一个合约,并传入新合约构造函数所需的参数 Contract x = new Contract{value: _value}(params)

其中Contract是要创建的合约名,x是合约对象(地址),如果构造函数是payable,可以创建时转入_value数量的ETH,params是新合约构造函数的参数。

极简Uniswap1

Uniswap V2核心合约中包含两个合约: UniswapV2Pair: 币对合约,用于管理币对地址、流动性、买卖。 UniswapV2Factory: 工厂合约,用于创建新的币对,并管理币对地址。 下面我们用create方法实现一个极简版的Uniswap:Pair币对合约负责管理币对地址,PairFactory工厂合约用于创建新的币对,并管理币对地址。 Pair合约

contract Pair{
    address public factory; // 工厂合约地址
    address public token0; // 代币1
    address public token1; // 代币2

    constructor() payable {
        factory = msg.sender;
    }

    // called once by the factory at time of deployment
    function initialize(address _token0, address _token1) external {
        require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
        token0 = _token0;
        token1 = _token1;
    }
}

Pair合约很简单,包含3个状态变量:factory,token0和token1。 构造函数constructor在部署时将factory赋值为工厂合约地址。initialize函数会在Pair合约创建的时候被工厂合约调用一次,将token0和token1更新为币对中两种代币的地址。

提问:为什么uniswap不在constructor中将token0和token1地址更新好? 答:因为uniswap使用的是create2创建合约,限制构造函数不能有参数。当使用create时, Pair合约允许构造函数有参数,可以在constructor中将token0和token1地址更新好。

PairFactory

contract PairFactory{
    mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址
    address[] public allPairs; // 保存所有Pair地址

    function createPair(address tokenA, address tokenB) external returns (address pairAddr) {
        // 创建新合约
        Pair pair = new Pair(); 
        // 调用新合约的initialize方法
        pair.initialize(tokenA, tokenB);
        // 更新地址map
        pairAddr = address(pair);
        allPairs.push(pairAddr);
        getPair[tokenA][tokenB] = pairAddr;
        getPair[tokenB][tokenA] = pairAddr;
    }
}

工厂合约(PairFactory)有两个状态变量getPair是两个代币地址到币对地址的map,方便根据代币找到币对地址;allPairs是币对地址的数组,存储了所有代币地址。 PairFactory合约只有一个createPair函数,根据输入的两个代币地址tokenA和tokenB来创建新的Pair合约。其中 ` Pair pair = new Pair(); `

就是创建合约的代码,非常简单。大家可以部署好PairFactory合约,然后用下面两个地址作为参数调用createPair,看看创建的币对地址是什么:

WBNB地址: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78
BSC链上的PEOPLE地址:
0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c

Create2

CREATE2 操作码使我们在智能合约部署在以太坊网络之前就能预测合约的地址。Uniswap创建Pair合约用的就是CREATE2而不是CREATE

CREATE如何计算地址​

智能合约可以由其他合约和普通账户利用CREATE操作码创建。 在这两种情况下,新合约的地址都以相同的方式计算:创建者的地址(通常为部署的钱包地址或者合约地址)和nonce(该地址发送交易的总数,对于合约账户是创建的合约总数,每创建一个合约nonce+1))的哈希。 新地址 = hash(创建者地址, nonce)

创建者地址不会变,但nonce可能会随时间而改变,因此用CREATE创建的合约地址不好预测。

CREATE2如何计算地址

CREATE2的目的是为了让合约地址独立于未来的事件。不管未来区块链上发生了什么,你都可以把合约部署在事先计算好的地址上。用CREATE2创建的合约地址由4个部分决定: 0xFF:一个常数,避免和CREATE冲突 创建者地址 salt(盐):一个创建者给定的数值 待部署合约的字节码(bytecode) 新地址 = hash(“0xFF”,创建者地址, salt, bytecode)

CREATE2 确保,如果创建者使用 CREATE2 和提供的 salt 部署给定的合约bytecode,它将存储在 新地址 中 如何使用CREATE2 CREATE2的用法和之前讲的Create类似,同样是new一个合约,并传入新合约构造函数所需的参数,只不过要多传一个salt参数: Contract x = new Contract{salt: _salt, value: _value}(params)

其中Contract是要创建的合约名,x是合约对象(地址),_salt是指定的盐;如果构造函数是payable,可以创建时转入_value数量的ETH,params是新合约构造函数的参数。 极简Uniswap2 Pair的方法参考极简uniswap1 PairFactory2

contract PairFactory2{
        mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址
        address[] public allPairs; // 保存所有Pair地址

        function createPair2(address tokenA, address tokenB) external returns (address pairAddr) {
            require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //避免tokenA和tokenB相同产生的冲突
            // 计算用tokenA和tokenB地址计算salt
            (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序
            bytes32 salt = keccak256(abi.encodePacked(token0, token1));
            // 用create2部署新合约
            Pair pair = new Pair{salt: salt}(); 
            // 调用新合约的initialize方法
            pair.initialize(tokenA, tokenB);
            // 更新地址map
            pairAddr = address(pair);
            allPairs.push(pairAddr);
            getPair[tokenA][tokenB] = pairAddr;
            getPair[tokenB][tokenA] = pairAddr;
        }

注意:利用Create2创建合约的代码中,参数salt等于:一对代币地址的哈希

删除合约

selfdestruct命令可以用来删除智能合约,并将该合约剩余ETH转到指定地址。selfdestruct是为了应对合约出错的极端情况而设计的。它最早被命名为suicide(自杀),但是这个词太敏感。为了保护抑郁的程序员,改名为selfdestruct。 selfdestruct(_addr);//使用:_addr是接收合约中剩余ETH的地址

示例代码

contract DeleteContract {
    uint public value = 10;
    constructor() payable {}
    receive() external payable {}

    function deleteContract() external {
        // 调用selfdestruct销毁合约,并把剩余的ETH转给msg.sender
        selfdestruct(payable(msg.sender));
    }
    function getBalance() external view returns(uint balance){
        balance = address(this).balance;
    }
}

注意事项​

对外提供合约销毁接口时,最好设置为只有合约所有者可以调用,可以使用函数修饰符onlyOwner进行函数声明。 当合约被销毁后与智能合约的交互也能成功,并且返回0。 当合约中有selfdestruct功能时常常会带来安全问题和信任问题,合约中的Selfdestruct功能会为攻击者打开攻击向量(例如使用selfdestruct向一个合约频繁转入token进行攻击,这将大大节省了GAS的费用,虽然很少人这么做),此外,此功能还会降低用户对合约的信心。

ABI编码解码

ABI (Application Binary Interface,应用二进制接口)是与以太坊智能合约交互的标准。数据基于他们的类型编码;并且由于编码后不包含类型信息,解码时需要注明它们的类型。 Solidity中,ABI编码有4个函数:abi.encode, abi.encodePacked, abi.encodeWithSignature, abi.encodeWithSelector。而ABI解码有1个函数:abi.decode,用于解码abi.encode的数据。

ABI编码

我们将用编码4个变量,他们的类型分别是uint256, address, string, uint256[2]

    uint x = 10;
    address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
    string name = "0xAA";
    uint[2] array = [5, 6]; 

abi.encode 将给定参数利用ABI规则编码。ABI被设计出来跟智能合约交互,他将每个参数填充为32字节的数据,并拼接在一起。如果你要和合约交互,你要用的就是abi.encode。

    function encode() public view returns(bytes memory result) {
        result = abi.encode(x, addr, name, array);
    }
   // 编码的结果为
   0x000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000
   07a58c0be72be218b41c608b7fe7c5bb630736c71000000000000000000000000000000000000000000000000
   00000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000
   00000000000000000000000000000000000000000000000000000060000000000000000000000000000000000
   00000000000000000000000000000430784141000000000000000000000000000000000000000000000000000
   00000//由于abi.encode将每个数据都填充为32字节,中间有很多0。

abi.encodePacked

将给定参数根据其所需最低空间编码。它类似 abi.encode,但是会把其中填充的很多0省略。比如,只用1字节来编码uint类型。当你想省空间,并且不与合约交互的时候,可以使用abi.encodePacked,例如算一些数据的hash时

    function encodePacked() public view returns(bytes memory result) {
        result = abi.encodePacked(x, addr, name, array);
    }
    //由于abi.encodePacked对编码进行了压缩,长度比abi.encode短很多。以下为结果
    0x000000000000000000000000000000000000000000000000000000000000000a7a58c0be72be218b41c608b
    7fe7c5bb630736c71307841410000000000000000000000000000000000000000000000000000000000000005
    0000000000000000000000000000000000000000000000000000000000000006

abi.encodeWithSignature

与abi.encode功能类似,只不过第一个参数为函数签名,比如”foo(uint256,address)”。当调用其他合约的时候可以使用

    function encodeWithSignature() public view returns(bytes memory result) {
        result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array);
    }
    //结果如下,等同于在abi.encode编码结果前加上了4字节的函数选择器说明。 说明: 函数选择器就是通过函数名和参数进行
    //签名处理(Keccak–Sha3)来标识函数,可以用于不同合约之间的函数调用
    0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000
    000000007a58c0be72be218b41c608b7fe7c5bb630736c71000000000000000000000000000000000000000000
    00000000000000000000a000000000000000000000000000000000000000000000000000000000000000050000
    000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000
    000000000000000000000000000000000430784141000000000000000000000000000000000000000000000000
    00000000

abi.encodeWithSelector

与abi.encodeWithSignature功能类似,只不过第一个参数为函数选择器,为函数签名Keccak哈希的前4个字节。

    function encodeWithSelector() public view returns(bytes memory result) {
        result = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array);
    }
    //与abi.encodeWithSignature结果一样。同上

ABI解码

abi.decode abi.decode用于解码abi.encode生成的二进制编码,将它还原成原本的参数。 function decode(bytes memory data) public pure returns(uint dx, address daddr, string memory dname, uint[2] memory darray) { (dx, daddr, dname, darray) = abi.decode(data, (uint, address, string, uint[2])); }

ABI的使用场景

1,在合约开发中,ABI常配合call来实现对合约的底层调用。

    bytes4 selector = contract.getValue.selector;

    bytes memory data = abi.encodeWithSelector(selector, _x);
    (bool success, bytes memory returnedData) = address(contract).staticcall(data);
    require(success);

    return abi.decode(returnedData, (uint256));

2,在合约开发中,ABI常配合call来实现对合约的底层调用。

    const wavePortalContract = new ethers.Contract(contractAddress, contractABI, signer);
    /*
        * Call the getAllWaves method from your Smart Contract
        */
    const waves = await wavePortalContract.getAllWaves();

3,对不开源合约进行反编译后,某些函数无法查到函数签名,可通过ABI进行调用。 0x533ba33a() 是一个反编译后显示的函数,只有函数编码后的结果,并且无法查到函数签名

4,这种情况无法通过构造interface接口或contract来进行调用

就可以通过ABI函数选择器来调用

    bytes memory data = abi.encodeWithSelector(bytes4(0x533ba33a));

    (bool success, bytes memory returnedData) = address(contract).staticcall(data);
    require(success);

    return abi.decode(returnedData, (uint256));

总结:在以太坊中,数据必须编码成字节码才能和智能合约交互

Hash

哈希函数(hash function)是一个密码学概念,它可以将任意长度的消息转换为一个固定长度的值,这个值也称作哈希(hash)。

一个好的哈希函数应该具有以下几个特性:

Keccak256和sha3

sha3由keccak标准化而来,在很多场合下Keccak和SHA3是同义词,但在2015年8月SHA3最终完成标准化时,NIST调整了填充算法。所以SHA3就和keccak计算的结果不一样,这点在实际开发中要注意。 以太坊在开发的时候sha3还在标准化中,所以采用了keccak,所以Ethereum和Solidity智能合约代码中的SHA3是指Keccak256,而不是标准的NIST-SHA3,为了避免混淆,直接在合约代码中写成Keccak256是最清晰的。 题目: 强抗碰撞性意味着哈希函数的任意两个输入对应的输出都不同。 错误,太绝对 如果哈希值相同,说明文件的内容一定完全相同,下载过程没有出现问题。 错误,太绝对

选择器Selector

当我们调用智能合约时,本质上是向目标合约发送了一段calldata,在remix中发送一次交易后,可以在详细信息中看见input即为此次交易的calldata,发送的calldata中前4个字节是selector(函数选择器)。 msg.data是solidity中的一个全局变量,值为完整的calldata(调用函数时传入的数据)。 在下面的代码中,我们可以通过Log事件来输出调用mint函数的calldata:

    // event 返回msg.data
    event Log(bytes data);

    function mint(address to) external{
        emit Log(msg.data);
    }

当参数为0x2c44b726ADF1963cA47Af88B284C06f30380fC78时,输出的calldata为 0x6a6278420000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78

这段很乱的字节码可以分成两部分: 前4个字节为函数选择器selector: 0x6a627842

后面32个字节为输入的参数: 0x0000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78

其实calldata就是告诉智能合约,我要调用哪个函数,以及参数是什么。 method id、selector和函数签名 method id定义为函数签名的Keccak哈希后的前4个字节,当selector与method id相匹配时,即表示调用该函数,那么函数签名是什么? 在同一个智能合约中,不同的函数有不同的函数签名,因此我们可以通过函数签名来确定要调用哪个函数。 //写一个函数,来验证mint函数的method id是否为0x6a627842 function mintSelector() external pure returns(bytes4 mSelector){ return bytes4(keccak256(“mint(address)”)); }

注意,在函数签名中,uint和int要写为uint256和int256。 使用selector 我们可以利用selector来调用目标函数。例如我想调用mint函数,我只需要利用abi.encodeWithSelector将mint函数的method id作为selector和参数打包编码,传给call函数:

    function callWithSignature() external returns(bool, bytes memory){
        (bool success, bytes memory data) = address(this).call(abi.encodeWithSelector(0x6a627842, "0x2c44b726ADF1963cA47Af88B284C06f30380fC78"));
        return(success, data);
    }

Try Catch

在solidity中,try-catch只能被用于external函数或创建合约时constructor(被视为external函数)的调用。基本语法如下:

        try externalContract.f() {
            // call成功的情况下 运行一些代码
        } catch {
            // call失败的情况下 运行一些代码
        }

同样可以使用this.f()来替代externalContract.f(),this.f()也被视作为外部调用,但不可在构造函数中使用,因为此时合约还未创建。 如果调用的函数有返回值,那么必须在try之后声明returns(returnType val),并且在try模块中可以使用返回的变量;如果是创建合约,那么返回值是新创建的合约变量。

        try externalContract.f() returns(returnType val){
            // call成功的情况下 运行一些代码
        } catch {
            // call失败的情况下 运行一些代码
        }

另外,catch模块支持捕获特殊的异常原因:

        try externalContract.f() returns(returnType){
            // call成功的情况下 运行一些代码
        } catch Error(string memory reason) {
            // 捕获失败的 revert() 和 require()
        } catch (bytes memory reason) {
            // 捕获失败的 assert()
        }

try-catch捕获到异常后是否会使try-catch所在的方法调用失败?不会 try代码块内的revert是否会被catch本身捕获?不会 以下异常返回值类型为bytes的是:revert(),require(),assert() 都可以