美文网首页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