Skip to content

Read a storage slot ​

Read stETH's raw storage — the slots where an Aragon app keeps its kernel and its id, and a counter you can check against a call.

cql
let stETH = ethereum:0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;

from stETH as s
| project {
    kernel:      s.$storage[keccak256('aragonOS.appStorage.kernel')] as address,
    appId:       s.$storage[keccak256('aragonOS.appStorage.appId')],
    elRewards:   s.$storage[keccak256('lido.Lido.totalELRewardsCollected')] as uint256,
    elRewardsFn: s.getTotalELRewardsCollected()
  }

Open in workbench →

kernelappIdelRewardselRewardsFn
ethereum:0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc0x3ca7c3e38968823ccb4c78ea688df41356f182ae1d159e4ee608d30d68cef320238142947935636941239384238142947935636941239384

kernel is the Aragon kernel that governs the app, appId the 32-byte id it is registered under, and elRewards the execution-layer rewards collected so far, in wei — the same number getTotalELRewardsCollected() returns, so the raw read and the call agree.

How it reads ​

s.$storage[slot] reads one word of the contract's storage at the row's block, with no ABI involved. The slot is a number or a 32-byte hex, and the answer is bytes32. Aragon and Lido keep these values at slots named in their source — keccak256('aragonOS.appStorage.kernel') — so keccak256 of the same string is the slot. as address reinterprets the word the way Solidity stores an address — the low 20 bytes — and as uint256, as bool, as bytes4 do the same for their types; appId stays as it is because it is a bytes32.

The cross-check is why you read storage at all. A call goes through the ABI and the proxy; a slot goes through neither. When the two agree you know which slot holds what, and on a contract with no verified ABI the slot is still there to read.

Mappings and arrays are not at fixed slots; slot() computes where they are:

cql
let stETH  = ethereum:0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
let wstETH = ethereum:0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;

from stETH as s
| project { raw: s.$storage[slot(mapping: 0, key: wstETH)] as uint256, shares: s.sharesOf(wstETH) }

Open in workbench →

slot(mapping: 0, key: wstETH) is the slot of m[wstETH] for a mapping m declared in slot 0; put it beside sharesOf(wstETH) and the two columns tell you whether stETH's share ledger is that mapping. slot(array: n, index: i) and slot(base: s, offset: k) cover dynamic arrays and struct members.

See also ​