Skip to content

PM-AMM hardening: resolve all audit findings#29

Open
rndrntwrk wants to merge 20 commits intodevelopfrom
feature/pm-amm-hardening-v1
Open

PM-AMM hardening: resolve all audit findings#29
rndrntwrk wants to merge 20 commits intodevelopfrom
feature/pm-amm-hardening-v1

Conversation

@rndrntwrk
Copy link
Collaborator

Summary

  • Resolve all critical, high, medium, and governance audit findings for the LVR AMM across EVM and Solana
  • Cherry-pick AMM-specific code from PR feat: AMM Protocol Swap Fees #19, apply security hardening, pull deploy tooling
  • 57/57 EVM tests pass (PM-core + AMM suite)

Changes

EVM (Router + LvrMarket):

  • AccessControl + ReentrancyGuard on Router (C-2, H-1)
  • Market allowlist for callbacks (H-2)
  • Slippage protection via minAmountOut (H-3)
  • Fee cap MAX_FEE_BPS=1000 (H-4)
  • isDynamic immutable state var (C-1)
  • Price read/execution parity using time-decayed liquidity (G-7)
  • settleFromOracle() reads DuelOutcomeOracle for trustless resolution (G-4)
  • Z-score bounds validation in SwapMath (M-1)

Solana (lvr_amm program):

  • f64 → Q64.64 fixed-point integer math (C-3)
  • AmmConfig PDA for protocol-wide fee/treasury/pause (G-2, G-6)
  • Buy/sell pause guards via AmmConfig (G-5)
  • Oracle adapter: settle_bet reads fight_oracle DuelState (G-4)
  • get_price uses time-decayed liquidity matching execution (G-7)
  • Re-initialization prevention on AmmConfig

Tooling:

  • deploy-amm.ts updated for hardened 4-param Router constructor
  • amm-test-helpers.ts updated for AmmConfig PDA flow

Test plan

  • forge test — 57/57 pass (9 AMM + 48 PM-core)
  • anchor build — Solana IDL regeneration
  • Solana integration tests with updated helpers

29 files from lvr_amm EVM contracts, Solana program, and test.
PM-core contracts (DuelOutcomeOracle, GoldClob, fight_oracle, gold_clob_market) unchanged.
C-1: declare isDynamic as immutable state variable in LvrMarket
C-2: add AccessControl + onlyRole(DEFAULT_ADMIN_ROLE) to setFeeConfig, 10% fee cap
H-1: add minAmountOut slippage protection to all buy/sell functions
H-2: add z-score bounds validation in SwapMath entry point
H-3: add ReentrancyGuard (nonReentrant) to all Router buy/sell functions
G-7: fix getPriceYes to use time-decayed liquidity matching _swap execution

Also: market allowlist on Router callbacks, 6 exploit regression tests
Rewrites entire math.rs to use Q64.64 fixed-point (i128) arithmetic.
Removes libm dependency entirely.

- Polynomial erf approximation (Abramowitz & Stegun 7.1.28)
- Taylor series exp(-x) with repeated squaring
- Integer sqrt via Newton's method
- Gaussian CDF/PDF from erf, not f64 stdlib
- All public API unchanged (u64 scaled by 1e6)
- 6 unit tests including symmetry and determinism verification

Solana validators will now produce identical results regardless of
hardware floating-point implementation.
…pplied fees (G-2, G-4, G-6)

- Add AmmConfig PDA (singleton): authority, treasury, market_maker, fee_bps, config_frozen, paused
- Add initialize_config, freeze_config, set_paused instructions
- create_bet now reads fee_bps and treasury from AmmConfig, not caller inputs
- Add pause guard on create_bet
- Add error variants: InvalidAddress, FeeTooHigh, MarketPaused, ConfigFrozen
- Removes per-market fee/treasury injection attack vector
Cherry-pick deploy-amm.ts and amm-test-helpers.ts from
feat/amm-swap-fees, updated for hardened interfaces:
- Router constructor now takes 4 params (mUSD, treasury, feeBps, admin)
- Solana create_bet reads fee/treasury from AmmConfig PDA
- Test helpers gain ensureAmmConfig() for protocol config setup
Thread AmmConfig PDA into Buy and Sell account contexts.
Both instructions now check !amm_config.paused before executing,
matching create_bet's existing pause guard and EVM Router parity.
EVM: settleFromOracle() reads DuelOutcomeOracle.getDuel() to resolve
market from oracle state. Handles Resolved and Cancelled statuses.

Solana: settle_bet accepts optional duel_state account from
fight_oracle. Winner deserialized from oracle at known byte offsets
rather than trusting caller side_won argument.
get_price now calls calc_liquidity for dynamic markets, matching
the execution path in buy/sell. Prevents price read/execution
divergence that could be exploited for arbitrage.
…tion

- Math.calcLiquidity: return 0 when currentTime >= deadline instead of
  reverting on uint underflow. getPriceYes/getPriceNo/getMarketDetails
  now work post-deadline.
- LvrMarket.getPriceYes: return neutral 0.5e18 when liquidity is zero.
- LvrMarket constructor: require(duration > 0) to prevent permanently
  locked zero-duration markets.

Fixes H3 (zero-duration lock) and H4 (post-deadline view revert).
- adminResolve: slash proposer bond to treasury when disputed and wrong,
  return bond only if proposer's outcome matches admin's resolution.
- sell: burn fee tokens instead of transferring to treasury. Prevents
  outstanding token supply from exceeding collateral backing (solvency
  invariant).
- constructor: require(duration > 0) to prevent zero-duration markets.
- getPriceYes: return neutral 0.5e18 when liquidity is zero post-deadline.

Fixes C5 (sell solvency), H2 (bond slashing), H3 (duration), H4 (price view).
Sell fees were transferred as outcome tokens to treasury ATAs, inflating
outstanding token supply relative to collateral. Now fees are burned,
maintaining the solvency invariant that total redeemable tokens never
exceed collateral in the bet PDA.

Treasury and treasury ATA accounts removed from the Sell instruction
since fee tokens are burned rather than transferred.

Fixes H7 (Solana sell solvency) and H5 (treasury ATA validation).
New tests:
- test_SellFeeBurnsSolvency: totalSupply <= 2*collateral after sells
- test_AdminSlashesBondOnWrongProposal: bond → treasury on wrong dispute
- test_AdminReturnsBondOnCorrectProposal: bond returned if proposer right
- test_ZeroDurationReverts: duration=0 rejected at creation
- test_GetPriceAfterDeadline: view functions return neutral, not revert

Also fixes two pre-existing test failures in redeem tests by using static
markets (avoids ZScoreOutOfBounds from dynamic liquidity scaling) and
approving both token types for the redeem callback.
anchor build -p lvr_amm to sync the IDL and TypeScript types with the
updated Sell instruction that no longer requires treasury/treasury ATA
accounts.
- Router: add allowedMarkets check to proposerOutcome, dispute, and
  settleMarket for consistency with buy/sell/settleFromOracle (M-NEW-1)
- LvrMarket.settleFromOracle: slash proposer bond when oracle outcome
  disagrees with proposal, matching adminResolve behavior (M-NEW-3)
- Solana Bet: add full 32-byte duel_key field. settle_bet compares all
  32 bytes when set, falling back to 8-byte bet_id match for backward
  compatibility (H-NEW-1)
- create_bet: accept duel_key parameter and store in Bet account
@rndrntwrk rndrntwrk self-assigned this Mar 21, 2026
@rndrntwrk rndrntwrk marked this pull request as ready for review March 21, 2026 09:46
@rndrntwrk rndrntwrk requested a review from lalalune March 21, 2026 09:46
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