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

在上一篇《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值。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,888评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,677评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,386评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,726评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,729评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,337评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,902评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,807评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,349评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,439评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,567评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,242评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,933评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,420评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,531评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,995评论 3 377
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,585评论 2 359

推荐阅读更多精彩内容