StreamRecoveryClaim Accounting Invariants
No user can claim more than their share. The round pool stays solvent. The two token flows don't interfere.
The StreamRecoveryClaim contract is a Merkle-proof-based distribution system for recovering USDC and WETH to ~40 users affected by the Stream Trading incident on Trevee Earn's vaults on Sonic chain. An admin snapshots affected users' vault balances, computes each user's pro-rata share, builds a Merkle tree with leaves (address, shareWad), and stores only the root onchain.
Users sign an EIP-712 liability waiver, then call claimUsdc or claimWeth. The contract verifies the Merkle proof against the stored root, computes amount = shareWad × roundTotal / 1e18, checks accounting bounds, and pays out. claimBoth calls both sequentially.
Why this matters
This contract distributes recovery funds to victims of an exploit. A bug in the claim logic could let someone extract more than their entitlement, claim twice, or drain the round pool, taking funds that belong to other victims. The stakes are not abstract: every dollar overclaimed is a dollar another affected user does not receive.
What these invariants cover
The proofs cover six property families across all three claim functions:
- Correct payout: claimed counter increases and allocated counter decreases by exactly
shareWad × roundTotal / 1e18 - Books balanced:
roundClaimed + totalAllocatedis conserved per token - Pool solvent:
roundClaimed ≤ roundTotalper token - No double-dip: if user already claimed, function reverts, state unchanged
- No overclaim: if payout would exceed round budget, function reverts, state unchanged
- Cross-token independence:
claimBothproduces identical state to calling each claim separately
The model covers a single-round protocol slice. Multi-round batching (claimMultipleUsdc) is out of scope. The per-round accounting properties hold identically for each round.
How this was proven
The claim functions were modeled in Verity from the Solidity StreamRecoveryClaim.sol implementation. Merkle verification is abstracted as a boolean witness and token transfers are elided; the proofs focus on accounting correctness.
The Lean model maps ten storage slots: round totals, claimed counters, allocated counters, round-active flag, per-user waiver and claim flags, split across USDC and WETH. Each function mutates this state differently:
- claimUsdc: verifies the Merkle proof, computes the payout, increments
roundUsdcClaimed, decrementstotalUsdcAllocated, marks the user as claimed - claimWeth: symmetric to
claimUsdcon the WETH slots - claimBoth: calls
claimUsdcthenclaimWethsequentially. The cross-token independence proof shows this composition is safe: USDC accounting does not interfere with WETH accounting
How do you prove the books stay balanced? You show that every successful claim increases roundClaimed and decreases totalAllocated by exactly the same amount, so their sum is conserved. Combined with the solvency bound (roundClaimed ≤ roundTotal), this guarantees no value is created or destroyed and the round pot is never overdrawn.
All proofs are verified by Lean 4's kernel and are provided in Proofs.lean. If any step were wrong, the code would not compile.
Verify it yourself
git clone https://github.com/lfglabs-dev/verity-benchmark cd verity-benchmark lake build Benchmark.Cases.PaladinVotes.StreamRecoveryClaimUsdc.Compile
If the build succeeds, the proofs are correct. Source repository
Proof status
All 18 theorems are proven. Proofs.lean is sorry-free.
| Function | Correct payout | Books balanced | Pool solvent | No double-dip | No overclaim | Cross-token |
|---|---|---|---|---|---|---|
| claimUsdc | proven | proven | proven | proven | proven | — |
| claimWeth | proven | proven | proven | proven | proven | — |
| claimBoth | proven | proven | proven | proven | proven | proven |
Assumptions
The proofs use zero axioms. The only trust assumption is the Merkle proof abstraction. All other conditions are consumed in exactly the form the contract enforces them.
Merkle proof abstractionproofAccepted: BoolCryptographic guarantee (OpenZeppelin MerkleProof)
Merkle verification is modeled as a boolean witness
proofAccepted. This is the one real trust assumption: we assume the Merkle tree correctly binds(msg.sender, shareWad)to the stored root. This is a cryptographic guarantee from OpenZeppelin's MerkleProof library, not a contract-level concern.Single-round specializationmodel covers one roundScope decision
The model covers one round. Multi-round batching (
claimMultipleUsdc) is out of scope. The per-round accounting properties hold identically for each round.Token transfers elidedstate accounting onlyOpenZeppelin SafeERC20
The model verifies state accounting, not ERC20 transfer execution. SafeERC20 correctness is trusted from OpenZeppelin.
msg.sender authenticitycaller identityEVM guarantee
msg.senderis authentic. This is an EVM-level guarantee, not a contract-level concern.
Learn more
Sonic Earn Recovery System, the upstream contract repository.
What is a formal proof? A short explanation for non-specialists.
More research
Alchemix V3 Earmark Conservation
Formally verified earmark conservation invariant for Alchemix V3's lazy-accrual debt accounting system.

Reserve DTF Auction Price Band Invariant
Formally verified per-pair price band invariant for the auction pricing path in Reserve DTF Protocol.
Cork Protocol Pool Solvency
Formally verified solvency invariant for the unwindExerciseOther function in Cork Phoenix.