Skip to content

Perps: fix the funding rate at pool creation and cap it - #172

Merged
mikemaccana merged 1 commit into
mainfrom
claude/perps-fixed-funding-rate
Sep 30, 2026
Merged

mikemaccana merged 1 commit into
mainfrom
claude/perps-fixed-funding-rate

Conversation

@mikemaccana

Copy link
Copy Markdown
Collaborator

The attack

set_funding_rate let the pool's authority change the funding rate at any time, with no upper bound. The lighter side of open interest is paid funding out of liquidity, which is the liquidity providers' deposits. So the authority could:

  1. From any wallet, open a small position on the lighter side. open_position accepts any signer, and the program can't tell which wallets belong to the authority.
  2. Call set_funding_rate with a huge rate.
  3. A few seconds later, close the position. The funding it is owed is paid out of liquidity; the only limit is that liquidity can't go below zero (PoolInsolvent).
  4. Liquidate the heavier side's positions, whose collateral the funding has consumed, and collect the liquidation fees.

The Perpetual Futures chapter in quicknode/solana-book says the program gives the operator "no path to anyone's collateral". This PR makes that true.

The fix

  • set_funding_rate is removed from the Anchor, Anchor v1 and Quasar versions. Quasar's discriminator 7 is retired.
  • initialize_pool refuses a funding rate above MAX_FUNDING_RATE_PER_SECOND, which is 277: just under 0.1% of a position's size per hour. It uses the existing InvalidParameter error, the same way MAX_LEVERAGE_CEILING is enforced.
  • Everyone who opens a position or deposits liquidity has seen the rate, because it can't change afterwards.

Tests

  • initialize_pool_rejects_funding_rate_above_the_maximum: 278 is refused and 277 is accepted.
  • operator_on_the_lighter_side_earns_only_the_fixed_rate: a wallet the operator controls holds a 1,000 USDC short against a 10,000 USDC long for an hour at the maximum rate. It asserts that the wallet receives exactly size × 277 × 3600 ÷ 10⁹ (0.9972 USDC), and in the Anchor versions that liquidity falls by exactly that much.
  • The existing funding tests now run at the maximum rate.
  • The liquidity-inflation test used to hold its position for 1,000 seconds at an uncapped rate. It now holds it for ten years at the maximum rate. The pool can back only 1,001 base units of notional, so a capped rate needs that long to pump liquidity well above the withheld minimum. The test's assertions on what the attacker and the victim get back are unchanged.
  • Removed along with the handler:
    • set_funding_rate_settles_at_the_old_rate_first
    • only_authority_can_set_funding_rate

Checks

  • cargo fmt is clean.
  • cargo check --tests passes for all three versions.
  • The tests themselves need the compiled programs, which this environment can't build, so CI runs them.

The book change follows once this merges, as a PR stacked on quicknode/solana-book#190.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JGEoAUjMm7Evv69k46eNcn


Generated by Claude Code

The pool's authority could change the funding rate at any time with
set_funding_rate, with no upper bound. The lighter side of open interest is
paid funding out of `liquidity`, so the authority could open a small position on
that side from any wallet, raise the rate, and close the position to take the
liquidity providers' deposits. They could then liquidate the heavier side's
positions, whose collateral the funding had consumed.

- set_funding_rate is removed from all three versions (Quasar discriminator 7
  is retired).
- initialize_pool refuses a funding rate above MAX_FUNDING_RATE_PER_SECOND
  (277, just under 0.1% of a position's size per hour), with the existing
  InvalidParameter error.
- New tests:
  - initialize_pool_rejects_funding_rate_above_the_maximum
  - operator_on_the_lighter_side_earns_only_the_fixed_rate: a wallet the
    operator controls holds a short against a larger long for an hour at the
    maximum rate, and is paid exactly the fixed rate.
- The funding tests now run at the maximum rate. The liquidity-inflation test
  holds its position for ten years at that rate instead of 1,000 seconds at an
  uncapped one.

Claude-Session: https://claude.ai/code/session_01JGEoAUjMm7Evv69k46eNcn
@mikemaccana
mikemaccana merged commit efcfadf into main Sep 30, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant