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: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.3/contracts/proxy/Proxy.sol

Proxy库对外只暴露了fallback和receive函数,是代理合约的基础实现。所有对Proxy合约的call都将被delegatecall到implement合约并且delegatecall的执行结果会原封不动地返还给Proxy合约的调用方。我们通常称implement合约为代理合约背后的逻辑合约。

1. 目标合约

继承Proxy合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/src/proxy/MockProxy.sol

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: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/proxy/Proxy/Proxy.t.sol

测试使用的物料合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/proxy/Proxy/Implement.sol

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神。

以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。

同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下!

如果需要转发,麻烦注明作者。十分感谢!

公众号名称:后现代泼痞浪漫主义奠基人

相关推荐
天涯学馆7 天前
从零到英雄:Solidity 智能合约开发全攻略
后端·智能合约·solidity
MavenTalk8 天前
Move开发语言在区块链的开发与应用
开发语言·python·rust·区块链·solidity·move
jc0803kevin19 天前
solidity call使用
区块链·solidity
jc0803kevin22 天前
solidity selfdestruct合约销毁
区块链·solidity
jc0803kevin23 天前
solidity的struct对象,web3j java解析输出参数
java·web3·solidity
Keegan小钢1 个月前
智能合约开发工具Remix
web3·智能合约·solidity
applebomb1 个月前
vite server正则表达式
正则·vite·proxy·regexp·转发·server
yoona10201 个月前
盲拍合约:让竞拍更公平与神秘的创新解决方案
区块链·学习方法·solidity·remix·盲拍合约
0x派大星2 个月前
Solidity 设计模式:实现灵活与可扩展的智能合约架构
设计模式·架构·web3·区块链·智能合约·solidity
0x派大星2 个月前
Solidity 存储和内存管理:深入理解与高效优化
web3·区块链·智能合约·solidity