实践create2进行合约无缝升级

关于create和create2对于create创建的合约地址的生成实践

关于create和create2

对于create创建的合约地址的生成,可以参考:https://learnblockchain.cn/article/4867#Recovery

以测试环境的交易https://goerli.etherscan.io/tx/0xb385c360b009d7c0a312fe9e411e13401a65626898f3b871b0d2f4a23ba14549 举例: 其创建的合约地址是:0xc65dc0119f3e2802cb29cd64104dcfe0aec658bf

% node
> var util = require('ethereumjs-util');
> var buf=[Buffer.from("91aa5df5b42ef16b658b1d2231d73e442cd9c6ed", "hex"), 266];
> util.keccak256(util.rlp.encode(buf)).toString("Hex").slice(-40);
'c65dc0119f3e2802cb29cd64104dcfe0aec658bf'

create2 使用

关于使用create2预知合约地址的资料,参考:https://eips.ethereum.org/EIPS/eip-1014 代码示例参考:https://solidity-by-example.org/app/create2/ 关于代码的几点解释如下:

// 这里的bytecode就是合约的creationCode,即可以通过type(C).creationCode得到。这个code里包含构造函数+执行函数以及构造函数的参数。
// 这里的create2函数的调用,中间两个参数:
//    add(bytecode, 0x20),表示的是bytecode的偏移量,这里个bytecode可以理解为一个指针。bytecode实际上是deploy的参数,在deploy的调用时,由于bytecode是一个变长字段,所以在编码时,是长度+实际内容的编码方式。左移这里的add(bytecode, 0x20)实际上是绕过了长度字段,指向了实际bytecode的内容
//    mload(bytecode),就是从bytecode这个地址,load一个32字节的值,这个值就是bytecode的长度。可以看看上面的说明。
function deploy(bytes memory bytecode, uint _salt) public payable {
    assembly {
        addr := create2(
            callvalue(), // wei sent with current call
            // Actual code starts after skipping the first 32 bytes
            add(bytecode, 0x20),
            mload(bytecode), // Load the size of code contained in the first 32 bytes
            _salt // Salt from function arguments
        )

        if iszero(extcodesize(addr)) {
            revert(0, 0)
        }
    }
}

所以,实际上create2是一个通过固定bytecode可以提前算好地址的合约创建。如果bytecode有变化,则创建的合约地址是不一样的。

create2 升级合约

那么有么有可能通过create2进行合约的升级呢?答案是肯定的

参考:https://ethereum-blockchain-developer.com/110-upgrade-smart-contracts/12-metamorphosis-create2/#overwriting-smart-contracts

过程解释如下:

image.png

图中的0xaaf10f42getImplementation()函数

> web3.utils.sha3("getImplementation()").slice(0,10)
'0xaaf10f42'

也就是,固定代码会调用getImplementation()函数获取动态代码段进行部署,绕过create2对bytecode的变化校验。

需要注意,create2再次部署时,需要目标合约地址是selfdestruct的,也就是目标地址是一个空合约。 另外,reinit的合约,在etherscan里是有标记的。如下图:

部署合约V1: image.png

销毁合约V1: image.png

重新部署合约V2: image.png

可以看到图中的合约地址都是同一个。

解读metamorphic源码

https://github.com/0age/metamorphic

ImmutableCreate2Factory.sol

这个合约提供了一个safeCreate2的方法。实际上就是用一个_deployed变量来记录是否在目标地址部署过,如果部署过,就不让部署了。

// 主要代码:

// 这里可以通过将salt的前20字节设置为跟msg.sender一样,就可以阻止其他人重新部署合约。因为其他人的msg.sender无法使用这个salt,所以部署不了。
modifier containsCaller(bytes32 salt) {
 require(
      (address(bytes20(salt)) == msg.sender) ||
      (bytes20(salt) == bytes20(0)),
      "Invalid salt - first 20 bytes of the salt must match calling address."
    );
    _;
}

function safeCreate2(
    bytes32 salt,
    bytes calldata initializationCode
  ) external payable containsCaller(salt) returns (address deploymentAddress) {
  //...
  // 获取目标地址,也就是最终部署完后的合约地址
  address targetDeploymentAddress = address(....);
  //防止二次部署
  require(
      !_deployed[targetDeploymentAddress],
      "Invalid contract creation - contract has already been deployed."
    );
    // 使用create2进行部署,调用参数传入的bytecode
    assembly {                                // solhint-disable-line
      let encoded_data := add(0x20, initCode) // load initialization code.
      let encoded_size := mload(initCode)     // load the init code's length.
      deploymentAddress := create2(           // call CREATE2 with 4 arguments.
        callvalue,                            // forward any attached value.
        encoded_data,                         // pass in initialization code.
        encoded_size,                         // pass in init code's length.
        salt                                  // pass in the salt value.
      )
    }
  }
  //...
  _deployed[deploymentAddress] = true;// 设置防止重新部署

