深入理解可升级智能合约

准备

为了理解可升级合约,必须理解proxy机制,要理解proxy机制,必须理解solidity的sstoresload,以及关于以太坊架构和存储结构(数据结构)。

关于Solidity中的sstoresload深入理解:

  • 非常好的一篇剖析: https://learnblockchain.cn/article/4172

简单概括一下:

  • sstore将一对key-value存入storage
  • sload按照key取出storage中的value
  • 一笔交易中可以多次sstoresload
  • key一般用slot(槽)代替,是32bytes的哈希
  • 以上的storage是某一个合约下面的storage

合约代码分析

基于solidity ^0.4.24

  • openzeppelin的实现: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Proxy.sol

  • USDC合约代码: https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48#code

其中Proxy:

// 抽象合约
contract Proxy {

    // fallback函数
    function () payable external {
        _fallback();
    }

    // 虚函数,需要子类实现
    function _implementation() internal view returns (address);

    // 以下是proxy合约通用代码,
    function _delegate(address implementation) internal {
        assembly {
            // 将msg.data,即交易的所有数据,复制到内存
            calldatacopy(0, 0, calldatasize)

            // 调用实现合约
            let result := delegatecall(gas, implementation, 0, calldatasize, 0, 0)

            // 将返回数据拷贝到内存
            returndatacopy(0, 0, returndatasize)

            switch result
            case 0 { revert(0, returndatasize) } // 0,失败
            default { return(0, returndatasize) } // 1, 成功
        }
    }

  // 子类可以重写次函数
  function _willFallback() internal {
  }

  // fallback函数实现
  function _fallback() internal {
    _willFallback();
    _delegate(_implementation());
  }
}
  • calldatacopy(t, f, s):将calldata(输入数据)从位置f开始复制s字节到mem(内存)的位置t。
  • delegatecall(g, a, in, insize, out, outsize):调用地址a的合约,输入为mem[in…(in+insize)) ,输出为mem[out…(out+outsize)), 提供g的gas 和v wei的以太坊。这个操作码在错误时返回0,在成功时返回1。
  • returndatacopy(t, f, s):将returndata(输出数据)从位置f开始复制s字节到mem(内存)的位置t。
  • switch:基础版if/else,不同的情况case返回不同值。可以有一个默认的default情况。
  • return(p, s):终止函数执行, 返回数据mem[p…(p+s))。
  • revert(p, s):终止函数执行, 回滚状态,返回数据mem[p…(p+s))。

参考代理合约: https://blog.csdn.net/weixin_30230009/article/details/127312438

示例

理解了代理(可升级)合约机制之后,我们动手实践一下

实现合约(implement):

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

contract Storage {

    uint256 number;

    function store(uint256 num) public {
        number = num;
    }

    function retrieve() public view returns (uint256){
        return number;
    }
}

代理合约:

// SPDX-License-Identifier: None

pragma solidity ^0.8.0;

library AddressUtils {

  /**
   * Returns whether the target address is a contract
   * @dev This function will return false if invoked during the constructor of a contract,
   * as the code is not actually created until after the constructor finishes.
   * @param addr address to check
   * @return whether the target address is a contract
   */
  function isContract(address addr) internal view returns (bool) {
    uint256 size;
    // XXX Currently there is no better way to check if there is a contract in an address
    // than to check the size of the code at that address.
    // See https://ethereum.stackexchange.com/a/14016/36603
    // for more details about how this works.
    // TODO Check this again before the Serenity release, because all addresses will be
    // contracts then.
    // solium-disable-next-line security/no-inline-assembly
    assembly { size := extcodesize(addr) }
    return size > 0;
  }

}
abstract contract Proxy {
  /**
   * @dev Fallback function.
   * Implemented entirely in `_fallback`.
   */
   fallback() payable external {
    _fallback();
  }

  receive() payable external {

  }

  /**
   * @return The Address of the implementation.
   */
  function _implementation() public virtual view returns (address);

  /**
   * @dev Delegates execution to an implementation contract.
   * This is a low level function that doesn't return to its internal call site.
   * It will return to the external caller whatever the implementation returns.
   * @param implementation Address to delegate.
   */
  function _delegate(address implementation) internal {
    assembly {
      // Copy msg.data. We take full control of memory in this inline assembly
      // block because it will not return to Solidity code. We overwrite the
      // Solidity scratch pad at memory position 0.
      calldatacopy(0, 0, calldatasize())

      // Call the implementation.
      // out and outsize are 0 because we don't know the size yet.
      let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)

      // Copy the returned data.
      returndatacopy(0, 0, returndatasize())

      switch result
      // delegatecall returns 0 on error.
      case 0 { revert(0, returndatasize()) }
      default { return(0, returndatasize()) }
    }
  }

  /**
   * @dev Function that is run as the first thing in the fallback function.
   * Can be redefined in derived contracts to add functionality.
   * Redefinitions must call super._willFallback().
   */
  function _willFallback() internal {
  }

  /**
   * @dev fallback implementation.
   * Extracted to enable manual triggering.
   */
  function _fallback() internal {
    _willFallback();
    _delegate(_implementation());
  }
}

contract TestSstore is Proxy {

    bytes32 private constant IMPLEMENTATION_SLOT = 0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3;

     constructor(address implementation_)  {
        assert(IMPLEMENTATION_SLOT == keccak256("org.zeppelinos.proxy.implementation"));

        setImplementation(implementation_);
    }

    function _implementation() public override view returns (address impl) {
        bytes32 slot = IMPLEMENTATION_SLOT;
        assembly {
            impl := sload(slot)
        }
    }


    function setImplementation(address newImplementation) public {
        require(AddressUtils.isContract(newImplementation), "Cannot set a proxy implementation to a non-contract address");

        bytes32 slot = IMPLEMENTATION_SLOT;

        assembly {
            sstore(slot, newImplementation)
        }
    }

}
  • 部署代理合约(Proxy)时候, 需要填写实现合约(implement)的地址
  • 为了获得调用代理合约的数据,可以先在remix里面调用实现合约,在Metamask中拿到数据,然后粘贴到代理合约调用处

示例:

总结

通过delegatecall进行调用实现合约,数据是存放在代理合约中,因此当“升级”实现合约合约后,不会影响现有的数据。

delegatecall很像“动态库”


关于delegatecallcall的对比:

猜你喜欢

转载自blog.csdn.net/yqq1997/article/details/128333040
今日推荐