Files
proofs/lean/Bridge/YieldVault.lean
2d7e2a6309 proofs: drive Lean library to 0 errors, eliminate native_decide
The lean/ library (112 files) had ~110 type-check errors across 52 files (the
prior "green" status was a grep artifact). It now verifies at 0 errors under
the project's strict setting: `lake env lean -D autoImplicit=false`, all 112
files.

Fixed:
- Quasar/Consensus: replaced `by native_decide` with `by decide` (kernel-checked
  rather than compiler-trusted).
- Vault/KeyIsolation, Bridge/{YieldVault,OmnichainRouter,FillIntegrity},
  Storage/{ZapDB,Replicate}: PRF/string-encoding lemmas, division-bound vault
  accounting, renamed/removed deprecated List lemmas, noncomputable.
- Crypto (Pulsar/Magnetar/Corona/CGGMP21/MLKEM/FROST/FHE/P3Q): missing local
  dependency oleans compiled in dependency order; theorem→noncomputable def for
  Type-valued re-exports; deriving Inhabited for opaque artifact types; dead
  Mathlib imports removed.
- DeFi (AMM/staking/fees/flashloan/HFT/router) + Protocol + GPU + Warp + Trust +
  Compute + Network + Build: Nat-division/AMM-invariant proofs via
  Nat.div_le_of_le_mul / Nat.div_mul_le_self; omega in-scope invariants;
  record-projection opacity; dead tactics; stale Nat.Basic import → Nat.Defs;
  BFT quorum theorems tightened to n=3f+1.

Integrity: no sorry/admit/native_decide remaining (verified); no
proof-by-assumption axiom cheats. Statement changes only tightened false draft
theorems to true invariants. Remaining axioms are trusted-base only (opaque
crypto groups/pairings, RLWE/MLWE/MSIS hardness, FIPS 203/204/205 correctness,
VSS/Paillier, protocol modeling).

Co-authored-by: Hanzo Dev <[email protected]>
2026-06-10 22:34:27 -07:00

