准备
为了理解可升级合约,必须理解proxy机制,要理解proxy机制,必须理解solidity的sstore
和sload
,以及关于以太坊架构和存储结构(数据结构)。
关于Solidity中的sstore
和sload
深入理解:
- 非常好的一篇剖析: https://learnblockchain.cn/article/4172
简单概括一下:
sstore
将一对key-value
存入storagesload
按照key
取出storage中的value
- 一笔交易中可以多次
sstore
和sload
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中拿到数据,然后粘贴到代理合约调用处
示例:
-
部署实现合约: https://goerli.etherscan.io/tx/0x6a26cf6ff348bc32831d8dbd00ed4aae807591613e48435407fa41cb73b78c02
-
部署代理合约: https://goerli.etherscan.io/tx/0xcba8a55c22a31285185190170cf6d2a150a84b87f0c31d041023e64da3a5d83c
-
通过代理合约调用实现合约的store函数,写入
12345
:https://goerli.etherscan.io/tx/0x3ac401e00f917898d6d47765132667414b4a5f290e599df7e5af537297fba78b -
直接读取实现合约,结果是
0
: -
读取代理合约, 结果是
12345
-
我们再“升级”一下实现合约:
contract Storage { uint256 public number; uint256 public nb; /** * @dev Store value in variable * @param num value to store */ function store(uint256 num) public { number = num ; nb = number * 2; } /** * @dev Return value * @return value of 'number' */ function retrieve() public view returns (uint256){ return number; } }
-
部署新的实现合约:https://goerli.etherscan.io/tx/0x4b5287ac0fd8756ac70cf50d4b66565310467f53c931489eaf6cf65014da116b
-
设置新实现合约地址: https://goerli.etherscan.io/tx/0x1f074c4a25f46fa7d6010a9e169847706aece07107b5a889918c20e84ea5d876
-
调用代理写入
12345
-
读取代理合约,
总结
通过delegatecall
进行调用实现合约,数据是存放在代理合约中,因此当“升级”实现合约合约后,不会影响现有的数据。
delegatecall
很像“动态库”
关于delegatecall
和 call
的对比: