实践create2进行合约无缝升级(2) – Metapod.sol 解析

Metapod.sol是带有保险库合约。主要用于转移合约的钱(balance)。核心思想就是在销毁合约前,将需要销毁合约的余额转到保险库,等合约升级完成,再将保险库的钱转移到最终合约。

由于文章:实践create2进行合约无缝升级过了编辑时间,不能继续编辑。单开一篇文章,把后半部分补全。

Metapod.sol

这个合约,是带有保险库合约。主要用于转移合约的钱(balance)。核心思想就是在销毁合约前,将需要销毁合约的余额转到保险库,等合约升级完成,再将保险库的钱转移到最终合约。 看下保险库合约的代码:

// _getVaultContractInitializationCode

bytes27(0x586e03212eb796dee588acdbbbd777d4e733185857595959303173),
// the transient contract is the recipient of funds
transientContract,
// GAS CALL PUSH1 49 MSIZE DUP2 MSIZEx2 CODECOPY RETURN
bytes10(0x5af160315981595939f3)

// 转换下:
[0] PC
[16] PUSH15 0x03212eb796dee588acdbbbd777d4e7  // Metapod合约
[17] CALLER
[18] XOR
[19] PC   // 这里如果caller不等于0x03212eb796dee588acdbbbd777d4e7
[20] JUMPI // 就会跳到上一行[18]然后执行错误(不是JUMPDEST)
[21] MSIZE
[22] MSIZE
[23] MSIZE
[24] ADDRESS
[25] BALANCE // 将保险库合约的余额转出
[46] PUSH20 0x0000000000000000000000000000000000000000 // 临时合约地址
[47] GAS
[48] CALL  // 调用call, 将保险库的钱转入临时合约
[50] PUSH1 0x31 // codecopy的代码长度49,拷贝从开始到上一句call即止
[51] MSIZE
[52] DUP2
[53] MSIZE 
[54] MSIZE
[55] CODECOPY
[56] RETURN // 注意,如果合约是重新部署或者创建,上面的call都会执行。

关于msize的说明,参考: https://betterprogramming.pub/solidity-tutorial-all-about-memory-1e1696d71ee4

msize tracks is the highest offset ever accessed in the current execution. A first write or read to a bigger offset will trigger a memory expansion

Any opcode accessing memory may trigger an expansion (including, for example, MLOAD, RETURN or CALLDATACOPY). Each opcode that can is mentioned in the reference. Note also that an opcode with a byte size parameter of 0 will not trigger a memory expansion, regardless of its offset parameters.

_triggerVaultFundsRelease

这个是保险库合约的部署/调用方法。主要完成钱从保险库转移到临时合约。

