From 9acdae7cc9a990f35fce6de54b81d47e008ce2c6 Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Wed, 23 Sep 2026 00:31:00 +0000 Subject: [PATCH] Share inflation: the perpetual futures pool uses two defenses, and the formula shows why MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The Adversary: the section closed on "a program needs one of these four", listing the perpetual futures pool as one that locks shares, while the bullet above it says recording the pool's size is not enough there. The close now says a program also has to check that nothing else it allows raises the pool's value the way a donation does, and that the perpetual futures pool both ignores donations and locks shares, and why. - Perpetual Futures: the inflation failure mode gives the share formula, deposit × (shares outstanding + 1,000) ÷ AUM, and the test's result: the victim is minted 2,001 shares against the attacker's 1, where the bare supply of 1 would have minted a single share. Claude-Session: https://claude.ai/code/session_01HagqhpPNqcywT37qqgnFey --- building-financial-products-in-solana/perpetual-futures.md | 2 +- financial-programming-concepts-in-solana/the-adversary.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/building-financial-products-in-solana/perpetual-futures.md b/building-financial-products-in-solana/perpetual-futures.md index b6f0a51e..fbd4e136 100644 --- a/building-financial-products-in-solana/perpetual-futures.md +++ b/building-financial-products-in-solana/perpetual-futures.md @@ -135,7 +135,7 @@ The book's standing invariants hold here too. The vault balance always equals `l - **A stale or manipulated price** is the catastrophic one: with 10x leverage, a 1% price error is a 10% equity error, market-wide, simultaneously. That explains the five checks, the confidence cap, and the one trust decision the program leaves visible: the pool creator chooses the feed, and the feed account's owner is deliberately unchecked. Choose oracles the way you choose custodians. - **The unliquidated-position gap**: liquidations that pay nothing do not happen, and the LPs absorb the loss. The example documents this gap, and it is why production venues add an insurance fund. -- **First-depositor share inflation**: a donation to the vault moves nothing, because AUM reads `liquidity`, not the vault balance. But `liquidity` grows with every funding payment and trader loss, and a liquidity provider can also be the pool's only trader. An attacker holding a single share can pay funding into the pool from their own position to make that share expensive, then wait for a deposit to round down. The withheld `MINIMUM_LIQUIDITY` makes this cost them, because it counts as 1,000 shares in both divisors: the attacker's share is 1 of 1,001, and almost everything they pay in stays with the locked shares. `test_inflating_liquidity_through_own_trades_does_not_pay` runs the attack: the attacker pays about 1,000 USDC of funding and gets back about 1 USDC. Divided by the bare supply instead, the same attack returns them about 1,500 USDC, 500 of it from the victim's 2,000 USDC deposit. `test_first_deposit_below_minimum_fails` proves a dust first deposit cannot even open the pool. +- **First-depositor share inflation**: a donation to the vault moves nothing, because AUM reads `liquidity`, not the vault balance. But `liquidity` grows with every funding payment and trader loss, and a liquidity provider can also be the pool's only trader. An attacker holding a single share can pay funding into the pool from their own position to make that share expensive, then wait for a deposit to round down. The withheld `MINIMUM_LIQUIDITY` makes this cost them, because it counts as 1,000 shares in both divisors: the attacker's share is 1 of 1,001, and almost everything they pay in stays with the locked shares. A deposit mints deposit × (shares outstanding + 1,000) ÷ AUM. In the test the attacker pumps AUM to just over 1,000 USDC and the victim deposits just under twice that, which mints the victim 2,001 shares against the attacker's 1. Dividing by the bare supply of 1 would have minted the victim a single share. `test_inflating_liquidity_through_own_trades_does_not_pay` runs the attack: the attacker pays about 1,000 USDC of funding and gets back about 1 USDC. Divided by the bare supply instead, the same attack returns them about 1,500 USDC, 500 of it from the victim's 2,000 USDC deposit. `test_first_deposit_below_minimum_fails` proves a dust first deposit cannot even open the pool. - **Winning too big**: profit beyond the reservation is clipped by the cap rather than bankrupting the pool, and traders know the cap before they open. - **Sandwiching and price drift**: every money handler takes a user-supplied bound and fails with `SlippageExceeded` instead of filling at a surprise price. - **Double close, double liquidation**: the position account closes on exit; the second call finds no account. diff --git a/financial-programming-concepts-in-solana/the-adversary.md b/financial-programming-concepts-in-solana/the-adversary.md index f86f0ecd..5c2d1366 100644 --- a/financial-programming-concepts-in-solana/the-adversary.md +++ b/financial-programming-concepts-in-solana/the-adversary.md @@ -56,7 +56,7 @@ Bob can protect his own deposit with a minimum-output bound. The vault strategy' - **Ignore donations.** The program records the pool's size in its own state, as the lending market's `available_liquidity` and the vault strategy's recorded holdings do, rather than reading the vault's token balance. Step 2 then changes the vault's balance and nothing the handler reads. The pool is still one minor unit against one share, and Bob's deposit mints a share per minor unit as it should. The donated 1,000 USDC sits in the vault outside the program's accounting, lost to the attacker, and the lending example tests this with `raw_token_donation_does_not_inflate_exchange_rate`. The vault strategy's `test_donation_does_not_inflate_share_price` runs these exact numbers. A recorded size stops donations only while the attacker has no other way to raise it, and the *Perpetual Futures* chapter shows one: a provider who is also the pool's only trader can pay funding into it. - **Require a large first deposit.** If the first deposit must be at least 100 USDC, the attacker's smallest position is 100,000,000 shares. Rounding Bob's 1,000 USDC down to nothing would then take a share price above 1,000 USDC, a vault holding a hundred billion dollars. -A program needs one of these four, chosen on purpose. The book's programs each name theirs: the AMM and the perpetual futures pool lock shares, and the lending market and the vault strategy ignore donations. +A program needs at least one of these four, chosen on purpose, and has to check that nothing else it allows raises the pool's value the way a donation does. The book's programs each name theirs. The AMM locks shares, and the lending market and the vault strategy ignore donations. The perpetual futures pool does both: it ignores donations by pricing shares from its recorded `liquidity`, and locks shares because its own traders can raise that figure. ## A True Price Can Still Be a Manipulated Price