Michael.W基于Foundry精读Openzeppelin第59期——Proxy.sol

Michael.W基于Foundry精读Openzeppelin第59期------Proxy.sol

      • [0. 版本](#0. 版本)
        • [0.1 Proxy.sol](#0.1 Proxy.sol)
      • [1. 目标合约](#1. 目标合约)
      • [2. 代码精读](#2. 代码精读)
        • [2.1 _delegate(address implementation) internal](#2.1 _delegate(address implementation) internal)
        • [2.2 _implementation() internal && _beforeFallback() internal](#2.2 _implementation() internal && _beforeFallback() internal)
        • [2.3 fallback() && receive()](#2.3 fallback() && receive())

0. 版本

openzeppelin\]:v4.8.3,\[forge-std\]:v1.5.6 ##### 0.1 Proxy.sol Github: Proxy库对外只暴露了fallback和receive函数,是代理合约的基础实现。所有对Proxy合约的call都将被delegatecall到implement合约并且delegatecall的执行结果会原封不动地返还给Proxy合约的调用方。我们通常称implement合约为代理合约背后的逻辑合约。 #### 1. 目标合约 继承Proxy合约: Github: ```js // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; import "openzeppelin-contracts/contracts/proxy/Proxy.sol"; contract MockProxy is Proxy { address immutable private _IMPLEMENTATION_ADDR; bool immutable private _ENABLE_BEFORE_FALLBACK; event ProxyBeforeFallback(uint value); constructor( address implementationAddress, bool enableBeforeFallback ){ _IMPLEMENTATION_ADDR = implementationAddress; _ENABLE_BEFORE_FALLBACK = enableBeforeFallback; } function _implementation() internal view override returns (address){ return _IMPLEMENTATION_ADDR; } function _beforeFallback() internal override { if (_ENABLE_BEFORE_FALLBACK) { emit ProxyBeforeFallback(msg.value); } } } ``` 全部foundry测试合约: Github: 测试使用的物料合约: Github: ```js // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; contract Implement { uint public i; address public addr; uint[3] public fixedArray; uint[] public dynamicArray; mapping(uint => uint) public map; event ImplementReceive(uint value); event ImplementFallback(uint value); function setUint(uint target) external { i = target; } function setUintPayable(uint target) external payable { i = target; } function setAddress(address target) external { addr = target; } function setAddressPayable(address target) external payable { addr = target; } function setFixedArray(uint[3] memory target) external { fixedArray = target; } function setFixedArrayPayable(uint[3] memory target) external payable { fixedArray = target; } function setDynamicArray(uint[] memory target) external { dynamicArray = target; } function setDynamicArrayPayable(uint[] memory target) external payable { dynamicArray = target; } function setMapping(uint key, uint value) external { map[key] = value; } function setMappingPayable(uint key, uint value) external payable { map[key] = value; } function triggerRevert() external pure { revert("Implement: revert"); } function triggerRevertPayable() external payable { revert("Implement: revert"); } function getPure() external pure returns (string memory){ return "pure return value"; } receive() external payable { emit ImplementReceive(msg.value); } fallback() external payable { emit ImplementFallback(msg.value); } } ``` #### 2. 代码精读 ##### 2.1 _delegate(address implementation) internal 将当前的call,委托调用到implementation地址。 注:通过内联汇编"黑魔法",使得没有返回值的_delegate()函数可以动态返回delegatecall的返回值。 ```js function _delegate(address implementation) internal virtual { // 内联汇编 assembly { // 从当前calldata的position 0开始将全部calldata都复制到内存中。内存中的数据存储也是从位置0开始。 // 为何此处使用内存的起始position不是从0x40处取空闲内存指针?原因见后文。 calldatacopy(0, 0, calldatasize()) // 使用delegatecall去调用逻辑合约。 // 第一个参数:调用delegatecall的过程允许使用的gas上限。为gas(),即执行到此处剩余可用的全部gas; // 第二个参数:逻辑合约的地址; // 第三个参数:delegatecall所携带的calldata相关。calldata是从当前内存中获取,第三个参数为开始载入的内存position; // 第四个参数:delegatecall所携带的calldata相关。第四个参数为从内存中读取calldata的字节长度; // 综上可知,delegatecall所用的calldata就是进入_delegate(address implementation)时的calldata; // 第五个参数:delegatecall得到的返回数据存储在内存中,第五个参数为开始存储返回值的内存position; // 第六个参数:delegatecall得到的返回数据存储在内存中的字节长度。 // 注:由于第五和第六个参数都设为0,即用来存储返回数据的内存长度为0。很明显delegatecall的返回数据长度(如有)要大于设定的存储空间, // 此时,全部的返回数据都要用returndatacopy()来复制到内存中。具体细则详见:https://learnblockchain.cn/article/6309 let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) // 由于delegatecall()时设定的存储返回数据的空间为0,要用returndatacopy()和returndatasize()来获取全部的返回数据。 // 第一个参数:内存中存储返回数据的起始position,即从position 0处开始存储; // 第二个参数:返回数据被复制的起始position,即从头开始复制返回数据; // 第三个参数:复制返回数据的字节长度。returndatasize()表示未存储到delegatecall()时设定的存储空间的返回数据字节长度,此时该 // 值应该为全部返回数据字节长度。 // 综上可知,delegatecall()得到的全部返回数据都存储到从0开始的内存空间中 returndatacopy(0, 0, returndatasize()) // 判断delegatecall是否成功调用 switch result case 0 { // 如果delegatecall调用失败(例如gas不足),result为0 // 那么就直接revert,revert携带的数据为内存中存储的delegatecall的全部返回数据 revert(0, returndatasize()) } default { // 如果非0(即1),表示delegatecall调用成功 // 那么就进行函数返回,返回值为内存中存储的delegatecall的全部返回数据 return(0, returndatasize()) } } } ``` 为何`_delegate()`中使用内存的起始position不是从0x40处取空闲内存指针,而是直接从position 0开始? 答:因为在该内联汇编代码块结束时直接进行函数返回,不会再有回到solidity代码逻辑的地方。**全部内存都只供汇编代码块使用**。只要在内联汇编中手动管理好内存指针,内存就是安全的。 ##### 2.2 _implementation() internal \&\& _beforeFallback() internal * `_implementation()`:返回逻辑合约的地址。该函数未带实现体,需要在主合约中进行重写; * ` _beforeFallback()`:执行delegatecall之前会执行的hook函数,如果有需要可以重写该函数并在其中增添逻辑。 ```js function _implementation() internal view virtual returns (address); function _beforeFallback() internal virtual {} ``` **foundry代码验证:** ```js contract ProxyTest is Test { Implement private _implement = new Implement(); address payable private _testingAddress = payable(address(new MockProxy(address(_implement), false))); event ImplementFallback(uint value); event ImplementReceive(uint value); event ProxyBeforeFallback(uint value); function test_beforeFallback() external { _testingAddress = payable(address(new MockProxy(address(_implement), true))); Implement proxy = Implement(_testingAddress); uint proxyBalance = _testingAddress.balance; assertEq(proxyBalance, 0); uint ethValue = 1 wei; // case 1: test setUint() vm.expectEmit(_testingAddress); emit ProxyBeforeFallback(0); proxy.setUint(1024); // case 2:test setUintPayable() vm.expectEmit(_testingAddress); emit ProxyBeforeFallback(ethValue); proxy.setUintPayable{value: ethValue}(1024); assertEq(_testingAddress.balance, proxyBalance + ethValue); proxyBalance += ethValue; // case 3: test setAddress() vm.expectEmit(_testingAddress); emit ProxyBeforeFallback(0); proxy.setAddress(address(1)); // case 4: test setAddressPayable() vm.expectEmit(_testingAddress); emit ProxyBeforeFallback(ethValue); proxy.setAddressPayable{value: ethValue}(address(1)); assertEq(_testingAddress.balance, proxyBalance + ethValue); proxyBalance += ethValue; // case 5: test setFixedArray() vm.expectEmit(_testingAddress); emit ProxyBeforeFallback(0); uint[3] memory targetFixedArray = [uint(1024), 2048, 4096]; proxy.setFixedArray(targetFixedArray); // case 6: test setFixedArrayPayable() vm.expectEmit(_testingAddress); emit ProxyBeforeFallback(ethValue); proxy.setFixedArrayPayable{value: ethValue}(targetFixedArray); assertEq(_testingAddress.balance, proxyBalance + ethValue); proxyBalance += ethValue; // case 7: test setDynamicArray() vm.expectEmit(_testingAddress); emit ProxyBeforeFallback(0); // build dynamic array as input uint[] memory targetDynamicArray = new uint[](3); targetDynamicArray[0] = 1024; targetDynamicArray[1] = 2048; targetDynamicArray[2] = 4096; proxy.setDynamicArray(targetDynamicArray); // case 8: test setDynamicArrayPayable() vm.expectEmit(_testingAddress); emit ProxyBeforeFallback(ethValue); proxy.setDynamicArrayPayable{value: ethValue}(targetDynamicArray); assertEq(_testingAddress.balance, proxyBalance + ethValue); proxyBalance += ethValue; // case 9: test setMapping() vm.expectEmit(_testingAddress); emit ProxyBeforeFallback(0); proxy.setMapping(1024, 2048); // case 10: test setMapping() vm.expectEmit(_testingAddress); emit ProxyBeforeFallback(ethValue); proxy.setMappingPayable{value: ethValue}(1024, 2048); assertEq(_testingAddress.balance, proxyBalance + ethValue); proxyBalance += ethValue; // case 11: revert with any static call because it emits event in _beforeFallback() // and causes the evm error: "StateChangeDuringStaticCall" vm.expectRevert(); proxy.i(); vm.expectRevert(); proxy.addr(); vm.expectRevert(); proxy.fixedArray(0); vm.expectRevert(); proxy.dynamicArray(0); vm.expectRevert(); proxy.map(1024); vm.expectRevert(); proxy.triggerRevert(); vm.expectRevert(); proxy.getPure(); // case 12: revert in the function of implement during a call vm.expectRevert("Implement: revert"); proxy.triggerRevertPayable{value: ethValue}(); // case 13: call the function not exists in the implement // and delegate call to the fallback function of implement // without value vm.expectEmit(_testingAddress); emit ProxyBeforeFallback(0); emit ImplementFallback(0); bytes memory calldata_ = abi.encodeWithSignature("unknown()"); (bool ok,) = _testingAddress.call(calldata_); assertTrue(ok); // with value vm.expectEmit(_testingAddress); emit ProxyBeforeFallback(ethValue); emit ImplementFallback(ethValue); (ok,) = _testingAddress.call{value: ethValue}(calldata_); assertTrue(ok); assertEq(_testingAddress.balance, proxyBalance + ethValue); proxyBalance += ethValue; // case 14: call the proxy with empty call data // and delegate call to the receive function of implement // without value vm.expectEmit(_testingAddress); emit ProxyBeforeFallback(0); emit ImplementReceive(0); (ok,) = _testingAddress.call(""); assertTrue(ok); // with value vm.expectEmit(_testingAddress); emit ProxyBeforeFallback(ethValue); emit ImplementReceive(ethValue); (ok,) = _testingAddress.call{value: ethValue}(""); assertTrue(ok); assertEq(_testingAddress.balance, proxyBalance + ethValue); } } ``` ##### 2.3 fallback() \&\& receive() * `fallback()`:当本合约被携带calldata的call调用时,进入该函数。随即将该call的calldata直接delegatecall到逻辑合约; * `receive()`:当本合约被不携带任何calldata的call调用时,进入该函数。随即直接delegatecall到逻辑合约(不携带任何calldata)。 ```js fallback() external payable virtual { // 调用_fallback() _fallback(); } receive() external payable virtual { // 调用_fallback() _fallback(); } // 携带当前对本合约的call的calldata,delegatecall到逻辑合约 function _fallback() internal virtual { // delegatecall之前运行hook函数 _beforeFallback(); // 携带当前对本合约的call的calldata,delegatecall到逻辑合约 _delegate(_implementation()); } ``` **foundry代码验证:** ```js contract ProxyTest is Test { Implement private _implement = new Implement(); address payable private _testingAddress = payable(address(new MockProxy(address(_implement), false))); event ImplementFallback(uint value); event ImplementReceive(uint value); function test_Call() external { Implement proxy = Implement(_testingAddress); // case 1: set uint256 assertEq(proxy.i(), 0); assertEq(_implement.i(), 0); proxy.setUint(1024); // check storage by static call assertEq(proxy.i(), 1024); assertEq(_implement.i(), 0); // check storage by slot number bytes32 slotNumber = bytes32(uint(0)); assertEq(vm.load(_testingAddress, slotNumber), bytes32(uint(1024))); // case 2: set address assertEq(proxy.addr(), address(0)); assertEq(_implement.addr(), address(0)); proxy.setAddress(address(2048)); // check storage by static call assertEq(proxy.addr(), address(2048)); assertEq(_implement.addr(), address(0)); // check storage by slot number slotNumber = bytes32(uint(1)); assertEq(vm.load(_testingAddress, slotNumber), bytes32(uint(2048))); // case 3: set fixed array assertEq(proxy.fixedArray(0), 0); assertEq(_implement.fixedArray(0), 0); uint[3] memory targetFixedArray = [uint(1024), 2048, 4096]; proxy.setFixedArray(targetFixedArray); for (uint i; i < 3; ++i) { // check storage by static call assertEq(proxy.fixedArray(i), targetFixedArray[i]); assertEq(_implement.fixedArray(i), 0); // check storage by slot number slotNumber = bytes32(uint(2 + i)); assertEq(vm.load(_testingAddress, slotNumber), bytes32(targetFixedArray[i])); } // case 4: set dynamic array // revert during static call because dynamic array isn't initialized vm.expectRevert(); proxy.dynamicArray(0); vm.expectRevert(); _implement.dynamicArray(0); // build dynamic array as input uint[] memory targetDynamicArray = new uint[](3); targetDynamicArray[0] = 1024; targetDynamicArray[1] = 2048; targetDynamicArray[2] = 4096; proxy.setDynamicArray(targetDynamicArray); for (uint i; i < 3; ++i) { // check storage by static call assertEq(proxy.dynamicArray(i), targetDynamicArray[i]); vm.expectRevert(); assertEq(_implement.dynamicArray(i), 0); // check storage by slot number slotNumber = bytes32(uint(keccak256(abi.encodePacked(uint(5)))) + i); assertEq(vm.load(_testingAddress, slotNumber), bytes32(targetDynamicArray[i])); } // case 5: set mapping uint key = 1024; uint value = 2048; assertEq(proxy.map(key), 0); assertEq(_implement.map(key), 0); proxy.setMapping(key, value); // check storage by static call assertEq(proxy.map(key), value); assertEq(_implement.map(key), 0); // check storage by slot number slotNumber = bytes32(uint(keccak256(abi.encodePacked(key, uint(6))))); assertEq(vm.load(_testingAddress, slotNumber), bytes32(value)); // case 6: revert with msg vm.expectRevert("Implement: revert"); proxy.triggerRevert(); // case 7: call pure (staticcall) assertEq(proxy.getPure(), "pure return value"); // case 8: call the function not exists in the implement // and delegate call to the fallback function of implement vm.expectEmit(_testingAddress); emit ImplementFallback(0); bytes memory calldata_ = abi.encodeWithSignature("unknown()"); (bool ok,) = _testingAddress.call(calldata_); assertTrue(ok); // case 9: call without value and calldata // and delegate call to the receive function of implement vm.expectEmit(_testingAddress); emit ImplementReceive(0); (ok,) = _testingAddress.call(""); assertTrue(ok); } function test_PayableCall() external { Implement proxy = Implement(_testingAddress); uint proxyBalance = _testingAddress.balance; assertEq(proxyBalance, 0); // case 1: set uint256 payable assertEq(proxy.i(), 0); assertEq(_implement.i(), 0); uint ethValue = 1 wei; proxy.setUintPayable{value: ethValue}(1024); assertEq(_testingAddress.balance, proxyBalance + ethValue); proxyBalance += ethValue; // check storage by static call assertEq(proxy.i(), 1024); assertEq(_implement.i(), 0); // check storage by slot number bytes32 slotNumber = bytes32(uint(0)); assertEq(vm.load(_testingAddress, slotNumber), bytes32(uint(1024))); // case 2: set address payble assertEq(proxy.addr(), address(0)); assertEq(_implement.addr(), address(0)); proxy.setAddressPayable{value: ethValue}(address(2048)); assertEq(_testingAddress.balance, proxyBalance + ethValue); proxyBalance += ethValue; // check storage by static call assertEq(proxy.addr(), address(2048)); assertEq(_implement.addr(), address(0)); // check storage by slot number slotNumber = bytes32(uint(1)); assertEq(vm.load(_testingAddress, slotNumber), bytes32(uint(2048))); // case 3: set fixed array payable assertEq(proxy.fixedArray(0), 0); assertEq(_implement.fixedArray(0), 0); uint[3] memory targetFixedArray = [uint(1024), 2048, 4096]; proxy.setFixedArrayPayable{value: ethValue}(targetFixedArray); assertEq(_testingAddress.balance, proxyBalance + ethValue); proxyBalance += ethValue; for (uint i; i < 3; ++i) { // check storage by static call assertEq(proxy.fixedArray(i), targetFixedArray[i]); assertEq(_implement.fixedArray(i), 0); // check storage by slot number slotNumber = bytes32(uint(2 + i)); assertEq(vm.load(_testingAddress, slotNumber), bytes32(targetFixedArray[i])); } // case 4: set dynamic array payable // revert during static call because dynamic array isn't initialized vm.expectRevert(); proxy.dynamicArray(0); vm.expectRevert(); _implement.dynamicArray(0); // build dynamic array as input uint[] memory targetDynamicArray = new uint[](3); targetDynamicArray[0] = 1024; targetDynamicArray[1] = 2048; targetDynamicArray[2] = 4096; proxy.setDynamicArrayPayable{value: ethValue}(targetDynamicArray); assertEq(_testingAddress.balance, proxyBalance + ethValue); proxyBalance += ethValue; for (uint i; i < 3; ++i) { // check storage by static call assertEq(proxy.dynamicArray(i), targetDynamicArray[i]); vm.expectRevert(); assertEq(_implement.dynamicArray(i), 0); // check storage by slot number slotNumber = bytes32(uint(keccak256(abi.encodePacked(uint(5)))) + i); assertEq(vm.load(_testingAddress, slotNumber), bytes32(targetDynamicArray[i])); } // case 5: set mapping payable uint key = 1024; uint value = 2048; assertEq(proxy.map(key), 0); assertEq(_implement.map(key), 0); proxy.setMappingPayable{value: ethValue}(key, value); assertEq(_testingAddress.balance, proxyBalance + ethValue); proxyBalance += ethValue; // check storage by static call assertEq(proxy.map(key), value); assertEq(_implement.map(key), 0); // check storage by slot number slotNumber = bytes32(uint(keccak256(abi.encodePacked(key, uint(6))))); assertEq(vm.load(_testingAddress, slotNumber), bytes32(value)); // case 6: revert with msg payable vm.expectRevert("Implement: revert"); proxy.triggerRevertPayable{value: ethValue}(); // case 7: call the function not exists in the implement with value // and delegate call to the fallback function of implement vm.expectEmit(_testingAddress); emit ImplementFallback(ethValue); bytes memory calldata_ = abi.encodeWithSignature("unknown()"); (bool ok,) = _testingAddress.call{value: ethValue}(calldata_); assertTrue(ok); assertEq(_testingAddress.balance, proxyBalance + ethValue); proxyBalance += ethValue; // case 8: call with value and empty callata // and delegate call to the receive function of implement vm.expectEmit(_testingAddress); emit ImplementReceive(ethValue); (ok,) = _testingAddress.call{value: ethValue}(""); assertTrue(ok); assertEq(_testingAddress.balance, proxyBalance + ethValue); } } ``` ps: 本人热爱图灵,热爱中本聪,热爱V神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢! ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200301142732861.jpg) 公众号名称:后现代泼痞浪漫主义奠基人

相关推荐
Blockchina3 天前
第 4 章 | Solidity安全 权限控制漏洞全解析
安全·web3·区块链·智能合约·solidity
Blockchina4 天前
第十四章 | DeFi / DAO / GameFi 项目高级实战
web3·区块链·智能合约·solidity
谭光志4 天前
如何估算和优化 Gas
web3·区块链·solidity
0x派大星6 天前
打造更安全的区块链资产管理:Solidity 多重签名机制详解
安全·web3·区块链·智能合约·solidity
Blockchina6 天前
第十五章 | Layer2、Rollup 与 ZK 技术实战解析
python·web3·区块链·智能合约·solidity
Blockchina7 天前
第 2 章 | 智能合约攻击图谱全景解析
web3·区块链·智能合约·solidity·区块链安全
Blockchina7 天前
第 1 章 | 开篇词:Dapp安全 区块链安全 Web3安全 区块链合约一旦部署,安全就是生死线
安全·web3·区块链·智能合约·solidity·合约审计
Blockchina9 天前
第八章 | 函数修饰符与访问控制模式
java·python·区块链·智能合约·solidity
Blockchina9 天前
第十二章 | Solidity 智能合约前后端集成实战
java·python·区块链·智能合约·solidity