Claude session c2c0decc-a219-4586-8fdf-1f51d55dc513

Included: messages plus tool calls, UI actions, task notifications

Started: 2026-06-14 23:29:31 EDT

Last Event: 2026-06-14 23:49:50 EDT

Timeline Begins: 2026-06-14 23:29:43 EDT

Timeline Ends: 2026-06-14 23:49:50 EDT

UI Action: Effort: xhigh (saved as your default for new sessions): Deeper reasoning than high, just below maximum (Fable 5, Opus 4.7+)
UI Action

Effort: xhigh (saved as your default for new sessions): Deeper reasoning than high, just below maximum (Fable 5, Opus 4.7+)

User
/home/abhishek/fv-workspace-inner#1

read reservebal.v . the execution check is implemented in monad/, in C++, particularly reserve_balance.cpp (dipped_into_reserve). there is spec of the dipped_into_reserve function at the end of exec_specs.v read it and do a proof sketch of dipped_into_reserve. don't do it in Coq yet, but do it in sufficient detail to later fomalize in Coq. it should be very detailed and precise: no handwaving

  • C++ is slightly less permissive (reverts more transactions) than the model in reservebal.v (isSC check is more permissive). this is known and not a safety issue.
  • ignore the MONAD_ASSERT in the code and assume that the staking contract's balance never goes below 10MON.
Agent
#2

I'll start by locating and reading the relevant files. Let me find them in parallel.

1 tool call
Tool

Bash: find /home/abhishek/fv-workspace-inner -name "reservebal.v" -o -name "exec_specs.v" -o -name "res...

{
  "command": "find /home/abhishek/fv-workspace-inner -name \"reservebal.v\" -o -name \"exec_specs.v\" -o -name \"reserve_balance.cpp\" 2>/dev/null",
  "description": "Locate the relevant source files"
}
Agent
#3

Since this is Rocq/Coq proof work for C++ code, let me invoke the rocqcpp skill, and read the key files.

1 tool call
Tool

Skill: skill="rocqcpp"

{
  "skill": "rocqcpp"
}
Agent
#4

Now let me read the three key files. Let me start with reservebal.v and the C++ implementation.

1 tool call
Tool

Read: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/reservebal.v

{
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/reservebal.v"
}
Agent
#5

Let me read the C++ implementation and the spec at the end of exec_specs.v.

1 tool call
Tool

Read: /home/abhishek/fv-workspace-inner/monad/category/execution/monad/reserve_balance.cpp

{
  "file_path": "/home/abhishek/fv-workspace-inner/monad/category/execution/monad/reserve_balance.cpp"
}
Agent
#6

Now let me find the dipped_into_reserve spec at the end of exec_specs.v.

1 tool call
Tool

Bash: grep -n "dipped_into_reserve\|dipped\|reserve" /home/abhishek/fv-workspace-inner/monad/monadproof...

{
  "command": "grep -n \"dipped_into_reserve\\|dipped\\|reserve\" /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v | head -80; echo \"---TOTAL LINES---\"; wc -l /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v",
  "description": "Find dipped_into_reserve spec location"
}
Agent
#7

The main spec is at line 3975. Let me read the model definitions and the spec.

1 tool call
Tool

Read: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v

{
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v",
  "offset": 3160,
  "limit": 320
}
Agent
#8

Let me read the main dipped_into_reserve_spec and surrounding helper specs.

1 tool call
Tool

Read: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v

{
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v",
  "offset": 3808,
  "limit": 243
}
Agent
#9

The spec is against reservebalold (imported at line 19), while the task references reservebal.v. Let me examine the actual model the spec uses, plus the key helper definitions. Let me launch parallel探索 to gather the model definitions precisely.

5 tool calls
Tool

Bash: cd /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs echo "===== reservebalold.v locatio...

{
  "command": "cd /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs\necho \"===== reservebalold.v location & finalBalSufficient/allFinalBalSufficient =====\"\ngrep -rn \"Definition finalBalSufficient\\|Definition allFinalBalSufficient\\|Definition isSC\\|Definition isAcSC\\|Definition isAllowedToEmpty\\b\\|Definition isAllowedToEmptyExec\\|StakingContractAddr\" reservebalold.v 2>/dev/null\necho\necho \"===== find reservebalold.v =====\"\nfind /home/abhishek/fv-workspace-inner/monad -name \"reservebalold.v\" 2>/dev/null | grep -v _build",
  "description": "Find reservebalold model definitions"
}
Tool

Bash: cd /home/abhishek/fv-workspace-inner/monad echo "===== files named reservebal* =====" find . -pat...

{
  "command": "cd /home/abhishek/fv-workspace-inner/monad\necho \"===== files named reservebal* =====\"\nfind . -path ./_build -prune -o -name \"reservebal*\" -print 2>/dev/null\necho\necho \"===== grep for 'reservebalold' as a module / dune name =====\"\ngrep -rn \"reservebalold\" --include=\"*.v\" --include=\"dune\" . 2>/dev/null | grep -v _build | head -20\necho\necho \"===== where is Definition finalBalSufficient defined across repo =====\"\ngrep -rln \"Definition finalBalSufficient\" --include=\"*.v\" . 2>/dev/null | grep -v _build",
  "description": "Locate reservebalold source"
}
Tool

Bash: cd /home/abhishek/fv-workspace-inner echo "===== any reservebalold in _build or anywhere =====" f...

{
  "command": "cd /home/abhishek/fv-workspace-inner\necho \"===== any reservebalold in _build or anywhere =====\"\nfind . -name \"reservebalold*\" 2>/dev/null | head\necho\necho \"===== check dune/coq mapping for proofs dir =====\"\nfind monad/monadproofs/proofs -name \"dune\" 2>/dev/null | grep -v _build | head\necho \"--- contents ---\"\ncat monad/monadproofs/proofs/dune 2>/dev/null | head -60",
  "description": "Check dune and reservebalold mapping"
}
Tool

Bash: cd /home/abhishek/fv-workspace-inner echo "===== _CoqProject files =====" find . -name "_CoqProje...

{
  "command": "cd /home/abhishek/fv-workspace-inner\necho \"===== _CoqProject files =====\"\nfind . -name \"_CoqProject\" 2>/dev/null | grep -v _build | head\necho\necho \"===== search for 'reservebal' bindings in any _CoqProject / dune / Makefile =====\"\ngrep -rn \"reservebal\" --include=\"_CoqProject\" --include=\"dune\" --include=\"dune-project\" --include=\"Makefile*\" . 2>/dev/null | grep -v _build | head\necho\necho \"===== dune files under monadproofs =====\"\nfind monad/monadproofs -name \"dune\" 2>/dev/null | grep -v _build",
  "description": "Find Coq project mapping config"
}
Tool

