Skip to content

fix(contracts): free memory after each mutation in execute - #66

Merged
kyscott18 merged 1 commit into
mainfrom
fix/execute-memory-limit
Oct 14, 2026
Merged

kyscott18 merged 1 commit into
mainfrom
fix/execute-memory-limit

Conversation

@kyscott18

@kyscott18 kyscott18 commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • execute copies the arguments of each mutation into memory, and Solidity never frees memory. Memory grew with each mutation in a batch. With 10,000 concurrent mutations, the harness benchmark settled a batch that went above the 8 MiB memory limit per transaction on Monad (MONAD_MEMORY_LIMIT), and the transaction halted with OutOfGas(MemoryLimit).
  • execute now saves the free memory pointer before each mutation and sets it back after the mutation. _execute returns nothing, so no memory that it allocates stays live after it returns. The pointer is saved before the call because the call copies the arguments into memory.
  • The harness benchmark now sends 500 concurrent mutations, not 10,000. The runtime settles all pending mutations in one transaction, and 10,000 Credit mutations need about 220M gas. Monad accepts a maximum of 30M gas per transaction (MONAD_TX_GAS_LIMIT_CAP). The largest transaction in the benchmark now has about 550 mutations and uses about 12M gas.
  • The benchmark waits until all mutations are included before it closes. Before this change, a settlement transaction could still run when afterEach dropped the test database.
  • The benchmark prints the mean settlement gas for each mutation.
  • Regenerate the Token and OrderBook declarations for the new AST ids.

Known issue (not fixed here)

The runtime has no limit on the number of mutations in one settlement transaction. A burst of more than about 1,300 Credit mutations between two submits will go above the 30M gas limit. The local simulation does not find this, because relax_cfg sets tx_gas_limit_cap to u64::MAX.

Results

Harness benchmark
single user, 1,000 sequential mutations:
  average 2.82ms, p50 1.58ms, p99 18.29ms
max throughput, 500 concurrent mutations:
  3,875 mutations/s
settlement gas:
  23,959 per mutation

Over 6 runs, throughput was 3,300–3,910 mutations/s, except one run at 2,034. The 500-mutation window is short, so throughput is noisy. The mean gas was 23,895–23,959. It changes a little with the number of settlement transactions, because each transaction adds a fixed cost.

Test plan

  • bun run benchmark in packages/typewriter passes
  • bun run test in packages/typewriter passes
  • forge test for the typewriter, token, and order-book contracts passes
  • bun run typecheck passes

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 4/5

The PR appears safe to merge, with a non-blocking request for a focused memory-growth regression test.

Fix All in Claude CodeFindings

  1. P2 Memory reuse lacks regression coverage ▶
Fix with agent prompt
### Issue 1
packages/typewriter/src/Typewriter.sol:197-199
The new memory-pointer reset has no focused regression test. The Solidity tests execute one mutation at a time, and the benchmark now uses a smaller batch without asserting memory use. Removing the reset could therefore leave these checks passing.

Add a test that checks memory stays bounded across repeated mutations, including queued mutations and different argument sizes.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

The PR restores the free memory pointer after each mutation in execute.

  • Large mutation batches reuse memory after each mutation.
  • The harness benchmark waits for settlement and reports gas per mutation.

Acknowledgment: kyscott18 explicitly identifies the missing settlement batch limit as known and deferred. The description states that large bursts can still exceed the transaction gas cap; this review does not report that existing issue.

Reviews (1) · Last reviewed commit: "fix(contracts): free memory after each m..." · Reviewed by Greptile

execute copies the arguments of each mutation into memory, and Solidity
never frees memory. Memory grew with each mutation in a batch. With
10,000 concurrent mutations, the harness benchmark settled a batch that
went above the 8 MiB memory limit per transaction on Monad, and the
transaction halted with OutOfGas(MemoryLimit). execute now saves the free
memory pointer before each mutation and sets it back after the mutation.

The harness benchmark now sends 500 concurrent mutations, not 10,000.
The runtime settles all pending mutations in one transaction, and 10,000
Credit mutations need about 220M gas. Monad accepts a maximum of 30M gas
per transaction. The benchmark also waits until all mutations are
included before it closes, and it prints the mean settlement gas for
each mutation.

Regenerate the Token and OrderBook declarations for the new AST ids.
Comment on lines +197 to +199
uint256 freeMemoryPointer = _getFreeMemoryPointer();
_execute(batch.mutations[i], batch.mutationData[i], batch.authorizationData[i]);
_setFreeMemoryPointer(freeMemoryPointer);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Memory reuse lacks regression coverage

The new memory-pointer reset has no focused regression test. The Solidity tests execute one mutation at a time, and the benchmark now uses a smaller batch without asserting memory use. Removing the reset could therefore leave these checks passing.

Add a test that checks memory stays bounded across repeated mutations, including queued mutations and different argument sizes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/typewriter/src/Typewriter.sol
Line: 197-199

Comment:
**Memory reuse lacks regression coverage**

The new memory-pointer reset has no focused regression test. The Solidity tests execute one mutation at a time, and the benchmark now uses a smaller batch without asserting memory use. Removing the reset could therefore leave these checks passing.

Add a test that checks memory stays bounded across repeated mutations, including queued mutations and different argument sizes.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@kyscott18
kyscott18 merged commit 5818077 into main Oct 14, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant