|
| 1 | +/** |
| 2 | + * `push` on a dynamic array in storage, as in Solidity: `a.push(v)` |
| 3 | + * writes `v` at index `a.length`, then adds one to the length; |
| 4 | + * `a.push()` adds a zero element. An index at or past the length |
| 5 | + * reverts with Panic(0x32), for reads and writes, as in memory. |
| 6 | + */ |
| 7 | +import { describe, it, expect } from "vitest"; |
| 8 | +import { keccak256 } from "ethereum-cryptography/keccak"; |
| 9 | +import { bytesToHex } from "ethereum-cryptography/utils"; |
| 10 | +import { createMachineState } from "@ethdebug/evm"; |
| 11 | +import { dereference } from "@ethdebug/pointers"; |
| 12 | +import type * as Format from "@ethdebug/format"; |
| 13 | + |
| 14 | +import { compile } from "#compiler"; |
| 15 | +import { executeProgram } from "#test/evm/behavioral"; |
| 16 | +import { traceLocals } from "#test/evm/locals"; |
| 17 | + |
| 18 | +const levels = [0, 1, 2, 3] as const; |
| 19 | + |
| 20 | +const panic32 = "4e487b71" + 0x32n.toString(16).padStart(64, "0"); |
| 21 | + |
| 22 | +function word(value: bigint): Uint8Array { |
| 23 | + const bytes = new Uint8Array(32); |
| 24 | + for (let i = 0; i < 32; i++) { |
| 25 | + bytes[31 - i] = Number((value >> BigInt(8 * i)) & 0xffn); |
| 26 | + } |
| 27 | + return bytes; |
| 28 | +} |
| 29 | + |
| 30 | +/** keccak256 of words, as a dynamic array's first slot */ |
| 31 | +function hashed(...values: bigint[]): bigint { |
| 32 | + const data = new Uint8Array(32 * values.length); |
| 33 | + values.forEach((value, i) => data.set(word(value), 32 * i)); |
| 34 | + return BigInt("0x" + bytesToHex(keccak256(data))); |
| 35 | +} |
| 36 | + |
| 37 | +const passes: Record<string, [string, [bigint, bigint][]]> = { |
| 38 | + "push values and read them back": [ |
| 39 | + `storage { [0] h: array<uint256>; [1] r: uint256; } |
| 40 | +code { h.push(5); h.push(7); r = h[1] + h.length; }`, |
| 41 | + [ |
| 42 | + [0n, 2n], |
| 43 | + [hashed(0n), 5n], |
| 44 | + [hashed(0n) + 1n, 7n], |
| 45 | + [1n, 9n], |
| 46 | + ], |
| 47 | + ], |
| 48 | + "push narrow elements into one slot": [ |
| 49 | + `storage { [0] h: array<uint8>; } |
| 50 | +code { h.push(1); h.push(2); }`, |
| 51 | + [ |
| 52 | + [0n, 2n], |
| 53 | + [hashed(0n), 0x0201n], |
| 54 | + ], |
| 55 | + ], |
| 56 | + "push an empty element, then into it": [ |
| 57 | + `storage { [0] m: array<array<uint256>>; [1] r: uint256; } |
| 58 | +code { m.push(); m[0].push(4); r = m[0][0] + m.length + m[0].length; }`, |
| 59 | + [ |
| 60 | + [0n, 1n], |
| 61 | + [hashed(0n), 1n], |
| 62 | + [hashed(hashed(0n)), 4n], |
| 63 | + [1n, 6n], |
| 64 | + ], |
| 65 | + ], |
| 66 | + "push into a mapping's array": [ |
| 67 | + `storage { [0] q: mapping<uint256, array<uint256>>; [1] r: uint256; } |
| 68 | +code { q[3].push(9); r = q[3][0]; }`, |
| 69 | + [ |
| 70 | + [hashed(3n, 0n), 1n], |
| 71 | + [hashed(hashed(3n, 0n)), 9n], |
| 72 | + [1n, 9n], |
| 73 | + ], |
| 74 | + ], |
| 75 | + "push into a struct's array": [ |
| 76 | + `define { struct S { n: uint8; xs: array<uint256>; }; } |
| 77 | +storage { [0] s: S; [2] r: uint256; } |
| 78 | +code { s.n = 1; s.xs.push(6); r = s.xs[0]; }`, |
| 79 | + [ |
| 80 | + [0n, 1n], |
| 81 | + [1n, 1n], |
| 82 | + [hashed(1n), 6n], |
| 83 | + [2n, 6n], |
| 84 | + ], |
| 85 | + ], |
| 86 | + "write an element below the length": [ |
| 87 | + `storage { [0] h: array<uint256>; } |
| 88 | +code { h.push(1); h.push(2); h[1] = 3; }`, |
| 89 | + [[hashed(0n) + 1n, 3n]], |
| 90 | + ], |
| 91 | +}; |
| 92 | + |
| 93 | +const reverts: Record<string, string> = { |
| 94 | + "write at the length": `storage { [0] h: array<uint256>; } |
| 95 | +code { h.push(1); h[1] = 2; }`, |
| 96 | + "read an empty array": `storage { [0] h: array<uint256>; [1] r: uint256; } |
| 97 | +code { r = h[0]; }`, |
| 98 | + "write into an empty inner array": `storage { [0] m: array<array<uint256>>; } |
| 99 | +code { m.push(); m[0][0] = 1; }`, |
| 100 | +}; |
| 101 | + |
| 102 | +describe("push on a storage array", () => { |
| 103 | + for (const [name, [body, slots]] of Object.entries(passes)) { |
| 104 | + for (const level of levels) { |
| 105 | + it(`should ${name} (level ${level})`, async () => { |
| 106 | + const result = await executeProgram(`name Push;\n${body}`, { |
| 107 | + calldata: "", |
| 108 | + optimizationLevel: level, |
| 109 | + }); |
| 110 | + expect(result.callSuccess).toBe(true); |
| 111 | + for (const [slot, value] of slots) { |
| 112 | + expect(await result.getStorage(slot), `slot ${slot}`).toBe(value); |
| 113 | + } |
| 114 | + }); |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + for (const [name, body] of Object.entries(reverts)) { |
| 119 | + for (const level of levels) { |
| 120 | + it(`should revert: ${name} (level ${level})`, async () => { |
| 121 | + const result = await executeProgram(`name Push;\n${body}`, { |
| 122 | + calldata: "", |
| 123 | + optimizationLevel: level, |
| 124 | + }); |
| 125 | + expect(result.callSuccess).toBe(false); |
| 126 | + expect(bytesToHex(result.returnValue)).toBe(panic32); |
| 127 | + }); |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + it("gives the pushed elements to the array's pointer", async () => { |
| 132 | + const { program, executor, steps } = await traceLocals( |
| 133 | + `name Push;\nstorage { [0] h: array<uint16>; } |
| 134 | +code { h.push(5); h.push(7); h.push(9); }`, |
| 135 | + ); |
| 136 | + const state = createMachineState(executor, { |
| 137 | + traceStep: steps[steps.length - 1], |
| 138 | + }); |
| 139 | + const variable = ( |
| 140 | + program.context as Format.Program.Context.Variables |
| 141 | + ).variables.find((v) => v.identifier === "h")!; |
| 142 | + const cursor = await dereference(variable.pointer!, { state }); |
| 143 | + const view = await cursor.view(state); |
| 144 | + const read = await Promise.all( |
| 145 | + view.regions |
| 146 | + .filter((region) => region.name === "element") |
| 147 | + .map(async (region) => (await view.read(region)).asUint()), |
| 148 | + ); |
| 149 | + expect(read).toEqual([5n, 7n, 9n]); |
| 150 | + }); |
| 151 | +}); |
| 152 | + |
| 153 | +describe("push type errors", () => { |
| 154 | + const errors: Record<string, string> = { |
| 155 | + "push on a fixed-size array": `storage { [0] f: array<uint256, 3>; } |
| 156 | +code { f.push(1); }`, |
| 157 | + "push on a memory array": `code { let a: array<uint256> = [1, 2]; a.push(3); }`, |
| 158 | + "push a value of the wrong type": `storage { [0] h: array<uint8>; } |
| 159 | +code { h.push(true); }`, |
| 160 | + "push two values": `storage { [0] h: array<uint256>; } |
| 161 | +code { h.push(1, 2); }`, |
| 162 | + }; |
| 163 | + for (const [name, body] of Object.entries(errors)) { |
| 164 | + it(`rejects ${name}`, async () => { |
| 165 | + const result = await compile({ |
| 166 | + to: "bytecode", |
| 167 | + source: `name Push;\n${body}`, |
| 168 | + }); |
| 169 | + expect(result.success).toBe(false); |
| 170 | + }); |
| 171 | + } |
| 172 | +}); |
0 commit comments