Bash: cd /home/abhishek/fv-workspace-inner echo "===== root _CoqProject (mapping for monad.proofs) ====...

{
  "command": "cd /home/abhishek/fv-workspace-inner\necho \"===== root _CoqProject (mapping for monad.proofs) =====\"\ngrep -n \"monad.proofs\\|monadproofs\\|proofs\" _CoqProject 2>/dev/null | head\necho\necho \"===== monadproofs/dune =====\"\ncat monad/monadproofs/dune\necho\necho \"===== how is reservebal.v module-named? grep theories mapping =====\"\ngrep -rn \"proofs\" _CoqProject | head",
  "description": "Inspect Coq logical path mapping"
}
Agent
#10

reservebalold.v doesn't exist as a file — finalBalSufficient/allFinalBalSufficient live only in reservebal.v (which maps to monad.proofs.reservebal). So the spec's Require Import monad.proofs.reservebalold is stale; the model definitions it uses are from reservebal.v. Let me confirm the signature and gather the abstraction machinery the spec relies on.

1 tool call
Tool

Bash: cd /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs echo "### Definitions referenced by...

{
  "command": "cd /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs\necho \"### Definitions referenced by the dipped_into_reserve spec — locate each ###\"\nfor sym in \"StateM\" \"Definition StateR\" \"satisfiesAssumptions\" \"Definition applyUpdates\" \"original_balance_pessimistic_model_map\" \"original_balance_pessimistic_model_assumed\" \"mapModelLookup\" \"AssumedPreTxAccountState\" \"AssumptionExactness\" \"check_min_balance_ok\" \"min_balance_update \" \"update_assum_exactness_state\" \"update_assum_exactness_at\" \"updates_stricter\" \"historyConsistent\" \"Definition DefReserve\" \"Definition validTx\" \"preTxAssumedState\" \"Definition newStates\" \"Record StateM\\|StateM :=\" \"Definition StateOriginalR\" \"preTxState \" \"postTxState \" \"applyUpdates\"; do\n  echo \"----- $sym -----\"\n  grep -rn \"$sym\" --include=\"*.v\" . 2>/dev/null | grep -v _build | grep -iE \"Definition|Record|Inductive|Fixpoint|Notation|Class\" | head -4\ndone",
  "description": "Locate abstraction-machinery definitions"
}
Agent
#11

All the machinery is in exec_specs.v. Let me read the key definition regions in parallel.

11 tool calls
Tool

Read: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v

{
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v",
  "offset": 80,
  "limit": 90
}
Tool

Read: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v

{
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v",
  "offset": 585,
  "limit": 70
}
Tool

Bash: cd /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs echo "===== MonadChainContext recor...

{
  "command": "cd /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs\necho \"===== MonadChainContext record + cblock/cblock txsWithHdr + wei_per_mon =====\"\ngrep -n \"Record MonadChainContext\\|Definition cblock\\|txsWithHdr\\|currentBlock\\|Definition blocks\\|wei_per_mon\\|Definition transactions\\|Record.*ChainContext\\|cblock\\b\\|Definition senders\\|txAuthoritiesDelFrom\\|Definition can_sender\\|sender_seen_in_current_prefix\" exec_specs.v | head -40",
  "description": "Locate chain-context model pieces"
}
Tool

Read: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v

{
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v",
  "offset": 655,
  "limit": 260
}
Tool

Read: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v

{
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v",
  "offset": 915,
  "limit": 45
}
Tool

Read: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v

{
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v",
  "offset": 1470,
  "limit": 110
}
Tool

Read: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v

{
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v",
  "offset": 1640,
  "limit": 90
}
Tool

Read: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v

{
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v",
  "offset": 2120,
  "limit": 160
}
Tool

Read: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v

{
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v",
  "offset": 2640,
  "limit": 130
}
Tool

Read: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v

{
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v",
  "offset": 3480,
  "limit": 120
}
Tool

Read: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v

{
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/exec_specs.v",
  "offset": 3040,
  "limit": 120
}
User
#12

yes use reservebal.v instead of reservebalold.v

Agent
#13

I found a reference to execproofs/reservebal/reserve_balance.v (the "copied from" comment). That may be the actual model the spec was written against. Let me check, and verify the loop/iteration mechanics (recent(), state.current()).

1 tool call
Tool

Bash: cd /home/abhishek/fv-workspace-inner/monad echo "===== does execproofs/reservebal/reserve_balance...

{
  "command": "cd /home/abhishek/fv-workspace-inner/monad\necho \"===== does execproofs/reservebal/reserve_balance.v exist? =====\"\nfind . -path ./_build -prune -o -name \"reserve_balance.v\" -print 2>/dev/null\necho\necho \"===== any OTHER definition of finalBalSufficient / allFinalBalSufficient (e.g. 5-arg) =====\"\ngrep -rn \"finalBalSufficient\" --include=\"*.v\" . 2>/dev/null | grep -v _build | grep -v \"exec_specs.v\" | head\necho\necho \"===== git log for reservebal.v / exec_specs.v (is this a git repo?) =====\"\ngit -C /home/abhishek/fv-workspace-inner log --oneline -5 -- monad/monadproofs/proofs/reservebal.v 2>&1 | head\necho \"--- git status of reservebalold ref ---\"\ngit -C /home/abhishek/fv-workspace-inner log --oneline --all -- \"*reservebalold*\" 2>&1 | head",
  "description": "Check for old model file and git history"
}
Agent
#14

The user confirmed: use reservebal.v. Let me do a few final precision checks (the finalBalSufficient lemma in reservebal.v, and the recent()/is_delegated/balance-application mechanics) before writing the sketch.

2 tool calls
Tool

Read: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/reservebal.v

{
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/reservebal.v",
  "offset": 2218,
  "limit": 55
}
Tool

Bash: cd /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs echo "===== recent() / VersionStack...

{
  "command": "cd /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs\necho \"===== recent() / VersionStack recent spec =====\"\ngrep -n \"recent\\|VersionStackR\\|\\.account_\\|account_\\\"\\|read_code\\|RecentR\\|is_delegated\\|balanceOfAccount\" exec_specs.v | head -30\necho\necho \"===== AccountM / balance / coreAc / balanceOfAccount def =====\"\ngrep -rn \"Definition balanceOfAccount\\|Definition balance \\|_balance\\b\\|Definition balanceOfAc\\b\" exec_specs.v reservebal.v evmmisc.v 2>/dev/null | grep -v _build | head",
  "description": "Check recent/balance/is_delegated mechanics"
}
Agent
#15

I now have a complete picture. Let me write up the detailed proof sketch as a document, then summarize.

1 tool call
Tool

Write: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/dipped_into_reserve_proof_sketch.md

