Proxy库对外只暴露了fallback和receive函数,是代理合约的基础实现。所有对Proxy合约的call都将被delegatecall到implement合约且delegatecall的执行结果会原封不动地返还给Proxy合约的调用方。通常称implement合约为代理合约背后的逻辑合约。
[openzeppelin]:v4.8.3,[forge-std]:v1.5.6
Github: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.3/contracts/proxy/Proxy.sol
Proxy库对外只暴露了fallback和receive函数,是代理合约的基础实现。所有对Proxy合约的call都将被delegatecall到implement合约并且delegatecall的执行结果会原封不动地返还给Proxy合约的调用方。我们通常称implement合约为代理合约背后的逻辑合约。
继承Proxy合约:
// 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测试合约:
测试使用的物料合约:
// 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);
}
}
将当前的call,委托调用到implementation地址。
注:通过内联汇编“黑魔法”,使得没有返回值的_delegate()函数可以动态返回delegatecall的返回值。
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代码逻辑的地方。全部内存都只供汇编代码块使用。只要在内联汇编中手动管理好内存指针,内存就是安全的。
_implementation()
:返回逻辑合约的地址。该函数未带实现体,需要在主合约中进行重写;_beforeFallback()
:执行delegatecall之前会执行的hook函数,如果有需要可以重写该函数并在其中增添逻辑。 function _implementation() internal view virtual returns (address);
function _beforeFallback() internal virtual {}
foundry代码验证:
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);
}
}
fallback()
:当本合约被携带calldata的call调用时,进入该函数。随即将该call的calldata直接delegatecall到逻辑合约;receive()
:当本合约被不携带任何calldata的call调用时,进入该函数。随即直接delegatecall到逻辑合约(不携带任何calldata)。 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代码验证:
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神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢!
公众号名称:后现代泼痞浪漫主义奠基人
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!