闽公网安备 35020302035485号
contract EntryPointContract {
address public owner = msg.sender;
uint256 public id = 5;
uint256 public updatedAt = block.timestamp;
}
我们看到它声明了 3 个状态变量,owner,id和updatedAt。这些状态变量有赋值,在存储中,它们看起来像这样:
第二个槽,索引为 1,保存了 "id"状态变量的值。
第三个槽,索引为 2,有第三个状态变量updatedAt的值。所有存储的数据都以十六进制表示,所以转换[6] 0x62fc3adb到十进制是 1660697307,用 js 转换为日期:const date = new Date(1660697307 * 1000); console.log(date)结果:
Tue Aug 16 2022 20:48:27 GMT-0400 (Atlantic Standard Time))所以,在访问状态变量id时,我们是在访问索引为 1 的槽。很好,那么,使用delegatecall的陷阱在哪里?为了让委托合约对主合约的存储进行修改,它同样需要声明自己的变量,其顺序与主合约的声明顺序完全相同,而且通常有相同数量的状态变量。例如,上面的 EntryPointContract 的委托合约,需要看起来是这样的:
contract DelegateContract {
address public owner;
uint256 public id;
uint256 public updatedAt;
}
有完全相同的状态变量,完全相同的类型,完全相同的顺序,最好有完全相同数量的状态变量。在此案例中,每个合约有 3 个状态变量。// 堆代码 www.duidaima.com
contract DelegateContract {
address public owner;
uint256 public id;
uint256 public updatedAt;
function setValues(uint256 _newId) public {
id = _newId;
}
}
contract EntryPointContract {
address public owner = msg.sender;
uint256 public id = 5;
uint256 public updatedAt = block.timestamp;
address delegateContract;
constructor(address _delegateContract) {
delegateContract = _delegateContract;
}
function delegate(uint256 _newId) public returns(bool) {
(bool success, ) =
delegateContract.delegatecall(abi.encodeWithSignature("setValues(uint256)",
_newId));
return success;
}
}
这里我们看到了一个真正简单的代理合约的实现。EntryPointContract有一个构造函数,接收部署的DelegateContract的地址来委托它的调用,以便自己的状态被DelegateContract修改。该delegate函数收到一个要设置的_newId,所以它使用低级别的delegatecall将该调用委托给DelegateContract 来更新id变量。
在用新的 id 值调用delegate函数,并检查EntryPointContract和DelegateContract合约的变量 id 值后,我们看到只有EntryPointContract的状态变量id有值,而DelegateContract的id状态变量没有赋值,仍然被设置为 0,因为DelegateContract修改的不是它自己的存储,而是EntryPointContract的存储。
contract DelegateContract {
address public owner;
// 堆代码 www.duidaima.com
// 注意:两个变量换了位置
uint256 public updatedAt;
uint256 public id;
function setValues(uint256 _newId) public {
id = _newId;
}
}
contract EntryPointContract {
address public owner = msg.sender;
uint256 public id = 5;
uint256 public updatedAt = block.timestamp;
address delegateContract;
constructor(address _delegateContract) {
delegateContract = _delegateContract;
}
function delegate(uint256 _newId) public returns(bool) {
(bool success, ) =
delegateContract.delegatecall(abi.encodeWithSignature("setValues(uint256)",
_newId));
return success;
}
}
现在 ,如果我用一个新的 id 值 15 再次调用delegate,会发生什么?





contract DelegateContract {
address public owner;
uint256 public id;
uint256 public updatedAt;
address public addressPlaceholder;
uint256 public unreachableValueByTheMainContract;
function setValues(uint256 _newId) public {
id = _newId;
unreachableValueByTheMainContract = 8;
}
}
contract EntryPointContract {
address public owner = msg.sender;
uint256 public id = 5;
uint256 public updatedAt = block.timestamp;
address public delegateContract;
constructor(address _delegateContract) {
delegateContract = _delegateContract;
}
function delegate(uint256 _newId) public returns(bool) {
(bool success, ) =
delegateContract.delegatecall(abi.encodeWithSignature("setValues(uint256)",
_newId));
return success;
}
}
我们看到,EntryPointContract仍然声明了 4 个状态变量,而DelegateContract声明了 5 个。我们知道,当EntryPointContract委托调用DelegateContract时,它将把自己的存储发送到DelegateContract.,但是EntryPointContract没有第五个状态变量(unreachableValueByTheMainContract)。那么,当DelegateContract修改它声明的但EntryPointContract没有声明的第五个变量时会发生什么?


web3.eth.getStorageAt("0xA80a6609e0cA08ed3D531FA1B8bbCC945b8ff409", 4)
返回:0x0000000000000000000000000000000000000000000000000000000000000008是的! 说明EntryPointContract 确实保存了这个数据。这是一种有趣的方式,即智能合约可以在部署后被 "扩展",只需在第一时间将其行动委托给另一个合约。这需要精心制作和设计。委托合约的地址需要能够在需要时被动态替换,这样入口点合约就可以在任何时候指向一个新的实现。有一些方法可以解决这个问题,其中之一就是EIP-1967: Standard Proxy Storage Slots[8]。