Skip to content

feat(evm): Disable create and reserve balance in delegate context - #114

Merged
AshinGau merged 1 commit into
Galxe:mainfrom
AshinGau:main
Jul 24, 2026
Merged

feat(evm): Disable create and reserve balance in delegate context#114
AshinGau merged 1 commit into
Galxe:mainfrom
AshinGau:main

Conversation

@AshinGau

@AshinGau AshinGau commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR adds two independent, opt-in safety policies for EIP-7702 delegated execution:

  • Forbid delegated CREATE / CREATE2: reject contract creation only when the current execution context is an EIP-7702 delegated account. Ordinary CREATE and CREATE2 remain unchanged.
  • Reserve delegated-account balance: when delegated execution makes a surviving balance debit, prevent it from consuming funds conservatively required by later transactions from the same account.

Both policies default to false, so the existing Grevm/revm execution behavior is preserved unless they are explicitly enabled through DelegatedSafetyConfig.

Reserve-balance semantics

For a delegated account A debited by transaction Ti:

future_cost(A, i) =
    sum(tx.max_balance_spending())
    for later transactions whose caller is A

required_reserve =
    min(balance before A's first surviving delegated debit, future_cost(A, i))

The guard:

  • checks only surviving delegated debits recorded by revm's journal;
  • ignores reverted inner-frame transfers and the root transaction's ordinary tx.value;
  • covers delegated CALL value transfers and post-Cancun SELFDESTRUCT balance movement;
  • lazily builds a sender index and only computes a suffix schedule for accounts that actually make a delegated debit;
  • uses the original block TxId in both parallel execution and sequential suffix fallback.

If the final balance is below the reserve, Grevm returns a top-level REVERT. Execution state, output, and execution-generated refunds are rolled back, while the transaction nonce, EIP-7702 authorization effects/refund, and gas charged for the attempted execution are preserved. Refund caps and the EIP-7623 gas floor are recomputed from the pre-refund gas state.

Comparison with Monad

The Grevm policy follows Monad's core failure semantics but intentionally uses a narrower reserve model for Gravity Reth's skipped-invalid-transaction risk.

Aspect Grevm Monad
Reserve amount min(balance before the first surviving delegated debit, sum of later same-sender transactions' max balance spending) min(original transaction balance, 10 MON), with a sender adjustment for upfront gas
Time horizon Only transactions strictly after the current transaction in the same block Does not sum later transaction costs; uses sender/authority history from the current, parent, and grandparent blocks to decide whether the sender may dip into reserve
Protected accounts Only EIP-7702 accounts that make an actual surviving delegated-execution debit Broader EOA and delegated-account tracking, with protocol-specific sender and account exemptions
Ordinary root tx.value Explicitly excluded and left to transaction admission/filtering May be reserve-protected when the transaction sender is delegated
Balance baseline Balance immediately before the first surviving delegated debit, including credits received earlier in the same execution Original balance at the start of the transaction
Check timing Compares against the final balance after unused-gas reimbursement Checks before unused gas is reimbursed; the sender threshold accounts for upfront gas
No later same-account transaction Required reserve is zero, so the account may drain its balance The account may still need to retain up to 10 MON
Later invalid transactions Their max_balance_spending() is still included, so Grevm may conservatively over-reserve Reserve size does not depend on later transaction costs
Value movement Covers delegated calls and post-Cancun SELFDESTRUCT; reverted inner-frame debits are ignored Covers balance mutations including calls and SELFDESTRUCT; rollback hooks remove reverted-frame violations
State on violation Rolls back execution state while preserving the transaction nonce, EIP-7702 authorization effects, and charged gas Same core state-preservation behavior
Refunds on violation Discards execution-generated refunds, preserves the EIP-7702 authorization refund, and recomputes refund caps and the EIP-7623 floor Discards execution-generated refunds and adds the EIP-7702 authorization refund back after execution
Result status Standard top-level REVERT, preserving unused gas Custom EVMC_MONAD_RESERVE_BALANCE_VIOLATION; top-level contract creation consequently has different remaining-gas behavior
Implementation Post-execution scan of revm's surviving journal plus lazy deterministic per-account suffix schedules Hooks balance debit, credit, rollback, and code changes directly in the state implementation
Activation Opt-in DelegatedSafetyConfig; disabled by default Protocol rule enabled from MONAD_FOUR

Neither policy is uniformly stricter: Grevm requires no reserve when the account has no later transaction, but it can reserve more than 10 native tokens when the later maximum-spending sum is larger.

Parallel correctness and determinism

  • Reserve requirements depend only on the ordered block transaction list and the transaction's surviving journal entries.
  • Lazy schedules are immutable after initialization and produce the same value regardless of which worker initializes them.
  • Speculative balance reads continue to participate in Grevm's normal MV validation. A dedicated test forces a stale speculative read, verifies that the transaction is re-executed, and compares the complete parallel result and bundle state with forced-sequential execution.
  • With reserve protection disabled, the handler retains revm's default validation, pre-execution, execution, and post-execution lifecycle.

Refactoring

The PR also splits the previous monolithic scheduler/library implementation into focused modules:

  • scheduler context, executor, fallback, metrics, and tests;
  • runtime configuration;
  • execution models and outcomes;
  • bundle and cache-database helpers;
  • isolated delegated-safety instructions, handler, and reserve planner.

This keeps the policy-specific logic out of the core scheduling path and makes parallel and sequential execution share the same configuration and transaction handler.

Testing

  • cargo test --features test-utils — all 70 tests passed.
  • cargo test --release --features test-utils --test delegated_safety — all 13 delegated-safety tests passed.
  • cargo clippy --all-features --all-targets -- -D warnings — passed.
  • cargo +nightly fmt --all -- --check — passed.
  • GREVM_MIN_PARALLEL_TXS=0 GREVM_MAINNET_BLOCKS=test_data/spec_coverage cargo test --features test-utils --test mainnet replay_mainnet_blocks -- --nocapture — all 18 hardfork-boundary fixtures from Tangerine through Prague passed.
  • The forced stale-read/re-execution test passed 100 consecutive runs.

CI now also builds every feature and target under Clippy with warnings denied.

@Richard1048576 Richard1048576 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at d0cedf4 (base f7dacd16). Adversarial pass over the two delegated-safety policies + the scheduler refactor, focused on reserve-accounting correctness, Block-STM determinism, the delegated-CREATE ban, and refactor regressions.

Verdict: LGTM — safe to merge

No correctness, determinism, or regression defect found. The only surviving items are Minor test-coverage gaps (logic verified correct by reading, just not exercised). Opt-in and default-false, so existing behavior is unchanged unless enabled.

Verified sound

  • Reserve accounting — Overflow is saturating (max_balance_spending().unwrap_or(U256::MAX) + saturating_add). The "strictly-later, excluding self" horizon is correct (partition_point(|c| *c <= txid)). The pre-debit baseline is reconstructed purely from this tx's own journal by reverse-applying every surviving entry from the first-debit index (all three balance-affecting JournalEntry variants handled; self-transfers are no-ops). Reverted inner-frame debits are correctly ignored (drained by checkpoint_revert); post-Cancun SELFDESTRUCT (EIP-6780) is covered; the root tx.value transfer is excluded exactly once. The revert path preserves nonce / EIP-7702 auth effects+refund / charged gas while discarding execution-state refunds, and recomputes refund caps + the EIP-7623 floor.
  • Parallel determinismrequired_after / build_schedule read only the static ordered tx list and TxEnv::max_balance_spending() — no execution results, no MV/DB reads — so the suffix sums are a pure deterministic function, and OnceLock/DashMap init is worker-independent. The whole reserve decision is a pure function of (this tx's own journal + the static tx list); it reads no unregistered speculative state, so Block-STM re-execution recomputes it consistently. The candidate set is OR-reduced into one boolean, so its ahash iteration order is irrelevant.
  • Delegated CREATE/CREATE2 ban — no issues.
  • Scheduler/lib refactor — no behavior regression; the split is a faithful move. (concurrency_level == 0 now asserts instead of hanging — an improvement; from_env re-reading per construction is cosmetic.)

Minor — test-coverage gaps (non-blocking)

The following behaviors are implemented correctly but have no test exercising them:

  1. Execution-refund discard on a reserve revert — every reserve-path delegate uses CALL+SELFBALANCE or SELFDESTRUCT, neither of which earns an EIP-3529 refund on PRAGUE, so set_refund(0) is a no-op in all current tests. Add a delegate that does an SSTORE nonzero→zero clear before the debit, so the discarded execution refund actually matters.
  2. EIP-7623 calldata floor binding on the revert path — no reserve-reverting tx makes the floor the binding gas. Add a type-4 delegated tx with several KB of calldata whose floor dominates.
  3. Stale-read determinism, other directionreserve_retry_reexecutes_after_a_stale_speculative_balance_read only covers violation→success. Add the mirror (a stale low read that first passes, then re-executes into a violation).
  4. Multi-candidate reserve — no test has a single tx produce surviving debits from two distinct delegated accounts; the per-candidate loop in has_reserve_violation is unexercised end-to-end.

Considered and dismissed

  • "A later same-sender tx that will be skipped/invalid still inflates the reserve." Real behavior, but intentional and documented conservative over-reservation (only ever causes more reverts, never under-protection), and opt-in. Not a defect — at most a test-hardening opportunity.

@Richard1048576 Richard1048576 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@AshinGau
AshinGau merged commit 2f5ffee into Galxe:main Jul 24, 2026
2 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.

2 participants