Metamorphic合约原理介绍
August 3rd, 2022

术语科普

Creation bytecode

这是大家最常提及的bytecode,用来生成runtime bytecode,包括了构造函数逻辑和逻辑函数参数。在solidity里可以用type(ContractName).creationCode来获取。当你编译合约时,creation bytecode就会生成。在Remix里可以用compilation details里查看,如下图,划红线的就是creation bytecode。

Runtime bytecode

这是存储在链上用来描述智能合约的代码。这个代码不包括构造函数逻辑和参数,实际上是creation bytecode在evm里执行完后的返回的字节码。下文讲述的metamorphic合约就是利用了这个性质来实现用不同的bytecode部署相同地址的合约的。

合约的runtime bytecode可以在链上用extcodecopy获取,在solidity里可以用type(ContractName).runtimeCode来获取,runtime bytecode的hash可以用extcodehash获取。

Bytecode

这是一个概括性的术语,包括了runtime bytecode和creation byte,但更通常用来表示runtime bytecode。

Init code

实际上也是creation bytecode,这个术语在create2 opcode的文档里有提及。

Metamorphic合约

在Constantinople硬分叉后,EVM增加了create2这个opcode,允许传入待创建合约的creation bytecode和salt,部署合约在指定的地址,这个地址可以用keccak256(0xff ++ deployersAddr ++ salt ++ keccak256(bytecode))[12:]计算出来。而EVM里还有另外一个opcode,叫做selfdestruct,可以用来销毁当前合约,销毁后合约的runtime code会变成0,而且storage也会全部清空。

Metamorphic合约利用了上述两个特性,非常巧妙的实现了合约的升级。

//SPDX-License-Identifier: MIT

pragma solidity 0.8.1;

