Storage Layout Annotations
Solidity and EIP-7201 namespace
Upgradeable contracts often need to add new state variables without interfering with existing storage layouts. To achieve this, they access storage by manually computing storage locations using hashing. EIP-7201 was proposed to standardize both the formula for deriving these storage locations and the documentation format for annotating them, making the process more reliable and interoperable.
/** @custom:storage-location erc7201:example.main */
struct MainStorage {
uint256 x;
uint256 y;
}
When storage is accessed using custom slot calculations—such as:
// keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant MAIN_STORAGE_LOCATION =
0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500;
function _getMainStorage() private pure returns (MainStorage storage _slot) {
assembly {
_slot.slot := MAIN_STORAGE_LOCATION
}
}
—the Certora Prover may not be able to automatically infer the correct storage layout, which can lead to analysis failures. By following the EIP-7201 proposal and using the automatic storage extension generation option, the Prover can accurately generate layout information for these annotated storage extensions. This improves reliability and reduces the risk of incorrect analysis when contracts use custom storage slots.
Not all contracts follow the EIP-7201 proposal. In such cases, the Certora Prover allows the user to explicitly specify storage layout information by writing an extension harness.
Table of Contents
Storage Extension Flags
To enable automatic storage extension, add one or both of the following flags to your .conf
JSON:
Flag |
Appearance in |
Pass directly to CLI option |
Purpose |
---|---|---|---|
storage_extension_annotation |
|
|
Detects |
extract_storage_extension_annotation |
|
|
Dumps the generated harness Solidity file(s) to |
storage_extension_harnesses |
|
`–storage_extension_harnesses A=H … |
Specifies that each contract |
Example:
{
// ...
"storage_extension_annotation": true,
"extract_storage_extension_annotation": true,
"storage_extension_harnesses" : [ "A1=H1", "A2=H2" ]
// ...
}
How automatic generation works (high level)
Scan ASTs – while compiling each
.sol
file, the Prover looks for struct definitions with a preceding comment@custom:storage-location erc7201:<namespace>
.Generate a minimal harness – for every unique namespace the tool emits an extra contract containing only:
Import statements for the original file,
One dummy state variable per namespace with the prefix
ext_<namespace>_
and the original struct name.
// auto-generated import "./VaultBridgeToken.sol"; contract _Auto_VaultBridgeTokenHarness_ { VaultBridgeTokenStorage ext_agglayer_vault_bridge_VaultBridgeToken_storage; // slot = keccak256("erc7201:agglayer.vault-bridge.VaultBridgeToken.storage")-1 & ~0xff }
Compile the harness with exactly the same
solc
flags that the main file is using.Splice the fields – the storage layout extracted from the harness is merged into the layout of every contract that inherits the annotated struct’s declaring contract.
(optional) Dump the harness into
.<build_dir>/
ifextract_storage_extension_annotation
is on.
User-provided harnesses
Not all contracts follow EIP-7201:
contract A {
struct MainStorage {
uint256 x;
uint256 y;
}
bytes32 private constant MAIN_NONSTANDARD_LOCATION = 0x12345678;
function _getMainStorage() private pure returns (MainStorage storage _slot) {
assembly {
_slot.slot := MAIN_NONSTANDARD_LOCATION
}
}
}
Here there is neither an annotation on the MainStorage
struct, nor is the storage slot
calculated according to the formula specified in the EIP. In this case, the user can manually
write a storage extension harness:
import {A} from "/path/to/A.sol";
contract H {
/** @custom:certoralink 0x12345678 */
A.MainStorage m;
}
and provide the CLI option --storage_extension_harnesses A=H
(or add "storage_extension_harnesses": ["A=H"]
to the .conf
file)
The extension harness may have several such state variables. Each variable must have
a @custom:certoralink
NatSpec annotation followed by a hexadecimal string. Each such
/** @custom:certoralink s_i */
T_i x_i;
annotation will instruct the Prover that storage slot s_i
has the layout corresponding to T_i
.
Quick example
contract VaultBridgeToken {
/** @custom:storage-location erc7201:agglayer.vault-bridge.VaultBridgeToken.storage */
struct VaultBridgeTokenStorage {
IERC20 underlyingToken;
uint256 reservedAssets;
// ...
}
}
With
{
// ...
"storage_extension_annotation": true
// ...
}
you can immediately assert over currentContract.ext_agglayer_vault_bridge_VaultBridgeToken_storage.underlyingToken
in CVL:
rule no_reserved_assets_leak(env e) {
require currentContract._reservedAssets() == 0;
deposit(e, 100);
assert currentContract.ext_agglayer_vault_bridge_VaultBridgeToken_storage.reservedAssets >= 100;
}
Troubleshooting
“Slot already declared” Two different annotations resolved to the same slot. Double-check the namespace strings and inheritance chain.