{
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/dipped_into_reserve_proof_sketch.md",
  "content": "# Proof sketch: `dipped_into_reserve` ⊨ `dipped_into_reserve_spec`\n\nTarget C++: `monad/category/execution/monad/reserve_balance.cpp`, function\n`dipped_into_reserve` (lines 41–109).\n\nTarget spec: `exec_specs.v`, `dipped_into_reserve_spec` (lines 3975–4026).\n\nModel: `reservebal.v` (`finalBalSufficient` 551–560, `allFinalBalSufficient`\n562–564).  Per the user, the model of record is `reservebal.v` (the\n`Require Import … reservebalold` at `exec_specs.v:19` is stale and should be\nre-pointed at `monad.proofs.reservebal`).\n\nThis is a sketch only — Coq-level statements are given, but routine separation\n-logic plumbing (loadSpec/`go`, type-ptr observations, `wp_for` boilerplate)\nis described rather than written.\n\n--------------------------------------------------------------------------------\n## 0. Standing assumptions and known gaps (read first)\n\nThese are stated up front because they change what is provable.\n\n**A0 (asserts assumed).** Every `MONAD_ASSERT(cond)` is given the\n`monad_assertion_failed_spec` whose precondition is `[| False |]`\n(`exec_specs.v:3103`). Operationally this means: at each assert site the proof\n*may assume* `cond` holds (the failure branch is dead). We do not prove the\nasserts independently. Two asserts matter:\n  - line 47–49: index/size well-formedness of `ctx` — assumed.\n  - line 94–97: `violation_threshold.has_value()` in the sender / cannot-dip\n    branch, i.e. `gas_fees ≤ reserve`. This is **load-bearing** (see §5, Step D,\n    sender case). Without it the truncated `N`-subtraction in the model would\n    not line up with the `optional` threshold in C++.\n\n**A1 (staking balance).** The staking contract `0x1000`\n(`StakingContractAddr = 4096`, `reservebal.v:546`) is assumed to always hold\n`balance ≥ 10 MON = DefReserve` (`exec_specs.v:803`,\n`DefReserve = 10 * 10^18`). This discharges the `a = StakingContractAddr`\ndisjunct of `finalBalSufficient` (see §5, Staking sub-case).\n\n**A2 (isSC discrepancy — known, not a safety issue).** The C++ decides\n\"this account is a contract, skip it\" from the **original/pre-tx** code hash\n(`orig_code_hash`, lines 57–69), whereas `reservebal.v`'s `finalBalSufficient`\nuses `isSC postTxState a` — the **post-tx** state. These do not coincide; see\n§5 Step A. The exact equality in the post-condition therefore fails on exactly\nthe accounts where pre-state and post-state contract-ness differ. This is the\ndocumented, accepted gap; the safety-relevant direction still holds (§7).\n\n**A3 (`nonDelegatedCodeExecAccounts` argument).** `reservebal.v`'s\n`finalBalSufficient`/`allFinalBalSufficient` take a\n`nonDelegatedCodeExecAccounts : list EvmAddr` argument that the 5-argument call\nin the current spec text omits. `dipped_into_reserve` has no \"executed code\"\nset of its own (it only does the original-code-hash skip), so when the spec is\nre-pointed at `reservebal.v` this argument is instantiated to `[]`. The skip it\nwould have provided is folded into the isSC discussion of A2/§5 Step A.\n\n--------------------------------------------------------------------------------\n## 1. The goal, restated precisely\n\n`dipped_into_reserve_spec` (`exec_specs.v:3975`) reduces, after consuming the\narg/prepost frame, to the Hoare triple \"running the body from `StateR st`\nreturns `Vbool retb` and leaves\":\n\n```\nExists (updates: gmap evm.address AssumptionExactness),\n  let stf := update_assum_exactness_state st updates in\n  statep |-> StateR stf\n  ** [| updates_stricter st updates |]\n  ** [| forall preTxState : StateOfAccounts,\n          satisfiesAssumptions stf preTxState ->\n          retb = negb (allFinalBalSufficient 3 (preTxState, hist)\n                         (applyUpdates stf preTxState)\n                         (map fst (newStates st)) (* []  -- A3 *) tx) |]\n```\n\nWrite throughout:\n  - `chg := map fst (newStates st)` (the changed-account keys; this is exactly\n    what `state.current()` iterates, by `StateR` `exec_specs.v:1660` →\n    `MapCurrentR (newStates st)`),\n  - `R_pess(a) := N.min DefReserve (original_balance_pessimistic_model_map\n                   (preTxAssumedState st…) a)` (the C++ `reserve`),\n  - `fee := maxTxFee tx`  (= C++ `gas_fees`; equal by §3 / `validTx`),\n  - for a concrete `preTxState`:\n      - `R(a)   := N.min DefReserve (balanceOfAc preTxState.1 a)`  (model `erb`),\n      - `cur(a) := balanceOfAc (applyUpdates stf preTxState) a`    (model post bal),\n      - `spec(a):= current_balance_pessimistic_model_stack (stack of a)` — the\n        speculative recent balance the C++ reads via `stack.recent().account_`.\n\nBecause `allFinalBalSufficient = forallb (finalBalSufficient …) chg`\n(`reservebal.v:564`) and the C++ returns on the **first** dipping account, the\nwhole proof is the conjunction of:\n\n  - **(Reduction)** `retb = true  ⟺  ∃ a ∈ chg. ¬finalBalSufficient(a)` and\n    `retb = false ⟺ ∀ a ∈ chg. finalBalSufficient(a)`, for every\n    `preTxState ⊨ satisfiesAssumptions stf`; and\n  - **(Per-account)** for each processed `a`, a bidirectional match between the\n    C++ decision `dippedAt(a)` and `¬finalBalSufficient(a)` (§5).\n\nNote (early return is sound): if C++ returns `true` at account `a_k`, `updates`\nonly records the tightenings for `a_1..a_k`. That suffices: we exhibit\n`¬finalBalSufficient(a_k)` for **all** `preTxState ⊨ stf`, which already makes\n`forallb … = false`, independent of `a_{k+1}…`.\n\n--------------------------------------------------------------------------------\n## 2. Fee equality (`gas_fees = maxTxFee tx`)\n\nC++ (line 51): `gas_fees = uint256{tx.gas_limit} * gas_price(rev, tx, base_fee)`.\nSpec hypothesis `validTx tx` (`exec_specs.v:924`) gives\n`maxTxFee tx = tx_gas_limit tx.1.1 * gas_price_cpp_model tx.1 (tx_base_fee …)`\nwith `gas_limit < 2^64`, `gas_price < 2^128`, hence the product `< 2^192 < 2^256`\n— no `uint256_t` overflow. The `gas_price` callee spec returns\n`gas_price_cpp_model …`. ⇒ **Lemma F:** `gas_fees = maxTxFee tx` as `N`, and the\nmultiplication never wraps. (Local obligation: thread `validTx`'s bounds through\nthe `uint256_t::operator*` spec.)\n\n--------------------------------------------------------------------------------\n## 3. The two reconciliation lemmas (the heart of the proof)\n\nThe C++ never sees the concrete `preTxState`. It computes with the **pessimistic\noriginal balance** and the **speculative current balance**, and *records\nassumptions* so that, on every `preTxState` consistent with them, its decision\nequals the model's. The post-condition's `∀ preTxState ⊨ stf` quantifier is\ndischarged entirely by these two lemmas.\n\n### Lemma 1 (original reserve is pinned: `R_pess(a) = R(a)`)\n\nThe threshold lambda (C++ lines 72–85) is specified by\n`dipped_into_reserve_lam_call_spec` (`exec_specs.v:3274–3293`):\n  - it returns `reserve_violation_threshold_model orig a sender gas_fees`\n    (`exec_specs.v:3200–3208`), and\n  - it updates the original map to\n    `check_min_original_balance_update orig a DefReserve`\n    (`exec_specs.v:3211–3217`), i.e.\n    `update_assum_exactness_at a (fun ex => min_balance_update ex orig_bal\n       orig_bal DefReserve) orig`, with `orig_bal := R_pess`'s pre-min argument\n    `original_balance_pessimistic_model_map orig a`.\n\nReading off `min_balance_update old orig cur debit` (`exec_specs.v:117–138`)\nwith `cur = orig = orig_bal`, `debit = DefReserve`:\n\n  - if `DefReserve ≤ orig_bal` (`check_min_balance_ok`): the new `min_balance`\n    is `max(old_min, orig_bal - (orig_bal - DefReserve)) = max(old_min,\n    DefReserve)`, i.e. the assumption now forces `actual_orig_bal ≥ DefReserve`.\n  - if `DefReserve > orig_bal`: `min_balance := None` — switch to **exact**\n    balance validation, forcing `actual_orig_bal = orig_bal`.\n\nNow take any `preTxState ⊨ satisfiesAssumptions stf`\n(`exec_specs.v:2688–2693`), which unfolds at `a` to\n`satAccountNonStorageAssumptions … (assumptionOfAddr (preTxAssumedState stf) a)\n (Some (preTxState.1 a))` (`2644`). In both regimes:\n\n  - `DefReserve ≤ orig_bal` ⇒ `min_balance = Some(≥DefReserve)` ⇒\n    `balanceOfAc preTxState.1 a ≥ DefReserve` ⇒\n    `R(a) = min(DefReserve, balanceOfAc…) = DefReserve = min(DefReserve,\n     orig_bal) = R_pess(a)`.\n  - `DefReserve > orig_bal` ⇒ exact ⇒ `balanceOfAc preTxState.1 a = orig_bal`\n    ⇒ `R(a) = min(DefReserve, orig_bal) = orig_bal = R_pess(a)`.\n\n⇒ **Lemma 1:** for every `preTxState ⊨ stf`, `R(a) = R_pess(a)`, and likewise\nthe model `fee > R(a)` ⟺ C++ `gas_fees > reserve`. In particular\n`reserve_violation_threshold_model` (which is `None` iff `fee > R_pess(a)` in\nthe sender case, else `R_pess(a) - fee`; else `R_pess(a)`) computes the **model**\nthreshold exactly. (This is also why A0/A2's `gas_fees ≤ reserve` assert ties to\nthe model's `N`-truncated `R(a) - fee`.)\n\nCaveat to formalize: `original_balance_pessimistic_model_map` is evaluated on\nthe *running* `orig` (which grows as `state.original_account_state` materializes\nabsent accounts — `state_original_account_state_post`, `exec_specs.v:1502`), not\nthe literal initial `preTxAssumedState st`. The loop invariant (§4) must carry\n\"the `orig` threaded through equals `preTxAssumedState` of the current `stf`\nrestricted to processed accounts\", so that the final `updates` satisfies\n`updates_stricter st updates` (`exec_specs.v:3495`) — each per-account\n`min_balance_update` is an `assumption_exactness_stricter` step\n(`min_balance_stricter`, `3482`; tightening `Some m → Some(max m _)` or\n`Some _ → None`).\n\n### Lemma 2 (current balance transfers: `spec(a) ⋚ θ ⟺ cur(a) ⋚ θ`)\n\nThe C++ reads `curr_balance = stack.recent().account_.balance` (lines 86–88) =\n`spec(a)` (`current_balance_pessimistic_model_stack`, `exec_specs.v:2184`;\n`recentAccountOf_stack`, `2178`). The model uses\n`cur(a) = balanceOfAc (applyUpdates stf preTxState) a`.\n\nUnfold `applyUpdates`/`accountFinalVal` (`exec_specs.v:2717–2754`) at `a` (which\n*has* updates, since `a ∈ chg`), with `csUpdated := recent post-tx account`\n(balance `spec(a)`), `csAssumed := preTxState assumed` (balance the assumed pre\n= `orig_bal`), `csActual := preTxState.1 a` (balance `balanceOfAc preTxState.1 a`):\n\n  - **exact regime** (`relaxedValidation = false`, or `min_balance = None`):\n    `accountFinalVal` returns `base`, whose balance is `spec(a)` directly ⇒\n    `cur(a) = spec(a)`. The comparison transfers trivially.\n  - **relaxed regime** (`min_balance = Some m`): balance becomes\n    `postTxActualBalNonce` (`exec_specs.v:2698`):\n    `cur(a) = balanceOfAc preTxState.1 a + (spec(a) - orig_bal)`\n    (`Z`/`N` care: this is the speculative *delta* re-based on the actual pre).\n    By Lemma 1, on `preTxState ⊨ stf` we have `balanceOfAc preTxState.1 a` either\n    `= orig_bal` (exact-orig regime) — then `cur(a) = spec(a)` — or\n    `≥ DefReserve ≥ R(a) = R_pess(a)` with `min_balance ≥ DefReserve`. In the\n    latter regime the recorded `min_balanceN` (a lower bound the subtraction-time\n    `min_balance_update` maintains, `exec_specs.v:2139–2154`,\n    `state_subtract_from_balance_post`) guarantees `cur(a) ≥` the same threshold\n    that the comparison tests, so `spec(a) < θ ⟺ cur(a) < θ` for the relevant\n    `θ ∈ {R(a), R(a) - fee}`.\n\n⇒ **Lemma 2:** for every `preTxState ⊨ stf` and `θ ∈ {R(a), R(a) − fee}`,\n`spec(a) < θ ⟺ cur(a) < θ`, and `θ ≤ spec(a) ⟺ θ ≤ cur(a)`.\n\nCaveat to formalize: this is the most delicate arithmetic step; the `Z↔N`\nre-basing in `postTxActualBalNonce` and the exact bookkeeping of `min_balanceN`\nacross the *whole* speculative tx (not just one subtraction) is where the bulk\nof the eventual Coq effort sits. The statement above is what must be proven; the\ninvariant of `state_subtract_from_balance_post` / `sliceInvariants`\n(`exec_specs.v:1565`) is the tool.\n\n--------------------------------------------------------------------------------\n## 4. Loop set-up and invariant\n\nThe body is `for (auto const &[addr, stack] : state.current()) { … }`\n(lines 54–107). `state.current()` is the `current_` field, modelled by\n`MapCurrentR (newStates st)` inside `StateR` (`exec_specs.v:1660`). Use the\nrepository's `wp_for` over the ankerl map spine (the iteration enumerates the\nkeys `chg = map fst (newStates st)` in some order; order is irrelevant since the\ntarget is `forallb`/`∃`).\n\n**Loop invariant** (over the processed prefix `P ⊆ chg`, with current original\nmap `origP` and accumulated `updatesP`):\n\n1. **(resources)** `statep |-> StateR (state_with_preTxAssumedState st origP)`\n   with the `current_`/`block_state_`/code fields unchanged; `ctxp`, `senderp`,\n   `txp`, `basefeep`, `NULL_HASH` prepost frames returned intact.\n2. **(assumptions tightened)** `origP = (preTxAssumedState st)` updated by\n   `updatesP` on the keys in `P`, and `updates_stricter st updatesP`.\n3. **(no-dip-yet flag)** a ghost boolean `found`:\n   - `found = false` ⇒ for all `a ∈ P` and all `preTxState ⊨\n     state_with_preTxAssumedState st origP`, `finalBalSufficient(a) = true`;\n   - `found = true`  ⇒ the function has already `return`ed `true` and there is a\n     witness `a_k ∈ P` with `¬finalBalSufficient(a_k)` for all such `preTxState`.\n4. **(unprocessed untouched)** for `a ∉ P`, `origP` agrees with the initial map,\n   so processing them later still starts from the right assumptions.\n\nAt loop entry `P = ∅`, `updatesP = ∅` (`updates_stricter_emp`, `exec_specs.v:3510`),\n`found = false`. At loop exit (no early return) `P = chg`, `found = false`, giving\n`∀ a ∈ chg. finalBalSufficient(a)` ⇒ `allFinalBalSufficient = true` ⇒\n`retb = false = negb true`. Early return sets `found = true` and `retb = true`;\ninvariant clause 3 supplies the `∃`-witness ⇒ `allFinalBalSufficient = false` ⇒\n`negb false = true = retb`. Either way the final `updates := updatesP` satisfies\nthe post-condition with `stf := update_assum_exactness_state st updates`.\n\n--------------------------------------------------------------------------------\n## 5. One iteration: C++ branch ⟷ `finalBalSufficient` (the core case split)\n\nFix the iteration's account `a` with version stack `stack`. Let\n`fbs(a) := finalBalSufficient (preTxState, hist) (applyUpdates stf preTxState)\n            [] tx a` (model, `reservebal.v:551`); recall `hist` satisfies\n`configuredReserveBal (hist a) = DefReserve` (spec hyp, `exec_specs.v:3988`), so\n`configuredReserveBalOfAddr hist a = DefReserve` and the model's `ReserveBal` is\n`DefReserve`, hence the model `erb = R(a)`.\n\n### Step A — the EOA/contract skip (lines 57–69) ⟷ `isSC` disjunct\n\nC++: `orig_code_hash = NULL_HASH` unless the *original* account has code; if it\nhas code and `is_delegated(code)` is false → `continue` (skip `a`).\nUsing `reserve_balance_bytes32_eq_spec` (`3043`), `intercode_size_spec` (`3119`),\nthe `is_delegated` spec, and `isDelegationMarker` (`reservebal.v:235`:\n`length = 23 ∧ marker_prefix`), the skip predicate is exactly\n\n```\nskip_cpp(a)  ⟺  isAcSC (orig_account a)            (reservebal.v:240)\n             ⟺  isSC PRE_state a   (PRE = original/pre-tx)\n```\n\nThe model's first disjunct is `isSC postTxState a || a ∈ [] || a = staking`\n(A3 sets the middle list to `[]`).\n\n**This is the A2 gap.** `skip_cpp` uses **PRE**, the model uses **POST**:\n  - EOA-in-pre / contract-in-post (e.g. `reservebal.v:586–591`: tx deploys+empties\n    a fresh `addr2`): C++ does *not* skip (checks balance, may report dip);\n    model *does* skip (`fbs = true`). C++ stricter.\n  - contract-in-pre / EOA-in-post (selfdestruct): C++ skips; model checks.\n\nOn all accounts where pre- and post-contract-ness agree (the overwhelming\nmajority — code is created/destroyed only by the executing tx), `skip_cpp(a) ⟺\nisSC postTxState a`, and the iteration matches exactly. On the disagreement set,\nthe equality is replaced by the safe one-sided bound of §7. We proceed assuming\nagreement (A2) for the exact-equality argument; if `skip_cpp(a)` (and `a` skipped)\nthe iteration contributes `fbs(a) = true` and `found` is unchanged.\n\nWhen **not** skipped (`a` is an EOA or a delegated EOA in PRE), continue to B–D.\n\n### Step B — the threshold lambda (lines 72–85) ⟷ `R(a)`, `fee`\n\nBy the lambda call spec + Lemma 1, the returned `optional<uint256> θ_cpp` equals\n`reserve_violation_threshold_model orig a sender gas_fees`, and the original map\ngains the `min_balance_update … DefReserve` tightening (this is the per-account\ncontribution to `updatesP`; clause 2 of the invariant). Concretely, with\n`R := R(a) = R_pess(a)` and `fee = maxTxFee tx`:\n  - `a = sender`: `θ_cpp = None` iff `fee > R`, else `Some (R − fee)`;\n  - `a ≠ sender`: `θ_cpp = Some R`.\n\n### Step C — read the current balance (lines 86–88) ⟷ `cur(a)`\n\n`curr_balance = spec(a)`; by **Lemma 2** every comparison below transfers to\n`cur(a) = balanceOfAc (applyUpdates stf preTxState) a`.\n\n### Step D — the violation test (lines 89–106) ⟷ sender/non-sender branches\n\nC++ flags a dip exactly when `(θ_cpp = None ∨ curr_balance < θ_cpp)` and then,\n\n**Non-sender (`a ≠ sender`, lines 102–105).** `θ_cpp = Some R` (never `None`),\nso dip ⟺ `spec(a) < R`. The `MONAD_ASSERT(θ.has_value())` is trivially true.\nModel branch (`reservebal.v:560`, `a ≠ sender`, not skipped, not staking):\n`fbs(a) = asbool (R ≤ cur(a))`, so `¬fbs(a) ⟺ cur(a) < R`. With Lemma 2:\n\n```\ndippedAt(a)  ⟺  spec(a) < R  ⟺  cur(a) < R  ⟺  ¬fbs(a).      ✓ (exact)\n```\n\n**Sender (`a = sender`, lines 91–101).** Reached the inner test means\n`θ_cpp = None ∨ curr_balance < θ_cpp`.\n  - `can_sender_dip_into_reserve(...)` is specified by\n    `can_sender_dip_into_reserve_spec` (`exec_specs.v:3167–3197`) to equal\n    `isAllowedToEmpty 3 (preTxState, hist) [] tx` =\n    `isAllowedToEmptyExec (preTxState, hist) tx` (`reservebal.v:457`). Its\n    `delegated`/history hypotheses are met by the spec's `historyConsistent`\n    hyp and the delegation hypothesis at `3991–4013` (the post-state\n    delegation-marker bookkeeping). Call this boolean `dip_ok`.\n  - If `dip_ok = true`: C++ does **not** report a dip (lines 100, \"skip\"); model\n    `fbs(a) = true` (`reservebal.v:559`, `isAllowedToEmptyExec` true branch).\n    Match: `¬dippedAt(a)` and `fbs(a)`. ✓\n  - If `dip_ok = false`: C++ hits the `MONAD_ASSERT(θ.has_value())` (line 94).\n    By **A0** we assume `θ_cpp = Some (R − fee)` (i.e. `fee ≤ R`). Then C++\n    reports dip ⟺ `curr_balance < R − fee` ⟺ (Lemma 2) `cur(a) < R − fee`.\n    Model (`reservebal.v:559`, `dip_ok` false branch): `fbs(a) =\n    asbool ((R − fee) ≤ cur(a))` with **`N`-truncated** subtraction. Because\n    `fee ≤ R` (A0), the truncation is inert (`R − fee` is the true difference),\n    so `¬fbs(a) ⟺ cur(a) < R − fee`. Hence\n\n```\ndippedAt(a)  ⟺  cur(a) < R − fee  ⟺  ¬fbs(a).               ✓ (modulo A0)\n```\n\n  (Without A0, the `θ_cpp = None` path would let C++ report a dip while the\n  model's truncated `R − fee = 0 ≤ cur(a)` says `fbs = true`; A0 declares that\n  path unreachable.)\n\n### Staking sub-case (`a = StakingContractAddr`)\n\nThe C++ has **no** special case for `0x1000`. Two ways it is handled:\n  - `0x1000` is a genuine contract in PRE ⇒ Step A skips it (`skip_cpp`),\n    matching the model's `a = staking` disjunct (`fbs = true`). This is the\n    normal case.\n  - Even if reached: by **A1**, `spec(0x1000) ≥ DefReserve ≥ R`, so the\n    non-sender test `spec(a) < R` is false ⇒ `¬dippedAt`, and the model's\n    `a = staking` disjunct gives `fbs = true`. Consistent.\n\n--------------------------------------------------------------------------------\n## 6. Assembling the post-condition\n\nRun the loop with the §4 invariant; each iteration is §5. On exit set\n`updates := updatesP`, `stf := update_assum_exactness_state st updates`.\n\n  - `statep |-> StateR stf`: from invariant clause 1 (the only field mutated is\n    `original_`, exactly `update_assum_exactness_state`'s effect — note\n    `update_assum_exactness_state` only rewrites `preTxAssumedState`,\n    `exec_specs.v:3469–3480`; `newStates`, `block_state_`, code maps unchanged,\n    matching the C++ which never writes `current_` here).\n  - `updates_stricter st updates`: invariant clause 2, accumulated from the\n    per-account `min_balance_update` steps (each an `assumption_exactness_stricter`\n    step; compose with transitivity of `min_balance_stricter`).\n  - the `∀ preTxState ⊨ stf` equality: §1 Reduction + §5 per-account, every step\n    of which was proven \"for all `preTxState ⊨ stf`\" via Lemmas 1–2.\n\n∎ (modulo A0, A1, A2.)\n\n--------------------------------------------------------------------------------\n## 7. What is actually provable vs. the stated equality (honesty about A2)\n\nAgainst `reservebal.v`'s `finalBalSufficient` (post-state `isSC`), the\npost-condition's **exact equality** `retb = negb (allFinalBalSufficient …)`\nholds for every `preTxState ⊨ stf` **except** on accounts in the pre/post\ncontract-ness disagreement set (§5 Step A). There the achievable, and\nsafety-relevant, statement is the one-sided bound:\n\n```\nallFinalBalSufficient 3 (preTxState,hist) (applyUpdates stf preTxState) chg [] tx = true\n  ⇒  retb = false              (* C++ accepts ⇒ model accepts *)\n```\n\nequivalently `retb = true ⇒ ∃ a ∈ chg. ¬finalBalSufficient(a)` is *weakened*\ninto \"C++ may additionally revert EOA-turned-contract accounts the model would\nhave waved through\". This is the \"C++ is slightly less permissive (reverts\nmore)\" property the user flagged: it only ever causes *extra* reverts, never a\nwrongful acceptance, so block-level soundness (`fullBlockStep`, `reservebal.v:680`)\n— which only needs \"accepted ⇒ balances sufficient\" — is preserved.\n\nTo recover a literal exact equality (if desired later), change `reservebal.v`'s\n`finalBalSufficient` first disjunct from `isSC postTxState a` to `isSC\npreTxState.1 a` (i.e. test the pre/original state, matching the C++), or add the\nEOA-turned-contract addresses to the `nonDelegatedCodeExecAccounts` argument\n(A3). Either makes Step A an exact iff and removes the A2 caveat.\n\n--------------------------------------------------------------------------------\n## 8. Checklist of Coq obligations (for formalization)\n\n1. **Lemma F** (§2): fee equality + no `uint256_t` overflow from `validTx`.\n2. **Lemma 1** (§3): `R_pess(a) = R(a)` for `preTxState ⊨ stf`, from\n   `min_balance_update`/`satAccountNonStorageAssumptions`; both regimes.\n3. **Lemma 2** (§3): speculative→applied balance comparison transfer; the\n   `Z↔N` re-basing in `postTxActualBalNonce` and `min_balanceN` bookkeeping —\n   *the heaviest step*.\n4. **skip_cpp characterization** (§5 A): `is_delegated`/`intercode_size`/code-hash\n   ⟹ `isAcSC (orig_account a)`; and the A2 pre/post bridge lemma (+ its caveat).\n5. **`can_sender_dip` ↔ `isAllowedToEmptyExec`** (§5 D): discharge the callee\n   spec's `historyConsistent`/delegation hypotheses from the outer spec hyps.\n6. **Loop framing** (§4): `wp_for` over `MapCurrentR`; carry the invariant;\n   `updates_stricter` accumulation (transitivity).\n7. **A0 assert discharge** (§5 D sender): assume `θ_cpp.has_value()`.\n8. **Final assembly** (§6): `StateR stf` reconstruction; the `∀ preTxState`\n   post-condition.\n"
}
Agent
#16

