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]。