TransientContract.sol

这个合约就只有一个构造函数,调用create生成一个合约,然后立马selfdestruct掉。是需要和后面MetamorphicContractFactory.sol里合约的deployMetamorphicContractWithConstructor方法配合使用。

//关键代码:

contract TransientContract {
    constructor() public payable {
        // 这里是从msg.sender合约调用getInitializationCode()方法,来获取初始化代码。
        // 所以调用这个合约的调用合约(msg.sender)一定有一个getInitializationCode()方法
        bytes memory initCode = FactoryInterface(msg.sender).getInitializationCode();
        //通过create生成一个合约。
        assembly {
          let encoded_data := add(0x20, initCode) // load initialization code.
          let encoded_size := mload(initCode)     // load init code's length.
          metamorphicContractAddress := create(   // call CREATE with 3 arguments.
            callvalue,                            // forward any supplied endowment.
            encoded_data,                         // pass in initialization code.
            encoded_size                          // pass in init code's length.
          )
        } /* solhint-enable no-inline-assembly */
        // 将生成的合约selfdestruct。注意,selfdestruct时,会将余额转移到msg.sender
        selfdestruct(metamorphicContractAddress);
    }
}

该合约的具体作用,在讨论MetamorphicContractFactory.soldeployMetamorphicContractWithConstructor时再解析

MetamorphicContractFactory.sol

构造函数

先看下合约的构造函数,构造函数会传递进来一个transientContractInitializationCode 也就是临时合约的初始化代码,从链上部署情况来看,是:

608060408190527f57b9f52300000000000000000000000000000000000000000000000000000000815260609033906357b9f5239060849060009060048186803b1801561004c57600080fd5b505afa158015610060573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052602081101561008957600080fd5b8101908080516401000000008111156100a157600080fd5b820160208101848111156100b457600080fd5b81516401000000008111828201871017156100ce57600080fd5b505092919050505090506000816020018251808234f0925050506001600160a01b0381166100fb57600080fd5b806001600160a01b0316fffe

这段编码实际上是什么呢: 实际上就是TransientContract.sol合约。可以通过将合约用solidity 0.5.6版本编译(选择Enable optimization)。比如我在remix里编译的:

image.png

image.png

image.png

另外,合约里有个_metamorphicContractInitializationCode变量,赋值的是: 5860208158601c335a63aaf10f428752fa158151803b80938091923cf3 那么这个是什么呢?转为op code看下,就是上文图中画的。参考create2 升级合约

OK,我们现在明白了,MetamorphicContractFactory.sol合约里的_transientContractInitializationCode就是TransientContract.sol合约内容。 _metamorphicContractInitializationCode实际上就是调用getImplementation()获取地址的执行代码(EXTCODECOPY)。

下面我们看下这个合约主要提供的三个创建变质合约方法:

deployMetamorphicContract