I've read all three artifacts and written a detailed, no-handwaving proof sketch to monad/monadproofs/proofs/dipped_into_reserve_proof_sketch.md. Here's the summary.

What the spec actually says

dipped_into_reserve_spec (exec_specs.v:3975) is, after the resource frame:

∃ updates, let stf := update_assum_exactness_state st updates in
  statep |-> StateR stf  **  [| updates_stricter st updates |]  **
  [| ∀ preTxState, satisfiesAssumptions stf preTxState →
       retb = negb (allFinalBalSufficient 3 (preTxState,hist)
                      (applyUpdates stf preTxState) (map fst (newStates st)) … tx) |]

So dipped_into_reserve must return true iff some changed account fails finalBalSufficient (reservebal.v:551), and it must do so *for every concrete preTxState consistent with the assumptions the function records into State*.

Two structural notes I confirmed:

  • reservebalold.v does not exist; finalBalSufficient/allFinalBalSufficient live only in reservebal.v. Per your confirmation, the sketch targets reservebal.v (the Require Import … reservebalold at line 19 is stale).
  • reservebal.v's allFinalBalSufficient takes a 6th arg (nonDelegatedCodeExecAccounts) the current spec text omits; it instantiates to [] here.

The shape of the proof

  1. Reduction. allFinalBalSufficient = forallb finalBalSufficient chg, and C++ returns on the first dipping account, so the goal is a per-account match dippedAt(a) ⟺ ¬finalBalSufficient(a). Early return is sound because one ¬finalBalSufficient witness already falsifies the forallb.
  1. Two reconciliation lemmas — the heart, since C++ works with a pessimistic original balance and a speculative current balance, never the real preTxState:
  • Lemma 1: the threshold lambda's recorded assumption (min_balance_update … DefReserve) pins min(DefReserve, actual_orig_bal) = min(DefReserve, pessimistic) on any preTxState ⊨ stf — either by forcing actual ≥ DefReserve or by switching to exact validation. So the model erb equals the C++ reserve.
  • Lemma 2: the speculative recent balance comparison transfers to the applyUpdates (actual) balance comparison, via postTxActualBalNonce re-basing + min_balanceN bookkeeping. This is the heaviest arithmetic step.
  1. Loop invariant over state.current() = newStates st, carrying tightened assumptions (updates_stricter) and a no-dip-yet flag.
  1. Per-iteration case split (§5), mapping each C++ branch to a finalBalSufficient branch: non-sender → R ≤ cur(a) (exact); sender → can_sender_dip ⟺ isAllowedToEmptyExec, then R − fee ≤ cur(a).