contract Factory {
    mapping (address => address) _implementations;

    event Deployed(address _addr);

    function deploy(uint salt, bytes calldata bytecode) public {

        bytes memory implInitCode = bytecode;

		/*
		* Metamorphic contract initialization code (29 bytes): 
		*
		*       0x5860208158601c335a63aaf10f428752fa158151803b80938091923cf3
		*
		* Description:
		*
		* pc|op|name         | [stack]                                | <memory>
		*
		* ** set the first stack item to zero - used later **
		* 00 58 getpc          [0]                                       <>
		*
		* ** set second stack item to 32, length of word returned from staticcall **
		* 01 60 push1
		* 02 20 outsize        [0, 32]                                   <>
		*
		* ** set third stack item to 0, position of word returned from staticcall **
		* 03 81 dup2           [0, 32, 0]                                <>
		*
		* ** set fourth stack item to 4, length of selector given to staticcall **
		* 04 58 getpc          [0, 32, 0, 4]                             <>
		*
		* ** set fifth stack item to 28, position of selector given to staticcall **
		* 05 60 push1
		* 06 1c inpos          [0, 32, 0, 4, 28]                         <>
		*
		* ** set the sixth stack item to msg.sender, target address for staticcall **
		* 07 33 caller         [0, 32, 0, 4, 28, caller]                 <>
		*
		* ** set the seventh stack item to msg.gas, gas to forward for staticcall **
		* 08 5a gas            [0, 32, 0, 4, 28, caller, gas]            <>
		*
		* ** set the eighth stack item to selector, "what" to store via mstore **
		* 09 63 push4
		* 10 aaf10f42 selector [0, 32, 0, 4, 28, caller, gas, 0xaaf10f42]    <>
		*
		* ** set the ninth stack item to 0, "where" to store via mstore ***
		* 11 87 dup8           [0, 32, 0, 4, 28, caller, gas, 0xaaf10f42, 0] <>
		*
		* ** call mstore, consume 8 and 9 from the stack, place selector in memory **
		* 12 52 mstore         [0, 32, 0, 4, 0, caller, gas]             <0xaaf10f42>
		*
		* ** call staticcall, consume items 2 through 7, place address in memory **
		* 13 fa staticcall     [0, 1 (if successful)]                    <address>
		*
		* ** flip success bit in second stack item to set to 0 **
		* 14 15 iszero         [0, 0]                                    <address>
		*
		* ** push a third 0 to the stack, position of address in memory **
		* 15 81 dup2           [0, 0, 0]                                 <address>
		*
		* ** place address from position in memory onto third stack item **
		* 16 51 mload          [0, 0, address]                           <>
		*
		* ** place address to fourth stack item for extcodesize to consume **
		* 17 80 dup1           [0, 0, address, address]                  <>
		*
		* ** get extcodesize on fourth stack item for extcodecopy **
		* 18 3b extcodesize    [0, 0, address, size]                     <>
		*
		* ** dup and swap size for use by return at end of init code **
		* 19 80 dup1           [0, 0, address, size, size]               <> 
		* 20 93 swap4          [size, 0, address, size, 0]               <>
		*
		* ** push code position 0 to stack and reorder stack items for extcodecopy **
		* 21 80 dup1           [size, 0, address, size, 0, 0]            <>
		* 22 91 swap2          [size, 0, address, 0, 0, size]            <>
		* 23 92 swap3          [size, 0, size, 0, 0, address]            <>
		*
		* ** call extcodecopy, consume four items, clone runtime code to memory **
		* 24 3c extcodecopy    [size, 0]                                 <code>
		*
		* ** return to deploy final code in memory **
		* 25 f3 return         []                                        *deployed!*
		*/
        bytes memory metamorphicCode  = (
          hex"5860208158601c335a63aaf10f428752fa158151803b80938091923cf3"
        );

         // determine the address of the metamorphic contract.
        address metamorphicContractAddress = _getMetamorphicContractAddress(salt, metamorphicCode);

        // declare a variable for the address of the implementation contract.
        address implementationContract;

        // load implementation init code and length, then deploy via CREATE.
        /* solhint-disable no-inline-assembly */
        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.
          )
        } /* solhint-enable no-inline-assembly */

        //first we deploy the code we want to deploy on a separate address
        // store the implementation to be retrieved by the metamorphic contract.
        _implementations[metamorphicContractAddress] = implementationContract;



        address addr;
        assembly {
            let encoded_data := add(0x20, metamorphicCode) // load initialization code.
            let encoded_size := mload(metamorphicCode)     // load init code's length.
            addr := create2(0, encoded_data, encoded_size, salt)
        }

         require(
          addr == metamorphicContractAddress,
          "Failed to deploy the new metamorphic contract."
        );
        emit Deployed(addr);
    }

    /**
    * @dev Internal view function for calculating a metamorphic contract address
    * given a particular salt.
    */
    function _getMetamorphicContractAddress(
        uint256 salt,
        bytes memory metamorphicCode
        ) internal view returns (address) {

        // determine the address of the metamorphic contract.
        return address(
          uint160(                      // downcast to match the address type.
            uint256(                    // convert to uint to truncate upper digits.
              keccak256(                // compute the CREATE2 hash using 4 inputs.
                abi.encodePacked(       // pack all inputs to the hash together.
                  hex"ff",              // start with 0xff to distinguish from RLP.
                  address(this),        // this contract will be the caller.
                  salt,                 // pass in the supplied salt value.
                  keccak256(
                      abi.encodePacked(
                        metamorphicCode
                      )
                    )     // the init code hash.
                )
              )
            )
          )
        );
    }

    //those two functions are getting called by the metamorphic Contract
    function getImplementation() external view returns (address implementation) {
        return _implementations[msg.sender];
    }
}
contract Test1 {
    uint public myUint;

    function setUint(uint _myUint) public {
        myUint = _myUint;
    }

    function killme() public {
        selfdestruct(payable(msg.sender));
    }
}

contract Test2 {
    uint public myUint;

    function setUint(uint _myUint) public {
        myUint = 2*_myUint;
    }

    function killme() public {
        selfdestruct(payable(msg.sender));
    }

}

5860208158601c335a63aaf10f428752fa158151803b80938091923cf3,这串bytecode的原理是staticcall调用getImplementation方法,获取implementation合约地址,再用extcodecopy把implementation合约的runtime bytecode复制到memory,做为当前部署合约的runtime bytecode,以此来动态替换合约的runtime bytecode,而合约地址又不变。

升级合约步骤

  • 部署Factory合约
  • Test1的bytecode和一个固定的salt,调用Factory合约的deploy方法部署Test1合约,Deployed事件的参数就是Test1合约的地址
  • 调用Test1合约的killme方法,销毁合约
  • Test2的bytecode和部署Test1时相同的salt,调用Factory合约的deploy方法部署Test2合约,Deployed事件的参数就是Test2合约的地址
  • 这样,就实现了合约地址不变,存储和逻辑更新的升级合约模式,而且没有用代理的delegateCall

识别与防范

审计合约里有没有出现selfdestruct,或者通过delegatecall或者callcode去调用selfdestruct。因为不能销毁也就意味着不能重新部署。如果存在selfdestruct,那要检查合约的部署者是否用create2创建此合约,如果是用create2创建的,就要辨别一下部署器是不是metamorphic合约,如果是那要小心了。

参考文献

Subscribe to franx.eth
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from franx.eth

Skeleton

Skeleton

Skeleton