Saltar al contenido
TrautsLablab

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.

  1. 1Preflight
  2. 2Fixture
  3. 3Layout V1
  4. 4Layout V2
  5. 5Slots crudos
  6. 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

  1. 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.mjs
    Fixture 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 storageLayout

    Comprueba

    • — 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.

  2. 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 -vv

    Comprueba

    • — 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 .

tools/labs/ethereum-storage-layout-lab/runbook.md
# 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.
tools/labs/ethereum-storage-layout-lab/preflight.mjs
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);
tools/labs/ethereum-storage-layout-lab/templates/foundry.toml
[profile.default]
src = "src"
test = "test"
out = "out"
solc_version = "0.8.28"
optimizer = true
optimizer_runs = 200
tools/labs/ethereum-storage-layout-lab/templates/src/StorageLab.sol
// 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;
}
tools/labs/ethereum-storage-layout-lab/templates/test/StorageLab.t.sol
// 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

Fixture
cd ../..
rm -rf .tmp/storage-layout-lab

Si algo falla

Síntoma, causa probable y cómo corregirlo
SíntomaCausaQué hacer
El preflight marca missing con rutas de templatesSe 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.