Where the exact equality breaks (your flagged item)

The isSC skip (C++ lines 57–69) tests the original code hash (isAcSC(orig) ⟺ isSC pre_state), whereas reservebal.v tests isSC postTxState. They diverge on EOA-turned-contract (the addr2 scenario at reservebal.v:586) and selfdestruct. As you said, this is known/not a safety issue: it only causes extra reverts, so the achievable, soundness-relevant direction C++ accepts ⇒ model accepts still holds. The sketch states this precisely (§7) and notes the one-line model change (isSC postTxState → isSC preTxState.1) that would restore literal equality.

I also handled your two assumptions explicitly: MONAD_ASSERTs are assumed (the monad_assertion_failed_spec has a False precondition) — and the line 94 assert gas_fees ≤ reserve turns out to be load-bearing for the sender case (it makes C++'s optional threshold line up with the model's N-truncated R − fee); and the staking ≥ 10MON assumption discharges the a = StakingContractAddr disjunct (it's normally skipped anyway as a contract).

§8 lists the 8 concrete Coq lemmas to formalize. Want me to start formalizing any of them — e.g. drafting Lemma 1 (the cleanest) or the can_sender_dip ↔ isAllowedToEmptyExec bridge — in Coq next?

User
/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs#17

the proof looks handwavy. try to find a counterexample (other than the issues I already mentioned before).

Agent
#18

You're right to push — my "Lemma 2" was where I waved my hands, and it's actually false. Here is a concrete counterexample that has nothing to do with the isSC pre/post issue, the MONAD_ASSERT, or the staking contract.

The counterexample: relaxed validation + raw speculative current balance

The C++ reads the speculative current balance directly and compares it to the reserve, recording no assumption about it:

std::optional<Account> const &curr_account = stack.recent().account_;      // speculative
uint256_t const curr_balance = curr_account.has_value() ? ... .balance : 0;
if (!violation_threshold.has_value() || curr_balance < violation_threshold.value()) { ... }

But the model (finalBalSufficient, reservebal.v:560) compares balanceOfAc postTxState a where postTxState := applyUpdates stf preTxState — the re-based actual balance. Under relaxed validation with min_balance = Some _, applyUpdates/accountFinalVal (exec_specs.v:2730–2733) sets

cur(a) = actualPre.balance + (specPost.balance − assumedPre.balance)   (= A + spec − O)

The only assumption the function records is the lambda's check_min_original_balance_update orig addr DefReserve — i.e. min_balance_update … debit:=DefReserve on the original balance. That records, at best, min_balance = max(drawdown, DefReserve) — which only guarantees the re-based balance stays ≥ 0, **not ≥ reserve**.

Concrete instance (one changed, non-sender, delegated EOA a)

Let DefReserve = 10 MON. Account a is a delegated EOA (so neither the C++ orig_code_hash/is_delegated skip nor the model isSC skip fires — both check it), a ≠ sender, a ≠ 0x1000, and relaxedValidation = true.

  • Speculative original balance (in orig): O = 100 MON.
  • The tx invokes a's delegated code and drains 90 MON → speculative final spec(a) = 10 MON.
  • Recorded assumption: drawdown d = O − spec = 90 MON; lambda update keeps min_balance = max(90, 10) = 90 MON. ⇒ constraint is just **A ≥ 90 MON** on the actual pre-tx balance A.
  • Pick the actual pre-state with **A = 90 MON** (satisfies satisfiesAssumptions stf).

| | reserve / erb | current balance compared | verdict | |---|---|---|---| | C++ | min(10, O=100) = 10 | curr = spec = 1010 < 10? no | not dipped, retb = false | | Model | erb = min(10, A=90) = 10 | cur(a) = 90 + 10 − 100 = 010 ≤ 0? no | finalBalSufficient(a) = falsedipped |

So retb = false, but negb (allFinalBalSufficient …) = negb false = true. **retb ≠ negb(allFinalBalSufficient)** — the spec's post-condition equality is violated.

And critically this is the unsafe direction: on the actual pre-state, the tx drains a from 90 MON to 0, far below its 10 MON reserve, yet the C++ commits it. The reserve mechanism's whole job (guaranteeing fee solvency of later already-accepted txs) is broken for this tx. (This is not "extra reverts" like the isSC issue — it's a missed revert.)

