Skip to content

feat(typewriter): reject apps whose mutations can execute differently offchain - #120

Merged
kyscott18 merged 4 commits into
mainfrom
feat/sol-determinism
Nov 2, 2026
Merged

kyscott18 merged 4 commits into
mainfrom
feat/sol-determinism

Conversation

@kyscott18

@kyscott18 kyscott18 commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #116. parseSolidity now rejects app code that can compute a different result in typewriter's offchain revm than onchain. The check runs on the solc AST, not the bytecode. Bytecode mixes in Typewriter.sol, which itself uses TIMESTAMP, CHAINID, CALLER, a STATICCALL to P256, and assembly. Separating app opcodes from those would need source maps, which depend on the optimizer. The AST also lets the error name the function and the expression.

Scope: every function, modifier, and user-defined operator reachable from the app contract's functions (constructor excluded) and each XMutation.execute. It follows calls, using … for attached calls, operators, and function references. Typewriter.sol is trusted and skipped. Library code a mutation never reaches, such as an unused assembly helper, isn't checked.

What's rejected, and why (offchain env from typewriter-evm/src/harness.rs and typewriter.ts)

Construct Why it diverges
block.* except chainid, number, timestamp; blockhash, blobhash revm runs in the last block the server saw, not the inclusion block
msg.*, tx.* offchain forceExecute replays from: app.account.address; onchain anyone calls it. Settlement batches calldata. tx.gasprice is 0 offchain
gasleft() revm runs with gas_limit(u64::MAX)
external calls, .call/.staticcall/.delegatecall/.send/.transfer, .balance, .code/.codehash, new C(), selfdestruct revm loads only the app contract's code and storage
inline assembly can't be checked (gas(), coinbase(), call …)
Typewriter's accounts, queue, executionIndex, _execute, … the runtime models account state outside revm; immutables like SCHEDULER stay allowed
function types anywhere in state the constructor isn't walked, so it could store a function that mutations then call

Errors name the function and the expression, e.g. Helpers.time uses `block.timestamp`, which can differ between offchain and onchain execution.

Behavior change vs. the old README

block.number and block.timestamp stay allowed, as before. They lag the inclusion block, so a check that reverts (e.g. a deadline) is safe: typewriter drops mutations whose settlement reverts. A branch that doesn't revert can still give different state offchain and onchain. The README says this.

msg.sender/tx.origin are now rejected. The README called them "stable", but offchain forceExecute runs as the scheduler (typewriter.ts:1060), while onchain the caller is whoever forces it.

No app (Token, OrderBook, Assessment) uses any rejected construct. All three load through loadSolidityApp (forge) unchanged.

Test plan

  • bun test src: 93 pass, 3 skip, 0 fail
  • Two new inline-snapshot tests: every rejected construct in an execute, and reachability (dispatch, transitive call, attached call, operator, library modifier, Typewriter internals, plus allowed: block.chainid, address(this), keccak256, verifySignature, SCHEDULER, unused library assembly)
  • tsc --noEmit, biome check
  • Token, OrderBook, Assessment parse via loadSolidityApp

🤖 Generated with Claude Code

kyscott18 and others added 3 commits November 2, 2026 20:35
… offchain

parseSolidity now walks every function, modifier, and operator reachable
from the app contract's functions and each XMutation.execute, and rejects
block.* (except chainid), blockhash/blobhash, msg.*/tx.*, gasleft, external
calls, account members (.call, .balance, .code, ...), contract creation,
selfdestruct, inline assembly, and use of Typewriter's own state and
functions. Typewriter.sol is trusted. The README's Determinism section
now matches what is enforced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyscott18
kyscott18 marked this pull request as ready for review November 2, 2026 21:07
A constructor could store a function in state that mutations then call,
bypassing the determinism check, which doesn't walk the constructor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyscott18
kyscott18 merged commit a937227 into main Nov 2, 2026
7 checks passed
@kyscott18
kyscott18 deleted the feat/sol-determinism branch November 2, 2026 21:15
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