Ejecutable local con Foundry
Storage layout y upgrades
Ver en qué slot, offset y ancho guarda Solidity cada variable, y por qué reordenar campos rompe un upgrade.
- Duración
- 25–40 min
- Nivel
- Intermedio Solidity
- Pasos
- 2
- Versión
- 3.29.0-alpha.1
Modelo mental
El storage es un arreglo de palabras de 32 bytes. Solidity empaqueta variables pequeñas en la misma palabra y los mappings guardan sus valores en keccak256(clave, slot). Cambiar el orden cambia qué bytes lee cada variable.
- 1Preflight
- 2Fixture
- 3Layout V1
- 4Layout V2
- 5Slots crudos
- 6Cleanup
Antes de empezar
- Foundry instalado: forge en el PATH (https://getfoundry.sh).
- Primera ejecución con internet: forge descarga solc 0.8.28 si no lo tienes.
- Node.js 20 o superior para el preflight (que también revisa anvil y cast).
Pasos
Paso 1Comparar los dos layouts
Ver slot, offset y bytes de cada campo en la versión original y en la incompatible.
Preflight node tools/labs/ethereum-storage-layout-lab/preflight.mjsFixture y layouts mkdir -p .tmp/storage-layout-lab cp -R tools/labs/ethereum-storage-layout-lab/templates/. .tmp/storage-layout-lab/ cd .tmp/storage-layout-lab forge inspect StorageLab storageLayout forge inspect StorageLabUnsafeV2 storageLayoutComprueba
- — El preflight devuelve un JSON con forge, anvil y cast disponibles y missing: [].
- — StorageLab: owner en slot 0 offset 0 (20 bytes), counter en slot 0 offset 20 (12 bytes), enabled en slot 1 offset 0, mode en slot 1 offset 1, credits en slot 2.
- — StorageLabUnsafeV2: counter ocupa todo el slot 0 y owner pasa al slot 1; el owner original se leería como parte de un número.
Evidencia esperada
Las dos tablas de forge inspect lado a lado.
Paso 2Probar el empaquetado contra slots crudos
Confirmar con sload los valores exactos de cada palabra, no solo que no estén vacías.
Tests forge test -vvComprueba
- — forge test: 1 passed (testInitialStateAndPacking).
- — El test exige slot 0 = owner | 7 << 160 y slot 1 = 1 | 42 << 8.
- — El test exige que slot 2 esté vacío y que credits[deployer] = 100 viva en keccak256(abi.encode(deployer, 2)).
Evidencia esperada
Salida de forge test y la fórmula de cada slot.
El código del laboratorio
Estos son los archivos que ejecutan los pasos, tal como están en el repositorio .
# Ethereum storage layout lab
Version: `2.0.44-alpha.1`
Copy `templates/` into `.tmp/storage-layout-lab`, then run (from the repository root):
```bash
node tools/labs/ethereum-storage-layout-lab/preflight.mjs
mkdir -p .tmp/storage-layout-lab
cp -R tools/labs/ethereum-storage-layout-lab/templates/. .tmp/storage-layout-lab/
cd .tmp/storage-layout-lab
forge inspect StorageLab storageLayout
forge inspect StorageLabUnsafeV2 storageLayout
forge test -vv
```
Compare the slot, offset, byte width and type of every field. The unsafe V2 intentionally changes the first slots; it is evidence, not a deployment recommendation.
The test reads raw slots with `sload` and checks the exact packed words: slot 0 = `owner | counter << 160`, slot 1 = `enabled | mode << 8`, and `credits[deployer]` at `keccak256(abi.encode(deployer, 2))`.
Cleanup removes `.tmp/storage-layout-lab` (`rm -rf .tmp/storage-layout-lab`). No Anvil process is needed for this lab.import { spawnSync } from 'node:child_process';
import { existsSync } from 'node:fs';
const commands = ['forge', 'anvil', 'cast'];
const versions = Object.fromEntries(
commands.map((command) => {
const result = spawnSync(command, ['--version'], { encoding: 'utf8' });
return [command, { available: result.status === 0, version: (result.stdout || result.stderr || result.error?.message || '').trim().split('\n')[0] }];
})
);
const artifacts = [
'tools/labs/ethereum-storage-layout-lab/templates/foundry.toml',
'tools/labs/ethereum-storage-layout-lab/templates/src/StorageLab.sol',
'tools/labs/ethereum-storage-layout-lab/templates/test/StorageLab.t.sol',
];
const missing = artifacts.filter((artifact) => !existsSync(artifact));
console.log(JSON.stringify({ versions, missing }, null, 2));
if (commands.some((command) => !versions[command].available) || missing.length) process.exit(1);[profile.default]
src = "src"
test = "test"
out = "out"
solc_version = "0.8.28"
optimizer = true
optimizer_runs = 200// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
contract StorageLab {
address public owner;
uint96 public counter;
bool public enabled;
uint16 public mode;
mapping(address => uint256) public credits;
constructor() {
owner = msg.sender;
counter = 7;
enabled = true;
mode = 42;
credits[msg.sender] = 100;
}
function rawSlot(uint256 slot) external view returns (bytes32 value) {
assembly {
value := sload(slot)
}
}
}
// Deliberately incompatible with StorageLab: do not use as an upgrade.
contract StorageLabUnsafeV2 {
uint256 public counter;
address public owner;
bool public enabled;
uint16 public mode;
mapping(address => uint256) public credits;
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {StorageLab} from "../src/StorageLab.sol";
contract StorageLabTest {
function testInitialStateAndPacking() public {
StorageLab lab = new StorageLab();
require(lab.owner() == address(this), "owner");
require(lab.counter() == 7, "counter");
require(lab.enabled(), "enabled");
require(lab.mode() == 42, "mode");
require(lab.credits(address(this)) == 100, "credits");
// Slot 0: owner (bytes 0..19) + counter (bytes 20..31).
bytes32 expectedSlot0 = bytes32((uint256(7) << 160) | uint256(uint160(address(this))));
require(lab.rawSlot(0) == expectedSlot0, "slot 0 packs owner + counter");
// Slot 1: enabled (byte 0) + mode (bytes 1..2).
bytes32 expectedSlot1 = bytes32((uint256(42) << 8) | uint256(1));
require(lab.rawSlot(1) == expectedSlot1, "slot 1 packs enabled + mode");
// Slot 2 is the mapping marker; the value lives at keccak256(key . slot).
require(lab.rawSlot(2) == bytes32(0), "mapping marker slot stays empty");
uint256 creditSlot = uint256(keccak256(abi.encode(address(this), uint256(2))));
require(uint256(lab.rawSlot(creditSlot)) == 100, "credits[deployer] at keccak slot");
}
}Criterios de aceptación
- forge test muestra 1 passed.
- Puedes señalar qué campo de V2 leería los bytes de owner de V1.
- Puedes calcular a mano el slot de un valor de mapping.
Limpieza
cd ../..
rm -rf .tmp/storage-layout-labSi algo falla
| Síntoma | Causa | Qué hacer |
|---|---|---|
| El preflight marca missing con rutas de templates | Se ejecutó fuera de la raíz del repositorio; usa rutas relativas. | Ejecuta el preflight desde la raíz del repo. |
| forge inspect falla con "No contract found" | Se ejecutó fuera de .tmp/storage-layout-lab. | Entra al fixture antes de inspeccionar. |
| Falla "slot 0 packs owner + counter" | Se cambió el orden o el tipo de las variables de StorageLab. | Compara con forge inspect y ajusta la expectativa solo si el cambio fue intencional. |