function _triggerVaultFundsRelease(
    bytes32 salt
  ) internal returns (address vaultContract) {
    // determine the address of the transient contract.
    // 通过salt获取临时合约地址,临时合约是通过Metapod合约+salt+临时合约代码hash值获取
    address transientContract = _getTransientContractAddress(salt);
    // 通过临时合约获取对应的保险库合约代码和地址
    bytes memory vaultContractInitCode = _getVaultContractInitializationCode(
      transientContract
    );
    vaultContract = _getVaultContractAddress(vaultContractInitCode);

    // 如果保险库合约余额大于0,则需要转移到临时合约
    if (vaultContract.balance > 0) {
        // 如果保险库合约代码为空(被selfdestruct了),则重新部署
        if (vaultContractCodeHash == EMPTY_DATA_HASH) {
            // 调用create2创建保险库合约,创建也会进行转账(初始化代码里)
        } else {// 调用call发起转账,这里就会调用保险库合约的初始化代码,转账。
            vaultContract.call("");
        }
    }

构造函数

这里有几个常量解释下

bytes private constant TRANSIENT_CONTRACT_INITIALIZATION_CODE = ( 
    hex"58601c59585992335a6357b9f5235952fa5060403031813d03839281943ef08015602557ff5b80fd"
  );

  //这个转换成opcode是:
[1] PUSH1 0x1c
[2] MSIZE
[3] PC
[4] MSIZE
[5] SWAP3
[6] CALLER
[7] GAS
[12] PUSH4 0x57b9f523
[13] MSIZE
[14] MSTORE
[15] STATICCALL   // 这里是调用caller的getInitializationCode()函数
[16] POP
[18] PUSH1 0x40
[19] ADDRESS
[20] BALANCE
[21] DUP2
[22] RETURNDATASIZE
[23] SUB
[24] DUP4
[25] SWAP3
[26] DUP2
[27] SWAP5
[28] RETURNDATACOPY
[29] CREATE    // 调用create部署新合约
[30] DUP1
[31] ISZERO
[33] PUSH1 0x25
[34] JUMPI
[35] SELFDESTRUCT // 将自己kill掉
[36] JUMPDEST
[37] DUP1
[38] REVERT

// 可以看出来,实际上上面代码完成的,就是TransientContract合约构造函数的主要内容。

EMPTY_DATA_HASH = bytes32(0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470)
// 这个就是个空字符串的keccak256编码。
> util.keccak256(Buffer.from("")).toString("hex")
'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'

注意关于EMPTY_DATA_HASH的标准提议:https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1052.md:

In case the account does not exist or is empty (as defined by EIP-161) 0 is pushed to the stack.

In case the account does not have code the keccak256 hash of empty data (i.e. c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470) is pushed to the stack.

deploy

deployMetamorphicContractWithConstructor差不多 不过这里有个校验,需要特别警示:

_verifyPrelude(metamorphicContract, _getPrelude(vaultContract));

这是什么呢?文件头有说明:

Also, bear in mind that any initialization code provided to the contract must contain the proper prelude, or initial sequence, with a length of 44 bytes: 0x6e03212eb796dee588acdbbbd777d4e73318602b5773 + vault_address + 0xff5b

也就是说,我们所有部署的最终合约,都需要在头部写上这44个字节。干嘛用的? 看下对应的opcode就明白了:

[15] PUSH15 0x03212eb796dee588acdbbbd777d4e7
[16] CALLER
[17] XOR   // 看看caller是不是0x03212eb796dee588acdbbbd777d4e7
[19] PUSH1 0x2b
[20] JUMPI
[41] PUSH20 0x0000000000000000000000000000000000000000
[42] SELFDESTRUCT // 如果是,就执行selfdestruct
[43] JUMPDEST // 否则就跳过这段代码

这是给合约0x03212eb796dee588acdbbbd777d4e7留了一个后门,可以让这个caller去执行selfdestruct来销毁合约。 这个地址,就是Metapod合约本身。见构造函数:

require(
      address(this) == address(0x000000000003212eb796dEE588acdbBbD777D4E7),
      "Incorrect deployment address."
    );

这里就有个疑问,合约部署的时候,怎么提前知道的地址?看下他的两个部署地址就知道了:

https://ropsten.etherscan.io/tx/0xabea820680d4c95b90f8eca3751b653846b2d1ca1ee4db966cfa6641ed345689#internal >https://etherscan.io/tx/0x96b3b4508c899c773748242cfeedb9ddfc95c2c36e96d3c084ce572a418abe7e#internal

他们都是通过创建的新合约去部署的,那么合约的地址就可以在合约内拿到,nonce肯定是1咯,那就可以算出来了。 比如主网的这个:

> web3.utils.sha3("0xd69410ca1adca9ff38988d75dd1f6ee19b1a6bfa919701").slice(-40);
'00000000002b13cccec913420a21e4d11b2dcd3c'

另外,还有一行: address vaultContract = _triggerVaultFundsRelease(salt);

整个连起来看,流程应该是:

  1. 如果保险库合约地址余额不为0,通过create2创建保险库合约,如果保险库合约里有钱,就转到临时合约地址
  2. 通过create2创建临时合约,并在临时合约里调用create创建最终合约,临时合约selfdestruct,将钱转到最终合约
  3. 校验最终合约的代码头部,是不是有后门可后续进入进行selfdestruct。

destroy

有了上面deploy的解释,那这个函数就不难理解了,调用selfdesctruct函数销毁部署的合约。

recover

流程和deploy差不多:

  1. 通过create2创建保险库合约(或者call调用保险库合约),如果保险库合约里有钱,就转到临时合约。
  2. 通过create2创建临时合约,并在临时合约里调用create创建最终合约 1) 最终合约的初始化是将最终合约里的钱转给交易发起者(也就是调用metapod的交易者),并调用selfdestruct 自毁。 2) 临时合约selfdestruct,将钱转到最终合约。

    函数里的初始化代码解析:

    //临时合约里创建的最终合约的初始化代码,实际上就是让
    _initCode = abi.encodePacked(
      bytes2(0x5873),  // PC PUSH20
      msg.sender,      // <the caller is the recipient of funds>
      bytes13(0x905959593031856108fcf150ff)
        // SWAP1 MSIZEx3 ADDRESS BALANCE DUP6 PUSH2 2300 CALL POP SELFDESTRUCT
    );
    // opcode如下:
    [0] PC
    [21] PUSH20 0x0000000000000000000000000000000000000000 // 调用metapod的交易者
    [22] SWAP1
    [23] MSIZE
    [24] MSIZE
    [25] MSIZE
    [26] ADDRESS
    [27] BALANCE  // 余额
    [28] DUP6
    [31] PUSH2 0x08fc  // 2300 gas
    [32] CALL
    [33] POP
    [34] SELFDESTRUCT

    实例分析

    从主链上的合约https://etherscan.io/address/0x00000000002b13cccec913420a21e4d11b2dcd3c 分析下整体的交易流程。

    1. deploy

    image.png

    1. create2创建保险库合约,保险库合约将自己的钱转给临时合约
    2. create2创建临时合约,通过getInitializationCode获取最终合约的代码,使用create部署最终合约(并将自己的钱转给最终合约),自己调用selfdestruct销毁。

2. Destroy

image.png

  1. 调用最终合约的selfdestruct 将钱转给保险库合约

3.Recover

image.png

  1. 调用保险库合约,保险库合约将自己的钱转给临时合约
  2. create2创建临时合约通过getInitializationCode获取最终合约的代码,使用create部署最终合约(并将自己的钱转给最终合约,最终合约转钱给sender并自毁),自己调用selfdestruct销毁

4.Deploy

image.png

  1. 因为保险库合约地址对应的余额为0,所以没有调用保险库合约的call
  2. create2创建临时合约,通过getInitializationCode获取最终合约的代码,使用create部署最终合约(并将自己的钱转给最终合约),自己调用selfdestruct销毁。

5.Destroy

image.png

6.Deploy

image.png

  1. 保险库合约余额不为0,所以会call调用保险库合约,并在保险库合约里调用临时合约的call方法将自己的钱转给临时合约。
  2. 使用create2创建临时合约,通过getInitializationCode获取最终合约的代码,使用create部署最终合约(并将自己的钱转给最终合约),自己调用selfdestruct销毁。

本文参与区块链技术网 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 2022-10-25 14:34
  • 阅读 ( 206 )
  • 学分 ( 4 )
  • 分类:智能合约
© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发
区块链技术的头像-区块链开发网

昵称

取消
昵称表情代码图片