<aside> 📘 Relates:

Solidity

Solidity layout and access of storage state variables simply explained

Solidity delegatecall usage and pitfalls

Understanding Smart Contract Vulnerabilities

</aside>

<aside> 📘 TL;DR;

按照惯例,用 proxy(文中称为 EntryPointContract) 和 implementation(文中称为 DelegateContract) 来指代入口和逻辑合约。

proxy contract

proxy contract

本文介绍了 slots 的映射规则,就记住如下几点即可:

  1. 变量的声明顺序,决定了其所占用的 slot index,和变量名字完全无关
  2. delegatecall 时,proxy 会将自己的变量和值,发送给 implementation,按照 slot index 进行覆盖
  3. implementation 对变量的所有操作,不要看变量名,而要看其 slot 所对应的 proxy 内的变量
  4. constant/immutable 不占用 slot

按照 openzepplin 的设计,proxy 内只放三个变量(implementation、admin、beacon),其他所有的变量,尽可能地放到 implementation 内。这样做可以简化复杂度,就是需要注意升级 implementation 时需要对齐新旧版本的 slots。

</aside>


Solidity delegatecall is a low-level function that allows us to load and call the code of another contract while preserving the context of the main contract. This means that the code of the called contract is executed but any state changes made by the called contract are actually made on the main contract storage, not on the called contract’s storage.

This is useful to create libraries and the proxy contract pattern, where we delegate the call to a different contract, “giving it” permission to modify the state of the calling contract.

This function also has some pitfalls that we need to be aware of and is basically what this article will focus on.

As explained in another article regarding the layout of state variables in storage, every state variable declared in a contract occupies a slot in the storage, potentially sharing a common slot with other state variables if their type is smaller than 32 bytes and they can fit together in a single slot.

Solidity layout and access of storage state variables simply explained

So, when we access these state variables, to assign values to them or to read from them, Solidity uses the declaration position of that state variable to know what storage slot to access and read from it or update it.

For example, given the following contract:

contract EntryPointContract {
    address public owner = msg.sender;
    uint256 public id = 5;
    uint256 public updatedAt = block.timestamp;
}

We see that it declares 3 state variables, owner, id and updatedAt. These state variables have some values assigned, and in storage, they would look something like this:

Untitled

We see that at slot index 0 we have the value of the first state variable, left padded with zeros because each slot holds 32 bytes of data.

The second slot, which has index 1, has the value of the id state variable.

The third slot, with index 2, has the value o the third state variable updatedAt. All the data in storage is represented as hexadecimal, so converting 0x62fc3adb to decimal is 1660697307, which converted to date with js:

const date = new Date(1660697307 * 1000);
console.log(date)

Produces:

Tue Aug 16 2022 20:48:27 GMT-0400 (Atlantic Standard Time))

So, when accessing the state variable id, we are accessing the slot with index 1.