Root cause

dipped_into_reserve records a min-balance assumption only on the original balance (debit = DefReserve), never on the current balance against the reserve threshold. To be sound under relaxed validation it would need min_balance ≥ drawdown + reserve (so A − drawdown ≥ reserve), i.e. it should route the current-balance comparison through State::check_min_balance(addr, threshold) (whose slice spec at exec_specs.v:2275 records exactly min_balance_update … cur:=spec value:=threshold, giving A ≥ 100 here and closing the gap). Instead it does a bare stack.recent().account_.balance < threshold, which records nothing. With relaxedValidation = false the bug vanishes (then cur(a) = spec(a) exactly), so it's specifically a relaxed-validation soundness hole.

I'll fix the sketch to mark Lemma 2 as refuted rather than "the heaviest step."

2 tool calls
Tool

Edit: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/dipped_into_reserve_proof_sketch.md

{
  "replace_all": false,
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/dipped_into_reserve_proof_sketch.md",
  "old_string": "### Lemma 2 (current balance transfers: `spec(a) ⋚ θ ⟺ cur(a) ⋚ θ`)",
  "new_string": "### Lemma 2 (current balance transfers: `spec(a) ⋚ θ ⟺ cur(a) ⋚ θ`)\n\n> **REFUTED — see \"Counterexample\" below.** This lemma is FALSE under relaxed\n> validation. The function never records any assumption on the *current* balance\n> against the reserve threshold, so the speculative comparison the C++ performs\n> does not transfer to the re-based actual comparison the model performs. The\n> text below is kept to show exactly where the (non-)argument breaks.\n"
}
Tool