// 函数参数里的两个calldata变量,
// implementationContractInitializationCode - 需要部署的合约字节码
// metamorphicContractInitializationCalldata - 部署完以后,需要执行的初始化函数字节码
// 因为下面的实现上,create2只会拷贝合约执行代码,不会执行初始化。所以需要创建完合约后立马进行初始化。
function deployMetamorphicContract(
    bytes32 salt,
    bytes calldata implementationContractInitializationCode,
    bytes calldata metamorphicContractInitializationCalldata
  ) external payable containsCaller(salt) returns (
    address metamorphicContractAddress
  ) {

  //...
  // 这里就是调用create,使用参数传递进来的字节码创建一个合约。这个合约本身会执行初始化构造函数。
   assembly {
      let encoded_data := add(0x20, implInitCode) // load initialization code.
      let encoded_size := mload(implInitCode)     // load init code's length.
      implementationContract := create(       // call CREATE with 3 arguments.
        0,                                    // do not forward any endowment.
        encoded_data,                         // pass in initialization code.
        encoded_size                          // pass in init code's length.
      )
    }
    //...
    // 这里记住了目标部署合约地址对应的合约。所以,在下面create2的时候,使用的是EXTCODECOPY,这个op实际上拷贝的是合约的执行代码(必然没有构造代码)。
    _implementations[metamorphicContractAddress] = implementationContract;

    // 执行create2,实际上就是从上面create的合约里,拷贝合约执行代码到新的合约里。
    // initCode是5860208158601c335a63aaf10f428752fa158151803b80938091923cf3
    // 这个上面有说明。
    assembly {
      let encoded_data := add(0x20, initCode) // load initialization code.
      let encoded_size := mload(initCode)     // load the init code's length.
      deployedMetamorphicContract := create2( // call CREATE2 with 4 arguments.
        0,                                    // do not forward any endowment.
        encoded_data,                         // pass in initialization code.
        encoded_size,                         // pass in init code's length.
        salt                                  // pass in the salt value.
      )
    }

   // 这里就是调用初始化方法,因为create2是没有调用构造函数的(构造函数没有办法拷贝过来)    
    if (data.length > 0 || msg.value > 0) {                                          
      (bool success,) = deployedMetamorphicContract.call.value(msg.value)(data);     
      require(success, "Failed to initialize the new metamorphic contract.");        
    }
  }

画个图:

image.png

可以看到实际上create2是从create的那个合约拷贝的执行代码。

deployMetamorphicContractFromExistingImplementation

这个函数和上面的类似,只是不用调用create方法去创建tmp合约了,直接用参数给出的合约地址。

deployMetamorphicContractWithConstructor

这个函数和上面两个的区别,主要是调用的顺序反掉了,先调用create2然后调用create。这样可以直接调用构造函数,而不用外部传递过来初始化函数。 来个图:

image.png

这里为何要在临时合约上调用selfdestruct,是为了让临时合约销毁掉,这样下次再调用时,其合约的nonce值还是从1开始,那么临时合约通过create创建的最终合约地址才不会变(最终合约地址跟msg.sendemsg.nonce相关)

另外还需要注意:如果想更新最终部署的合约,仍旧需要先将先前部署的最终部署合约selfdestruct掉,否则create方法会报错。

具体实例参考:https://goerli.etherscan.io/tx/0xb29f441f809e67e70e3b9385bfcd8b72a594c032b0fd1683b1771f759e7524fd

另外一个更容易理解的例子:

交易1: 1) 使用create2创建合约A(0x43cF39292785Ff959fF35E59dE698697A59E5404)

2) 在创建的合约A中用create创建合约B(0xD11e08dca07F7AD7acF2412F15CBec1b50390EF6) 3) 调用合约Aselfdestruct方法,销毁合约A https://goerli.etherscan.io/tx/0xfac1f1821f67d646454e80e7b809898f8fe0282db07a51245c33ab74f7260c63#internal

交易2:调用合约Bselfdestruct方法销毁合约 https://goerli.etherscan.io/tx/0x163c501c3fc45d2e088c6a3af300685f5cd370cb0ba1be22e4d58de601a33bbf

交易3:重复交易1 https://goerli.etherscan.io/tx/0x84004b0620cbc9ff0e972a611632b30b9b9abdbdd25b2e1e22af481b370cb805#internal

可以看到交易1交易3创建的最终合约地址是一样的,都是0xD11e08dca07F7AD7acF2412F15CBec1b50390EF6

这里再重点说下一个函数:

// 这个函数就是保证create地址不变的关键。注意这里transientContractAddress地址不变是由create2来保证,nonce=0x01写死了,也是因为让临时合约selfdestruct来实现的。
function _getMetamorphicContractAddressWithConstructor(
    address transientContractAddress
  ) internal pure returns (address) { 
    // determine the address of the metamorphic contract.
    return address(
      uint160(                          // downcast to match the address type.
        uint256(                        // set to uint to truncate upper digits.
          keccak256(                    // compute CREATE hash via RLP encoding.
            abi.encodePacked(           // pack all inputs to the hash together.
              byte(0xd6),               // first RLP byte.
              byte(0x94),               // second RLP byte.
              transientContractAddress, // called by the transient contract.
              byte(0x01)                // nonce begins at 1 for contracts.
            )
          )
        )
      )
    );
  }

Metapod.sol

构造函数

这里有几个常量解释下

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合约构造函数的主要内容。

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'

destroy

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

recover

deploy差不多,只是多了个_triggerVaultFundsRelease函数


有问题欢迎留言探讨

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

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

昵称

取消
昵称表情代码图片