207 lines
9.2 KiB
Lean4
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/-
YieldVault Formal Properties
ERC-4626 vault with virtual share offset to prevent first-depositor
donation attacks. Strategies are approved per-operation (no infinite
approvals). Yield reports are monotonic.
Maps to:
- contracts/bridge/YieldVault.sol: deposit/withdraw/yield logic
- contracts/bridge/strategies/: yield strategy adapters
Properties:
- Virtual offset protects first depositor
- Share accounting conservation (pricePerShare invariant)
- No infinite approvals (per-operation approve/reset pattern)
- Yield monotonicity (totalUnderlyingAssets only increases)
-/
import Mathlib.Data.Nat.Defs
import Mathlib.Tactic
namespace Bridge.YieldVault
-- ═══════════════════════════════════════════════════════════════
-- TYPES
-- ═══════════════════════════════════════════════════════════════
/-- Vault state tracking shares and underlying assets -/
structure VaultState where
totalShares : Nat -- total supply of vault shares
totalAssets : Nat -- total underlying assets
virtualShares : Nat -- constant offset (default: 1e8)
virtualAssets : Nat -- constant offset (default: 1)
deriving DecidableEq, Repr
/-- Strategy approval state -/
inductive ApprovalState where
| none : ApprovalState -- no active approval
| approved : Nat → ApprovalState -- approved for specific amount
deriving DecidableEq, Repr
/-- Yield report from a strategy -/
structure YieldReport where
strategyAddress : Nat
gain : Nat
loss : Nat -- capped at 0 in practice
-- ═══════════════════════════════════════════════════════════════
-- CORE FUNCTIONS
-- ═══════════════════════════════════════════════════════════════
/-- Effective total shares including virtual offset -/
def effectiveShares (v : VaultState) : Nat :=
v.totalShares + v.virtualShares
/-- Effective total assets including virtual offset -/
def effectiveAssets (v : VaultState) : Nat :=
v.totalAssets + v.virtualAssets
/-- Convert assets to shares: shares = assets * effectiveShares / effectiveAssets -/
def assetsToShares (v : VaultState) (assets : Nat) : Nat :=
assets * effectiveShares v / effectiveAssets v
/-- Convert shares to assets: assets = shares * effectiveAssets / effectiveShares -/
def sharesToAssets (v : VaultState) (shares : Nat) : Nat :=
shares * effectiveAssets v / effectiveShares v
/-- Deposit: mint shares proportional to deposit amount -/
def deposit (v : VaultState) (assets : Nat) : VaultState × Nat :=
let shares := assetsToShares v assets
({ v with totalShares := v.totalShares + shares,
totalAssets := v.totalAssets + assets }, shares)
/-- Withdraw: burn shares, return proportional assets -/
def withdraw (v : VaultState) (shares : Nat) : Option (VaultState × Nat) :=
if shares ≤ v.totalShares then
let assets := sharesToAssets v shares
some ({ v with totalShares := v.totalShares - shares,
totalAssets := v.totalAssets - assets }, assets)
else none
-- ═══════════════════════════════════════════════════════════════
-- THEOREMS
-- ═══════════════════════════════════════════════════════════════
/-- VIRTUAL OFFSET PROTECTION: With VIRTUAL_SHARES = 1e8, a first-depositor
donation attack yields NO profit — the attacker's redeemable assets are
bounded by what they contributed (the 1 wei deposit plus the `donation`).
Attack: deposit 1 wei, donate D tokens, redeem.
Without offset (totalShares = 0): the first depositor controls
price-per-share and can grief later depositors.
With the virtual offset, redeem(shares1) on the post-donation state
returns at most 1 + D — the attacker cannot extract more than they put in.
STATEMENT CHANGED: the original bound `(1 + donation)/(1 + virtualShares) + 1`
was FALSE — e.g. donation = 2·1e8 gives LHS = 100000001 but that RHS = 2.
The redeem math `shares1 * effAssets / effShares` with shares1 = 1e8 yields
`1e8·(donation+2)/2e8 ≈ donation/2`, which exceeds `donation/(1+1e8)`.
The true, non-trivial invariant is donation-attack-resistance:
redeemable ≤ contributed = 1 + donation (no free money for the attacker). -/
theorem virtual_offset_protection (v : VaultState)
(h_empty : v.totalShares = 0)
(h_no_assets : v.totalAssets = 0)
(h_virtual : v.virtualShares = 100000000) -- 1e8
(h_vassets : v.virtualAssets = 1)
(donation : Nat) :
-- After depositing 1 wei and donating `donation` to vault:
let (v1, shares1) := deposit v 1
let v2 := { v1 with totalAssets := v1.totalAssets + donation }
-- Attacker's redeemable assets bounded by what they contributed
sharesToAssets v2 shares1 ≤ 1 + donation := by
simp only [deposit, assetsToShares, sharesToAssets, effectiveShares, effectiveAssets,
h_empty, h_no_assets, h_virtual, h_vassets]
norm_num
apply Nat.div_le_of_le_mul
ring_nf
omega
/-- SHARE ACCOUNTING CONSERVATION: The price per share relationship holds.
totalSupply * pricePerShare ≈ totalAssets (within integer rounding).
Specifically: sharesToAssets(totalShares) ≤ totalAssets. -/
theorem share_accounting_conservation (v : VaultState)
(_h_pos : effectiveShares v > 0) :
sharesToAssets v v.totalShares ≤ v.totalAssets + v.virtualAssets := by
simp only [sharesToAssets, effectiveAssets, effectiveShares]
apply Nat.div_le_of_le_mul
exact Nat.mul_le_mul_right _ (by omega)
/-- Deposit then withdraw returns at most what was deposited (no free money) -/
theorem no_free_money (v : VaultState)
(assets : Nat)
(_h_pos : effectiveShares v > 0)
(_h_apos : effectiveAssets v > 0) :
let (v', shares) := deposit v assets
sharesToAssets v' shares ≤ assets := by
set S := v.totalShares
set A := v.totalAssets
set vS := v.virtualShares
set vA := v.virtualAssets
simp only [deposit, assetsToShares, sharesToAssets, effectiveShares, effectiveAssets]
-- shares = assets * (S+vS)/(A+vA); goal: shares*(A+assets+vA)/(S+shares+vS) ≤ assets
set sh := assets * (S + vS) / (A + vA) with hsh
-- key: division rounds down, so sh * (A+vA) ≤ assets * (S+vS)
have hkey : sh * (A + vA) ≤ assets * (S + vS) := by
rw [hsh]; exact Nat.div_mul_le_self _ _
apply Nat.div_le_of_le_mul
nlinarith [hkey, Nat.mul_comm sh assets, Nat.mul_comm assets sh]
/-- NO INFINITE APPROVALS: The approve-deposit-reset pattern ensures
strategies never hold persistent approvals.
Pattern: approve(strategy, amount) → strategy.deposit(amount) → approve(strategy, 0)
Each operation is: none → approved(amount) → none. -/
def approveStrategy (_ : Nat) (amount : Nat) : ApprovalState :=
ApprovalState.approved amount
def resetApproval (_ : Nat) : ApprovalState :=
ApprovalState.none
def executeStrategyDeposit (approval : ApprovalState) (amount : Nat) :
Option ApprovalState :=
match approval with
| ApprovalState.approved limit =>
if amount ≤ limit then some (ApprovalState.none) else none
| ApprovalState.none => none
theorem no_infinite_approvals (strategy : Nat) (amount : Nat) :
let approval := approveStrategy strategy amount
∀ result, executeStrategyDeposit approval amount = some result →
result = ApprovalState.none := by
simp [approveStrategy, executeStrategyDeposit]
/-- Approval resets to none after operation -/
theorem approval_lifecycle (strategy : Nat) (_amount : Nat) :
resetApproval strategy = ApprovalState.none := by
simp [resetApproval]
/-- YIELD MONOTONICITY: processYieldReport only increases totalAssets
when gain > loss. In practice, loss is always 0 for healthy strategies. -/
def processYieldReport (v : VaultState) (r : YieldReport) : VaultState :=
{ v with totalAssets := v.totalAssets + r.gain - r.loss }
theorem yield_monotonicity (v : VaultState) (r : YieldReport)
(h : r.gain ≥ r.loss) :
(processYieldReport v r).totalAssets ≥ v.totalAssets := by
simp [processYieldReport]; omega
/-- Healthy strategy (no loss) strictly increases assets when gain > 0 -/
theorem healthy_yield_increases (v : VaultState) (r : YieldReport)
(h_no_loss : r.loss = 0) (h_gain : r.gain > 0) :
(processYieldReport v r).totalAssets > v.totalAssets := by
simp [processYieldReport, h_no_loss]; omega
/-- Virtual offset is never zero (prevents division by zero) -/
theorem virtual_shares_positive (v : VaultState)
(h : v.virtualShares = 100000000) :
effectiveShares v > 0 := by
simp [effectiveShares, h]
theorem virtual_assets_positive (v : VaultState)
(h : v.virtualAssets = 1) :
effectiveAssets v > 0 := by
simp [effectiveAssets, h]
end Bridge.YieldVault