在上一篇《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
,然后依次部署Processor
、Proxy
和Client
合约,执行setName
方法,可以接收到返回值:
这样一来,我们的问题就解决了。
2.去掉无用的Storage变量定义(非结构化存储)
(1) 背景
由于delegatecall
的特性,就使得Proxy
和Processor
都必须保持相同的存储结构:
Student public student;
address public processor;
实际上对于Proxy来说,student在本合约的逻辑中是没有出现过的变量;而对于Processor来说,processor又是个无用的变量,仅仅是通过占位来保持和Proxy的存储结构一致。
(2) 解决
解决这个问题的主要思想是:
-
Proxy
合约的低位存储中留下一张白纸,“业务数据”(本例中的student
)一律不做定义,留给Processor
合约管理;而Proxy
合约自己需要的“控制性变量”(本例中的processor
)通过指定存储slot避开低位存储slot。 -
Processor
也不需要定义Proxy
合约中才需要的“控制性变量”(本例中的processor
)
proxy中需要自己操作的变量:用sload
和sstore
。
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;
}
}
可以发现,这里我们取消了Processor
对IProcessor
的继承。
我们现在的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
获取,发现能获取到结构体:
student
值。
网友评论