前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实践create2进行合约无缝升级(2) - Metapod.sol 解析

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

作者头像
Tiny熊
发布2022-11-07 12:48:33
7110
发布2022-11-07 12:48:33
举报
文章被收录于专栏:深入浅出区块链技术

本文作者:gasshadow[1]

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

Metapod.sol

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

代码语言:javascript
复制
// _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

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

代码语言:javascript
复制
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("");
        }
    }

构造函数

这里有几个常量解释下

代码语言:javascript
复制
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差不多 不过这里有个校验,需要特别警示:

代码语言:javascript
复制
_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 就明白了:

代码语言:javascript
复制
[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 合约本身。见构造函数:

代码语言:javascript
复制
require(
      address(this) == address(0x000000000003212eb796dEE588acdbBbD777D4E7),
      "Incorrect deployment address."
    );

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

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

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

代码语言:javascript
复制
> 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,将钱转到最终合约。

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

代码语言:javascript
复制
//临时合约里创建的最终合约的初始化代码,实际上就是让
_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 分析下整体的交易流程。

deploy

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

Destroy

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

Recover

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

Deploy

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

Destroy

Deploy

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

参考资料

[1]

gasshadow: https://learnblockchain.cn/people/11678

[2]

文章:实践create2进行合约无缝升级: https://learnblockchain.cn/article/4915

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-10-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Metapod.sol
  • _triggerVaultFundsRelease
  • 构造函数
  • deploy
  • destroy
  • recover
  • 实例分析
    • deploy
      • Destroy
        • Recover
          • Deploy
            • Destroy
              • Deploy
                • 参考资料
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档