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 It 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.
How This Was Modeled and 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.
Scope
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.
Proof artifacts
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.
Correct payout, books balanced, pool solvent, no double-dip, and no overclaim are proven for claimUsdc, claimWeth, and claimBoth. Cross-token independence is proven for claimBoth.
All 26 theorems are proven. Proofs.lean is sorry-free.
| Function | Theorem | Status |
|---|---|---|
| claimUsdc | claimUsdc_marks_user_claimed | Proven |
| claimUsdc | claimUsdc_updates_round_claimed | Proven |
| claimUsdc | claimUsdc_updates_total_allocated | Proven |
| claimUsdc | claimUsdc_preserves_weth_state | Proven |
| claimUsdc | claimUsdc_preserves_round_bound | Proven |
| claimUsdc | claimUsdc_claimed_plus_allocated_conserved | Proven |
| claimUsdc | claimUsdc_reverts_if_already_claimed | Proven |
| claimUsdc | claimUsdc_reverts_if_exceeds_total | Proven |
| claimWeth | claimWeth_marks_user_claimed | Proven |
| claimWeth | claimWeth_updates_round_claimed | Proven |
| claimWeth | claimWeth_updates_total_allocated | Proven |
| claimWeth | claimWeth_preserves_usdc_state | Proven |
| claimWeth | claimWeth_preserves_round_bound | Proven |
| claimWeth | claimWeth_claimed_plus_allocated_conserved | Proven |
| claimWeth | claimWeth_reverts_if_already_claimed | Proven |
| claimWeth | claimWeth_reverts_if_exceeds_total | Proven |
| claimBoth | claimBoth_marks_both_claimed | Proven |
| claimBoth | claimBoth_updates_round_claimed | Proven |
| claimBoth | claimBoth_updates_total_allocated | Proven |
| claimBoth | claimBoth_preserves_round_bounds | Proven |
| claimBoth | claimBoth_claimed_plus_allocated_conserved | Proven |
| claimBoth | claimBoth_reverts_if_usdc_already_claimed | Proven |
| claimBoth | claimBoth_reverts_if_weth_already_claimed | Proven |
| claimBoth | claimBoth_reverts_if_usdc_exceeds_total | Proven |
| claimBoth | claimBoth_reverts_if_weth_exceeds_total | Proven |
| claimBoth | claimBoth_matches_independent_claims | Proven |
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
Hypotheses
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 check is trustedCryptographic guarantee (OpenZeppelin MerkleProof)
Merkle verification is modeled as a boolean witness,
proofAccepted: Bool. This is the one real trust assumption: we assume the Merkle tree correctly binds(msg.sender, shareWad)to the stored root. That is a cryptographic guarantee from OpenZeppelin's MerkleProof library, not a contract-level concern.model covers a single 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 are not modeledOpenZeppelin SafeERC20
The model verifies state accounting only, not ERC20 transfer execution. SafeERC20 correctness is trusted from OpenZeppelin.
msg.sender is the real callerEVM guarantee
The model assumes the caller identity in
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
Kuru V2 Shared Withdrawal Budget
After any batch of ordinary withdrawals, the withdrawal budget drops by exactly what was paid out, whichever accounts or tokens were used.
Dromos MetaDEX Allocation Conservation
In the root-ledger model, chain allocations sum to committed voting power.

KPK Shares Settlement Accounting
A successful kpkShares settlement batch processes each eligible request at most once and accounts exactly for its fees, shares and asset transfers.