智能合约的升级
智能合约的特点是布署完成后不可修改,但是可以通过升级的方法修改业务逻辑。在做合约审计时经常会遇到合约有升级的功能,OpenZeppelin 里提供 ERC1967 代理升级合约,其原理是使用 delegatecall 委托调用。
delegatecall 委托调用
delegatecall 和 call 都可以调用另一个合约,但是 delegatecall 是使用的委托调用,区别在于 A 合约使用 call 调用 B 合约,B 合约发送主币ETH给 C 合约,C 合约获取到的 msg.sender 是 B 合约,而使用 delegatecall 却不同,C 合约获取到的 msg.sender 是 A。下面我们来编代码测试效果,编写一个合约名为 Test,代码如下,三个变量 num, sender, value,num 是 setNum 函数输入的参数相加 2,sender 是调用者,value 是发送的主币数量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
pragma solidity ^0.8.0; contract Test { uint public num; address public sender; uint public value; function setNum(uint _num) external payable { num = _num + 2; sender = msg.sender; value = msg.value; } } |
再编写一个 UpgradeDelegatecall 合约,同样也是三个参数,setNum 函数里的 _target 参数是 Test 合约地址,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
contract UpgradeDelegatecall { uint public num; address public sender; uint public value; function setNum(address _target, uint _num) external payable { (bool success, bytes memory data) = _target.delegatecall( abi.encodeWithSelector(Test.setNum.selector, _num) //调用 Test合约里的setNum函数 ); require(success, "delegatecall failed"); } } |
将两个合约布署完成后,在 UpgradeDelegatecall 合约里输入 Test 合约地址,再输入 num 参数为 1,获取的数值会相加 2,即是 3,如下图所示:
然后我们尝试更新合约,将 Test 改名为 Test2,其中的 setNum 将输入的参数改为乘以 2,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
contract Test2 { uint public num; address public sender; uint public value; function setNum(uint _num) external payable { num = _num * 2; //乘以2 sender = msg.sender; value = msg.value; } } |
布署 Test2,在 UpgradeDelegatecall 合约里填入 Test2 的合约地址,输入 num 为 100,此时会看到 num 会乘以 2,即是 200,如下图所示。此时我们已经实现了一种简单的合约升级的功能,数据是保存在 UpgradeDelegatecall 合约,而逻辑可以升级。
ERC1967 升级合约
在上面我们使用了 delegatecall 完成了一个简单的合约升级的功能,但大部分合约使用的 ERC1967 升级合约,OpenZeppelin 提供了相应的接口,不过底层原理依然还是 delegatecall。下面我们编写一个 ERC1967 升级合约的实例,需要用到三个合约,第一个是 Test,功能和之前一样,还是三个参数 num, sender, value,setNum 函数会将输入的参数加 2,第二个是 ProxyAdmin,它主要负责管理代理升级合约,第三个是 TransparentUpgradeableProxy 它负责升级合约,会调用 delegatecall 委托调用新合约,完整的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
pragma solidity ^0.8.0; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract Test is Initializable,OwnableUpgradeable { function initialize()public initializer{ __Context_init_unchained(); __Ownable_init_unchained(); } uint public num; address public sender; uint public value; function setNum(uint _num) external payable { num = _num + 2; sender = msg.sender; value = msg.value; } } contract ProxyAdmin is Ownable { function getProxyImplementation(TransparentUpgradeableProxy proxy) public view virtual returns (address) { // We need to manually run the static call since the getter cannot be flagged as view // bytes4(keccak256("implementation()")) == 0x5c60da1b (bool success, bytes memory returndata) = address(proxy).staticcall(hex"5c60da1b"); require(success); return abi.decode(returndata, (address)); } function getProxyAdmin(TransparentUpgradeableProxy proxy) public view virtual returns (address) { // We need to manually run the static call since the getter cannot be flagged as view // bytes4(keccak256("admin()")) == 0xf851a440 (bool success, bytes memory returndata) = address(proxy).staticcall(hex"f851a440"); require(success); return abi.decode(returndata, (address)); } function changeProxyAdmin(TransparentUpgradeableProxy proxy, address newAdmin) public virtual onlyOwner { proxy.changeAdmin(newAdmin); } function upgrade(TransparentUpgradeableProxy proxy, address implementation) public virtual onlyOwner { proxy.upgradeTo(implementation); } function upgradeAndCall( TransparentUpgradeableProxy proxy, address implementation, bytes memory data ) public payable virtual onlyOwner { proxy.upgradeToAndCall{value: msg.value}(implementation, data); } } contract TransparentUpgradeableProxy is ERC1967Proxy { constructor( address _logic, address admin_, bytes memory _data ) payable ERC1967Proxy(_logic, _data) { assert(_ADMIN_SLOT == bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1)); _changeAdmin(admin_); } modifier ifAdmin() { if (msg.sender == _getAdmin()) { _; } else { _fallback(); } } function admin() external ifAdmin returns (address admin_) { admin_ = _getAdmin(); } function implementation() external ifAdmin returns (address implementation_) { implementation_ = _implementation(); } function changeAdmin(address newAdmin) external virtual ifAdmin { _changeAdmin(newAdmin); } function upgradeTo(address newImplementation) external ifAdmin { _upgradeToAndCall(newImplementation, bytes(""), false); } function upgradeToAndCall(address newImplementation, bytes calldata data) external payable ifAdmin { _upgradeToAndCall(newImplementation, data, true); } function _admin() internal view virtual returns (address) { return _getAdmin(); } function _beforeFallback() internal virtual override { require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target"); super._beforeFallback(); } } |
布署这三个合约,需要注意的是布署 TransparentUpgradeableProxy 需要提供三个参数,第一个是 Test 合约地址,第二个是 Proxy Admin 的地址,第三个是 initialize 函数的签名,如下图所示:
然后选择 Test 输入 TransparentUpgradeableProxy 合约的地址,点击 At Address 加载,如下图所示:
加载完成后 TransparentUpgradeableProxy 合约的地址,我们看到的是 Test 合约的函数与变量,输入 1 点击 setNum,可以看到结果会相加 2,即是 3,如下图所示:
接下来我们需要测试升级合约,将 Test 合约里的 setNum 函数相加 2 的逻辑改成乘以 2,合约名改成 Test2,在 ProxyAdmin 里面 upgrade 函数输入 TransparentUpgradeableProxy 合约地址和 Test2 合约地址,如下图所示:
此时再回到 Test 合约,操作 setNum 输入 100,结果就是乘以2,即 200,如下图所示,说明合约升级成功。
转载请注明:exchen's blog » 智能合约的升级