Cork Protocol Pool Solvency

After an unwind, locked collateral still covers the outstanding swap share supply.

The left side converts collateral to 18-decimal units (e.g. for USDC with 6 decimals, colScaleUp = 1012). The right side is the outstanding cST shares held by users (total supply minus what the pool holds). The inequality says: there is enough collateral to back every outstanding share.

Cork Protocol is a risk layer for DeFi assets like stablecoins, LSTs, and yield-bearing tokens. Each Cork pool pairs a collateral asset (CA) with a reference asset (REF), an existing on-chain token (e.g. a stablecoin or LST) whose peg the pool protects against. Depositors provide CA and receive two tokens: a swap token (cST) granting the right to exchange REF for CA before expiry, and a principal token (cPT) representing the depositor's claim on whatever collateral remains after all swaps are settled. In the happy path (no depeg), cPT holders redeem their full deposit; in a depeg, cST holders exercise the swap to claim collateral, and cPT holders absorb the loss.

unwindExerciseOther lets a user reverse a swap position. The user provides reference assets; the pool computes how many cST shares to release and how much additional collateral to lock. Certora's formal verification of Cork Phoenix defined a solvency invariant called P-02: after any state-changing operation, locked collateral must still cover all outstanding cST claims. They verified P-02 for every other CorkPool function but timed out on this one. This proof closes that gap.

Why this matters

An unwind reverses a swap position: the user sends reference assets back, and the pool returns cST shares to its own balance (reducing outstanding claims) while locking additional collateral. If solvency broke on this code path, the collateral locked could fall short of the remaining outstanding cST, meaning holders who bought depeg protection could find the pool unable to pay out when they exercise their swap.

The function chains three rounding operations across different decimal scales. The proof covers the full parameter space: arbitrary exchange rates and arbitrary decimal combinations.

What this covers

This case study isolates the solvency-critical arithmetic of unwindExerciseOther from Cork Phoenix v1.1.2. The modeled call path includes:

  • calculateEqualSwapAmount (floor-rounded share computation)
  • tokenNativeDecimalsToFixed and normalizeDecimalsWithCeilDiv (decimal scaling)
  • mulDivCeil (ceiling-rounded collateral computation)
  • The require check for sufficient liquidity

Collateral and swap shares use different decimal precisions (e.g. USDC has 6 decimals, swap shares have 18). The invariant compares them on the same scale by multiplying collateral by colScaleUp (= 10(18 - decimals)).

Out of scope: external token transfers, oracle rate updates, the time-decay fee pipeline, access control, reentrancy guards, and event emissions. These are orthogonal to the solvency arithmetic.

How this was modeled / proven

The model is built directly from the Solidity source code of Cork Phoenix v1.1.2 (commit 40d9b173c4b), not from Certora's specification. The call path crosses four Solidity files: PoolLib.sol, MathHelper.sol, TransferHelper.sol, and CorkPoolManager.sol. These are inlined into a single Lean file (Contract.lean) since the proof only needs the arithmetic, not the contract boundaries.

When Cork processes an unwind, it computes two things: how many shares to release (using floor rounding, which rounds down) and how much collateral to lock (using ceiling rounding, which rounds up). Both roundings favor the pool: it locks slightly more collateral than strictly needed and releases slightly fewer shares than strictly needed.

The core lemma double_ceilDiv_sandwich proves that this asymmetry is enough to guarantee solvency for any exchange rate and any decimal combination. It says: the ceiling-rounded collateral always covers the floor-rounded shares.

Simplifications
  • External calls elided. Token transfers and oracle rate updates happen outside the solvency arithmetic. The model only captures the math that changes the pool's internal state (locked collateral and swap balances).
  • Time-decay fee factored out. In the real Solidity, the pool adds gross collateral then subtracts a fee. We model the net result directly ( += assetsInWithoutFee), which is mathematically identical but skips the fee calculation pipeline.
  • Access control and events omitted. Reentrancy guards, role checks, and event emissions do not affect the solvency arithmetic.
Verify it yourself
git clone https://github.com/lfglabs-dev/verity-benchmark
cd verity-benchmark
lake build Benchmark.Cases.Cork.PoolSolvency.Compile

If the build succeeds, the proofs are correct. Source repository

Proof status

The Cork case has one main theorem and two supporting results. All are fully proven with no axioms and no sorry.

TheoremStatus
solvency_preservedproven
unwind_slot_writeproven
double_ceilDiv_sandwichproven

View contract modelView specs in LeanView proofs in Lean

Assumptions / hypotheses

The proof uses zero axioms. The hypotheses below are preconditions on the contract state before the call. Each mirrors a constraint enforced by Cork's pool creation or by ERC-20 accounting.

  • hSolvencyBeforesolvency holds before the call

    P-02 invariant (inductive step)

    The theorem is an inductive step: if solvency held before unwindExerciseOther, it still holds after. The base case is pool creation, where both collateral and outstanding shares start at zero.

  • hColScale, hRefScale, hSwapRate, hRefOutcolScaleUp, refScaleUp, swapRate, referenceAssetsOut > 0

    Pool creation + oracle + non-trivial call

    All scale factors, the exchange rate, and the unwind amount must be positive. Scale factors are 10(18 - decimals) (positive for any token with ≤ 18 decimals); the rate comes from the oracle; a zero-amount unwind is a no-op. In Lean's natural number type there is no “unsigned nonzero”, so these are stated individually as > 0.

  • hNoOvf1 .. hNoOvf5intermediate uint256 arithmetic stays in range

    Overflow preconditions

    Five hypotheses guard that products and sums during the unwind do not exceed 2256. In practice these hold whenever token supplies and rates stay below ~2128 (well within any realistic ERC-20 supply). They could be derived from a single weaker bound on the inputs, but we state them per-operation to match the Solidity exactly.

  • hSupplyGeBalswapTotalSupply ≥ swapBalanceOfPool

    ERC-20 invariant

    Total supply of swap shares is at least the pool's own balance. This means outstanding shares (supply minus pool balance) is non-negative. Without this, the right side of the solvency inequality would underflow.

View specs in LeanView proofs in Lean

How this relates to Certora's audit

Certora verified P-02 solvency across 13 CorkPool functions using bounded model checking (SMT solver). Their prover timed out on unwindExerciseOther because the chained rounding produces non-linear arithmetic the solver cannot discharge within its search depth.

We took a different route: manual translation of the arithmetic into Lean 4, then a step-by-step proof that the ceiling/floor asymmetry preserves solvency for all inputs. No search bound, no timeout: the proof is checked by Lean's kernel.

Produced as part of the Verity benchmark, an open database of formally specified contract properties.

Learn more

Upstream Solidity contracts: PoolLib.sol, MathHelper.sol, TransferHelper.sol, CorkPoolManager.sol.

Full case directory in the Verity benchmark repository.

What is a formal proof? A short explanation for non-specialists.

More research