Edit: /home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/dipped_into_reserve_proof_sketch.md

{
  "replace_all": false,
  "file_path": "/home/abhishek/fv-workspace-inner/monad/monadproofs/proofs/dipped_into_reserve_proof_sketch.md",
  "old_string": "--------------------------------------------------------------------------------\n## 8. Checklist of Coq obligations (for formalization)",
  "new_string": "--------------------------------------------------------------------------------\n## 7b. COUNTEREXAMPLE — Lemma 2 is false (relaxed validation, current balance)\n\nDistinct from A0/A1/A2. This is an **unsafe** mismatch (C++ accepts a tx the\nmodel reverts), found by trying to discharge Lemma 2.\n\n**Root cause.** `dipped_into_reserve` compares the **speculative** current\nbalance `stack.recent().account_.balance` (C++ lines 86–90) directly against the\nthreshold and records **no** assumption about it. The model compares the\n**re-based actual** balance `balanceOfAc (applyUpdates stf preTxState) a`, which\nunder `relaxedValidation = true` and `min_balance = Some _` is\n`A + spec − O` (`accountFinalVal`, `exec_specs.v:2730–2733`). The only assumption\nthe function records is the lambda's `check_min_original_balance_update orig addr\nDefReserve` — on the **original** balance with debit `DefReserve` — which yields\n`min_balance = max(drawdown, DefReserve)`. That only forces the re-based balance\n`≥ 0`, **not `≥ reserve`**.\n\n**Instance.** `DefReserve = 10 MON`. `a` = delegated EOA (checked by both C++ and\nmodel — not skipped), `a ≠ sender`, `a ≠ 0x1000`, `relaxedValidation = true`.\n  - speculative original `O = 100 MON`; tx drains `a` by 90 ⇒ speculative final\n    `spec(a) = 10 MON`.\n  - drawdown `d = 90`; recorded `min_balance = max(90,10) = 90`; constraint on the\n    actual pre-tx balance is just `A ≥ 90 MON`.\n  - choose `A = 90 MON` (⊨ `satisfiesAssumptions stf`).\n\n| | reserve / erb | current compared vs threshold | verdict |\n|---|---|---|---|\n| C++ | `min(10,100)=10` | `curr = spec = 10`, `10<10`? no | not dipped, `retb=false` |\n| model | `erb = min(10,90)=10` | `cur(a)=90+10−100=0`, `10≤0`? no | `finalBalSufficient(a)=false`, dipped |\n\n⇒ `retb = false` but `negb (allFinalBalSufficient …) = true`. The equality\nfails. On the actual state `a` goes 90 → 0 MON (below its 10 MON reserve) yet the\ntx is committed — the reserve invariant is violated (a *missed* revert, not an\nextra one).\n\n**Why the proof can't paper over it.** With `relaxedValidation = false` we get\n`cur(a) = spec(a)` and the lemma holds; the bug is specific to relaxed\nvalidation, which the spec neither excludes nor the real (K-ahead speculative)\nsystem avoids.\n\n**Fix (code).** Route the current-balance check through\n`State::check_min_balance(addr, threshold)` (slice spec `exec_specs.v:2275`,\nrecords `min_balance_update … cur:=spec value:=threshold`) instead of a bare\n`stack.recent().account_.balance < threshold`. In the instance that records\n`min_balance = max(90, 100) = 100` ⇒ `A ≥ 100` ⇒ `cur(a) ≥ 10 = reserve`, closing\nthe gap. Equivalently: record `min_balance ≥ drawdown + reserve`.\n\n--------------------------------------------------------------------------------\n## 8. Checklist of Coq obligations (for formalization)"
}
Agent
#19

I've recorded the counterexample in the sketch doc (§7b) and flagged Lemma 2 as refuted.

Bottom line: the genuine gap — separate from the three you'd already noted — is that dipped_into_reserve compares the speculative stack.recent() balance against the reserve and records no assumption on it, while the model compares the re-based actual balance under relaxed validation. Because the only recorded assumption (check_min_original_balance(addr, DefReserve) on the original balance) merely keeps the re-based balance ≥ 0 rather than ≥ reserve, a drained non-sender delegated EOA whose actual pre-tx balance sits at the recorded min_balance ends up below reserve on the actual state — and the C++ commits it anyway. That's a missed revert (unsafe), and it disappears only when relaxedValidation = false. The fix is to take the current-balance check through State::check_min_balance(addr, threshold) so it records min_balance ≥ drawdown + reserve.