multi_claim 合约分析

囤了很久的一个任务,来做一个简单的代码解析。 一个用于合约批量Claim 的操作。

比如之前的$rnd,就可以使用这套代码来进行批量的Claim 的交互操作。

正文

代码思路

对代码思路来进行一个简单梳理。先定义一个ERC20 的 interface 用于来进行合约来进行交互。之后new claim 合约来进行对token的mint操作,在完成mint之后批量的转回sender。

这里的前提是代码没有EOA(Externally Owned Accounts)机制也就是说没有,对合约账户CA还是外部账户EOA的检测。

另外这里还需要说明的是,这种mint的Token是没有验证机制的,就是有claim接口之后就可以随便领取的。

代码分析

大体按照调用栈来逐步分析

  1. call
  2. Addressto
  3. abi.encodePacked
  4. New claimer
  5. claim
  6. transfer

Call

这里是这个合约的主调函数,函数体如下。这里主要是进行 循环并且自增nonce,计算出即将new 出来的合约的地址,用于后面的转账操作。之后吧nonce +1 之后进行接下来的操作。

    function call(uint256 times) public {
        for(uint i=0;i<times;++i){
            address to = addressto(address(this), nonce);
            new claimer(to, address(msg.sender));
            nonce+=1;
        }
    }

addressto

这个是比较有意思的地方,在一个合约创建之前能否知道这个的地址?

答案是可以的,这个也被用于一些LP添加前的预判,也就是说可能LP还没创建,你就可以拿到LP的地址,早早冲进去。

这里的 addressto 就是这个算法的实现。

合约地址生成

资料参考:以太坊合约地址是怎么计算出来的?

以太坊合约的地址是根据创建者(sender)的地址以及创建者发送过的交易数量(nonce)来计算确定的。 sendernonce 进行RLP编码,然后用Keccak-256 进行hash计算。

nonce0 = address(keccak256(0xd6, 0x94, address, 0x80))
nonce1 = address(keccak256(0xd6, 0x94, address, 0x01))

所以在这个合约里面可以看到也是按这个形式来进行了字节码的组装。这里来对组装过程进行分析。

这里第一个点是 0x80 在nonce 为0 的时候是它,但是在 nonce 为1的情况下却变成了 0x01 在。


encodePacked

encodePacked 是solidity 中的紧打包的方式,用于构造原始的字节串。

  • 函数选择器 不进行编码,
  • 长度低于 32 字节的类型,既不会进行补 0 操作,也不会进行符号扩展
  • 动态类型会直接进行编码,并且不包含长度信息。

assembly

这里作者还是使用了内联汇编,但是的确没有搞懂其中的意思,通过doc看到的,这个是把变量保存到0offset的内存,之后读取出来。感觉和直接return 是一个意思。可以只是一种用法练习,这里先存疑。后面清楚了在进行补充

assembly {
    mstore(0, hash)
    _address := mload(0)
}

New claimer

使用上面的函数计算得到这里Claimer 的合约地址,来传入这里的构造函数。用于这里对是否成功mint来做判断。避免后面的transfer的操作节约gas。

这里直接调用Token合约的 Claim 函数,如果有余额之后来进行transfer转账。汇集到总的账户下,完成批量mint。

contract claimer{
    constructor(address selfAdd, address receiver){
        address contra = address(0xbb2A2D70d6a4B80FA2C4d4Ca43a8525da430196c);
        airdrop(contra).claim();
        uint256 balance = airdrop(contra).balanceOf(selfAdd);
        require(balance>0,'Oh no');
        airdrop(contra).transfer(receiver, balance);
    }
}

值得注意的

前面提到这里的批量脚本只适用于没有验证EOA的,且每人人到可以claim 的项目,这里来担当度讨论一下,关于合约中的EOA的校验的方法。

这里使用到 深入解析Solidity 里面的 3. 修補EtherDice的漏洞 这段落。solidity里面提供原生 extcodesize 操作符号来进行EOA 地址的验证

assembly { size := extcodesize(addr) }

这里的extcodesize实际上是runtime 的bytecode 也就是运行时的 字节码,对合约地址 这里的 byte是 0。

所以这里可以做出来下下面的验证方式,来进行 EOA地址的检测。

    function isHuman(address addr) internal view returns (bool) {
      uint size;
      assembly { size := extcodesize(addr) }
      return size == 0;
    }

至此,这个mutilclaim 的合约已经分析完毕了,总结起来就是创建合约使用合约地址来远程调用token的claim函数,之后再使用标准的 transfer 的函数来吧token汇集到sender。

后面有实践的机会可以操作一波。

附上全部的代码内容,完全拷贝自代码仓库

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

interface airdrop {
    function transfer(address recipient, uint256 amount) external;
    function balanceOf(address account) external view returns (uint256);
    function claim() external;
}

contract multiCall{
    uint256 nonce = 1;
    function addressto(address _origin, uint256 _nonce) internal pure returns (address _address) {
        bytes memory data;
        if(_nonce == 0x00)          data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), _origin, bytes1(0x80));
        else if(_nonce <= 0x7f)     data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), _origin, uint8(_nonce));
        else if(_nonce <= 0xff)     data = abi.encodePacked(bytes1(0xd7), bytes1(0x94), _origin, bytes1(0x81), uint8(_nonce));
        else if(_nonce <= 0xffff)   data = abi.encodePacked(bytes1(0xd8), bytes1(0x94), _origin, bytes1(0x82), uint16(_nonce));
        else if(_nonce <= 0xffffff) data = abi.encodePacked(bytes1(0xd9), bytes1(0x94), _origin, bytes1(0x83), uint24(_nonce));
        else                        data = abi.encodePacked(bytes1(0xda), bytes1(0x94), _origin, bytes1(0x84), uint32(_nonce));
        bytes32 hash = keccak256(data);
        assembly {
            mstore(0, hash)
            _address := mload(0)
        }
    }
    function call(uint256 times) public {
        for(uint i=0;i<times;++i){
            address to = addressto(address(this), nonce);
            new claimer(to, address(msg.sender));
            nonce+=1;
        }
    }
}
contract claimer{
    constructor(address selfAdd, address receiver){
        address contra = address(0xbb2A2D70d6a4B80FA2C4d4Ca43a8525da430196c);
        airdrop(contra).claim();
        uint256 balance = airdrop(contra).balanceOf(selfAdd);
        require(balance>0,'Oh no');
        airdrop(contra).transfer(receiver, balance);
    }
}