|
| 1 | +/** |
| 2 | + * Storage arrays have Solidity's layout. A fixed-size array is inline: |
| 3 | + * its elements start at its own slot. A dynamic array keeps its length |
| 4 | + * in its slot and its elements from keccak256(slot). Either way, an |
| 5 | + * element narrower than 16 bytes shares a slot with its neighbors, as |
| 6 | + * many as fit, from the low-order end; any other element starts a slot |
| 7 | + * of its own and takes as many slots as its type needs. An array, as a |
| 8 | + * struct member, starts a slot, and the next member starts the slot |
| 9 | + * after it. Each storage variable's pointer must read the values the |
| 10 | + * program wrote. |
| 11 | + */ |
| 12 | +import { describe, it, expect } from "vitest"; |
| 13 | +import { keccak256 } from "ethereum-cryptography/keccak"; |
| 14 | +import { bytesToHex } from "ethereum-cryptography/utils"; |
| 15 | +import { createMachineState } from "@ethdebug/evm"; |
| 16 | +import { dereference } from "@ethdebug/pointers"; |
| 17 | +import type * as Format from "@ethdebug/format"; |
| 18 | + |
| 19 | +import { executeProgram } from "#test/evm/behavioral"; |
| 20 | +import { traceLocals } from "#test/evm/locals"; |
| 21 | + |
| 22 | +const levels = [0, 1, 2, 3] as const; |
| 23 | + |
| 24 | +/** keccak256 of a slot number, as a dynamic array's first slot */ |
| 25 | +function hashed(slot: bigint): bigint { |
| 26 | + const word = new Uint8Array(32); |
| 27 | + for (let i = 0; i < 32; i++) { |
| 28 | + word[31 - i] = Number((slot >> BigInt(8 * i)) & 0xffn); |
| 29 | + } |
| 30 | + return BigInt("0x" + bytesToHex(keccak256(word))); |
| 31 | +} |
| 32 | + |
| 33 | +interface Case { |
| 34 | + source: string; |
| 35 | + /** Expected words, by slot */ |
| 36 | + slots: [bigint, bigint][]; |
| 37 | + /** Expected values, in order, of each variable's regions, by name */ |
| 38 | + pointers?: Record<string, Record<string, bigint[]>>; |
| 39 | +} |
| 40 | + |
| 41 | +const cases: Record<string, Case> = { |
| 42 | + "a fixed-size array is inline": { |
| 43 | + source: `storage { |
| 44 | + [0] f: array<uint256, 3>; |
| 45 | + [3] r: uint256; |
| 46 | +} |
| 47 | +code { f[0] = 10; f[2] = 30; r = f[0] + f[2]; }`, |
| 48 | + slots: [ |
| 49 | + [0n, 10n], |
| 50 | + [1n, 0n], |
| 51 | + [2n, 30n], |
| 52 | + [3n, 40n], |
| 53 | + ], |
| 54 | + pointers: { f: { element: [10n, 0n, 30n] } }, |
| 55 | + }, |
| 56 | + "narrow elements share a slot, from the low-order end": { |
| 57 | + source: `storage { |
| 58 | + [0] a: array<uint8, 40>; |
| 59 | + [2] r: uint256; |
| 60 | +} |
| 61 | +code { a[0] = 1; a[1] = 2; a[33] = 7; r = (a[1] as uint256) + (a[33] as uint256); }`, |
| 62 | + slots: [ |
| 63 | + [0n, 0x0201n], |
| 64 | + [1n, 0x0700n], |
| 65 | + [2n, 9n], |
| 66 | + ], |
| 67 | + pointers: { |
| 68 | + a: { |
| 69 | + element: Array.from({ length: 40 }, (_, i) => |
| 70 | + i === 0 ? 1n : i === 1 ? 2n : i === 33 ? 7n : 0n, |
| 71 | + ), |
| 72 | + }, |
| 73 | + }, |
| 74 | + }, |
| 75 | + "a fixed-size array of fixed-size arrays": { |
| 76 | + source: `storage { |
| 77 | + [0] m: array<array<uint256, 3>, 2>; |
| 78 | + [6] r: uint256; |
| 79 | +} |
| 80 | +code { m[0][1] = 4; m[1][2] = 5; r = m[1][2]; }`, |
| 81 | + slots: [ |
| 82 | + [1n, 4n], |
| 83 | + [5n, 5n], |
| 84 | + [6n, 5n], |
| 85 | + ], |
| 86 | + pointers: { m: { element: [0n, 4n, 0n, 0n, 0n, 5n] } }, |
| 87 | + }, |
| 88 | + "an array in a struct takes its slots": { |
| 89 | + source: `define { |
| 90 | + struct S { x: uint8; arr: array<uint256, 2>; y: uint8; }; |
| 91 | +} |
| 92 | +storage { |
| 93 | + [0] s: S; |
| 94 | + [4] r: uint256; |
| 95 | +} |
| 96 | +code { s.x = 1; s.arr[1] = 2; s.y = 3; r = s.arr[1] + (s.y as uint256); }`, |
| 97 | + slots: [ |
| 98 | + [0n, 1n], |
| 99 | + [1n, 0n], |
| 100 | + [2n, 2n], |
| 101 | + [3n, 3n], |
| 102 | + [4n, 5n], |
| 103 | + ], |
| 104 | + pointers: { s: { x: [1n], "arr-element": [0n, 2n], y: [3n] } }, |
| 105 | + }, |
| 106 | + "a fixed-size array of structs": { |
| 107 | + source: `define { |
| 108 | + struct P { a: uint256; b: uint256; }; |
| 109 | +} |
| 110 | +storage { |
| 111 | + [0] ps: array<P, 2>; |
| 112 | + [4] r: uint256; |
| 113 | +} |
| 114 | +code { ps[1].a = 6; ps[1].b = 7; ps[0].b = 8; r = ps[1].b; }`, |
| 115 | + slots: [ |
| 116 | + [1n, 8n], |
| 117 | + [2n, 6n], |
| 118 | + [3n, 7n], |
| 119 | + [4n, 7n], |
| 120 | + ], |
| 121 | + pointers: { ps: { a: [0n, 6n], b: [8n, 7n] } }, |
| 122 | + }, |
| 123 | + "a dynamic array's narrow elements share a slot": { |
| 124 | + source: `storage { |
| 125 | + [0] d: array<uint16>; |
| 126 | + [1] r: uint256; |
| 127 | +} |
| 128 | +code { d[0] = 3; d[1] = 5; r = d[1] as uint256; }`, |
| 129 | + slots: [ |
| 130 | + [hashed(0n), 0x00050003n], |
| 131 | + [1n, 5n], |
| 132 | + ], |
| 133 | + }, |
| 134 | +}; |
| 135 | + |
| 136 | +describe("storage array layout", () => { |
| 137 | + for (const [name, { source, slots }] of Object.entries(cases)) { |
| 138 | + for (const level of levels) { |
| 139 | + it(`${name} (level ${level})`, async () => { |
| 140 | + const result = await executeProgram(`name Layout;\n${source}`, { |
| 141 | + calldata: "", |
| 142 | + optimizationLevel: level, |
| 143 | + }); |
| 144 | + expect(result.callSuccess).toBe(true); |
| 145 | + for (const [slot, value] of slots) { |
| 146 | + expect(await result.getStorage(slot), `slot ${slot}`).toBe(value); |
| 147 | + } |
| 148 | + }); |
| 149 | + } |
| 150 | + } |
| 151 | +}); |
| 152 | + |
| 153 | +describe("storage array pointers", () => { |
| 154 | + for (const [name, { source, pointers }] of Object.entries(cases)) { |
| 155 | + if (!pointers) continue; |
| 156 | + it(name, async () => { |
| 157 | + const { program, executor, steps } = await traceLocals( |
| 158 | + `name Layout;\n${source}`, |
| 159 | + ); |
| 160 | + const state = createMachineState(executor, { |
| 161 | + traceStep: steps[steps.length - 1], |
| 162 | + }); |
| 163 | + for (const [identifier, regions] of Object.entries(pointers)) { |
| 164 | + const variable = ( |
| 165 | + program.context as Format.Program.Context.Variables |
| 166 | + ).variables.find((v) => v.identifier === identifier)!; |
| 167 | + const cursor = await dereference(variable.pointer!, { state }); |
| 168 | + const view = await cursor.view(state); |
| 169 | + for (const [regionName, values] of Object.entries(regions)) { |
| 170 | + const read = await Promise.all( |
| 171 | + view.regions |
| 172 | + .filter((region) => region.name === regionName) |
| 173 | + .map(async (region) => (await view.read(region)).asUint()), |
| 174 | + ); |
| 175 | + expect(read, `${identifier}: ${regionName}`).toEqual(values); |
| 176 | + } |
| 177 | + } |
| 178 | + }); |
| 179 | + } |
| 180 | +}); |
0 commit comments