美文网首页Solidity智能合约专题
Solidity合约代理模式的几个技术技巧

Solidity合约代理模式的几个技术技巧

作者: 梁帆 | 来源:发表于2022-11-08 20:55 被阅读0次

    在上一篇《Solidity合约代理模式的简单示例》中,我们最后探讨了这种较为简易的实现方式的两个缺陷。

    1.获取fallback函数中delegatecall的返回值

    (1) 背景

    在fallback函数中:

        fallback() external payable  {
            (bool success, bytes memory data) = processor.delegatecall(msg.data);
            require(
                success,
                "error"
            );
        }
    

    这一行

    (bool success, bytes memory data) = processor.delegatecall(msg.data);
    

    的返回值data其实是无法返回给外部调用者的,这在我们实际操作中,就会带来很多不便。
    解决方法可以参考OpenZeppelin的Proxy设计。

    (2)解决

    在open zeppelin的Proxy代码中,没有用delegatecall,而是用了汇编写了个_delegate函数:

        /**
         * @dev Delegates the current call to `implementation`.
         *
         * This function does not return to its internal call site, it will return directly to the external caller.
         */
        function _delegate(address implementation) internal virtual {
            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())
                }
            }
        }
    

    我们把这个_delete函数替换掉原来的delegatecall函数,fallback如下:

        fallback() external payable  {
            _delegate(processor);
        }
    

    我们再将Processor合约中的方法setStudentAge设置返回值uint256 age,然后依次部署ProcessorProxyClient合约,执行setName方法,可以接收到返回值:

    remix命令行结果
    这样一来,我们的问题就解决了。

    2.去掉无用的Storage变量定义(非结构化存储)

    (1) 背景

    由于delegatecall的特性,就使得ProxyProcessor都必须保持相同的存储结构:

        Student public student;
        address public processor;
    

    实际上对于Proxy来说,student在本合约的逻辑中是没有出现过的变量;而对于Processor来说,processor又是个无用的变量,仅仅是通过占位来保持和Proxy的存储结构一致。

    (2) 解决

    解决这个问题的主要思想是:

    • Proxy合约的低位存储中留下一张白纸,“业务数据”(本例中的student)一律不做定义,留给Processor合约管理;而Proxy合约自己需要的“控制性变量”(本例中的processor)通过指定存储slot避开低位存储slot。
    • Processor也不需要定义Proxy合约中才需要的“控制性变量”(本例中的processor)

    proxy中需要自己操作的变量:用sloadsstore

    sload: 加载256位
    sstore: 存储256位

    在去掉“业务数据”student后,我们用keccak256函数计算出一个很大的数字:

        bytes32 private constant processorPosition = keccak256("https://www.jianshu.com/u/e75f05c11c97");
    

    一般在合约上线时,可以事先把这个值计算出来,在定义时直接赋值。
    这个数值表示的位置就可以用sstore来存入我们新的Processor合约的地址:

        function setProcessor(address newProcessor) internal {
            bytes32 position = processorPosition;
            assembly {
                sstore(position, newProcessor)
            }
        }
    

    同理,可以通过sload得到改地址存入的值:

        function processor() public view returns (address impl) {
            bytes32 position = processorPosition;
            assembly {
                impl := sload(position)
            }
        }
    

    这样做完,我们的Proxy.sol合约就变成了:

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.7;
    import "./IProxy.sol";
    
    contract Proxy {
        bytes32 private constant processorPosition = keccak256("https://www.jianshu.com/u/e75f05c11c97");
    
        function upgradeTo(address newProcessor) external {
            setProcessor(newProcessor);
        }
    
        function processor() public view returns (address impl) {
            bytes32 position = processorPosition;
            assembly {
                impl := sload(position)
            }
        }
    
        function setProcessor(address newProcessor) internal {
            bytes32 position = processorPosition;
            assembly {
                sstore(position, newProcessor)
            }
        }
    
        function _delegate(address implementation) internal virtual {
            assembly {
                calldatacopy(0, 0, calldatasize())
    
                let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
    
                returndatacopy(0, 0, returndatasize())
    
                switch result
                case 0 {
                    revert(0, returndatasize())
                }
                default {
                    return(0, returndatasize())
                }
            }
        }
    
        fallback() external payable  {
            _delegate(processor());
        }
    
        receive() external payable {
            _delegate(processor());
        }
    }
    

    而在我们的Processor合约中,在去掉填充位置的processor变量后,则有:

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.7;
    import {Gender, Student} from "./Types.sol";
    
    contract Processor {
        Student public student;
    
        function getStudent() external view returns (Student memory) {
            return student;
        }
    
        function setStudentName(string calldata _name) external {
            student.name = _name;
        }
    
        function setStudentGender(Gender _gender) external {
            student.gender = _gender;
        }
    
        function setStudentAge(uint256 _age) external returns(uint256) {
            student.age = _age;
            return _age;
        }
    }
    

    可以发现,这里我们取消了ProcessorIProcessor的继承。
    我们现在的IProcessor接口名已经改成了IProxy

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.7;
    import {Gender, Student} from "./Types.sol";
    
    interface IProxy {
        function getStudent() external view returns (Student memory);
        function setStudentName(string memory) external;
        function setStudentGender(Gender) external;
        function setStudentAge(uint256) external returns (uint256);
    }
    

    这是考虑到这个接口主要是提供给Proxy地址来调用的,用IProxy来命名更为合适。此外,考虑到这样一个情况,就是当Processor中含有一个非引用型变量,如:

    Contract Processor {
        uint256 x;
        ...
    }
    

    因为非引用型变量自带public函数,所以我们不需要像获取student那样,需要在Processor中手动写个getStudent函数,而只需要在IProxy接口文件中写入:

    Interface IProxy {
        function x() external view returns (uint256);
        ...
    }
    

    即可,此时的IProxy,就不能被Processor所继承,因为interface中的x()是方便Proxy读取的,而Processor已经有了x值即有了默认的x()函数,因此会报错。
    我们这个例子中的student,是一个引用类型,必须手动写getter函数,所以体现不出来这里不使用接口继承的好处。
    总之,这个IProxy的接口,只给Proxy用就好了,不需要给Processor继承。
    我们在Client合约中,加入一个获取Proxy合约中的student值的方法:

    contract Client {
        IProxy ip;
    
        constructor(address _proxy) {
            ip = IProxy(_proxy);
        }
    
        function getProxyStudent() external view returns (Student memory) {
            return ip.getStudent();
        }
        ...
    }
    

    通过Client进行setStudentAge的操作后,再使用getProxyStudent获取,发现能获取到结构体:

    getProxyStudent获取到的结构体 以一个tuple的形式输出,确实是我们想要的student值。

    相关文章

      网友评论

        本文标题:Solidity合约代理模式的几个技术技巧

        本文链接:https://www.haomeiwen.com/subject/lcontdtx.html