Skip to content

feat(token-swap): add pinocchio example - #716

Open
MarkFeder wants to merge 2 commits into
solana-foundation:mainfrom
MarkFeder:tokens-token-swap-pinocchio
Open

feat(token-swap): add pinocchio example#716
MarkFeder wants to merge 2 commits into
solana-foundation:mainfrom
MarkFeder:tokens-token-swap-pinocchio

Conversation

@MarkFeder

Copy link
Copy Markdown
Contributor

Adds a Pinocchio implementation of token-swap, alongside the existing Anchor one.

What it does

A constant-product AMM. Five instructions: CreateAmm, CreatePool, DepositLiquidity, WithdrawLiquidity and SwapExactTokensForTokens.

Everything a pool owns hangs off a single authority PDA — both vaults and the LP mint — so the program can move pool funds without any wallet holding that power.

The parts worth reading

  • Deposits are forced to the pool's ratio. Whichever side is short decides how much of the other is actually taken, so a depositor cannot move the price by adding lopsided amounts. LP shares are the geometric mean of the deposit, so they track the pool's value rather than either balance.
  • MINIMUM_LIQUIDITY is burned on the first deposit and never minted to anyone. It keeps the pool from being emptied completely, which is what would otherwise let the share price be skewed while the pool is near-empty. Withdrawals divide by supply + MINIMUM_LIQUIDITY for the same reason.
  • The invariant is re-checked after the CPIs. a * b is read again from the vaults after the transfers; a higher value is fine (rounding in the pool's favour), a lower one aborts.
  • Withdrawal burns last. An over-large amount fails at the burn and rolls the two transfers back, so the pool cannot be drained by asking for more than you hold.

All the arithmetic goes through a mul_div helper that widens to u128, so the product of two u64 balances cannot overflow.

Account binding

The pool records its AMM and both mints. PoolSeeds::load reads them back, checks the supplied mints against the stored ones, and rederives both the pool and the authority — so a caller cannot pair a real pool with unrelated token accounts, or point a pool at a cheaper AMM's fee. There is a test for the mint substitution.

Differences from the Anchor version

  • No 8-byte account discriminators, so the AMM is 66 bytes and the pool 96.
  • Instruction data is packed by hand rather than Borsh; swap_a is a u8.
  • PDA creation goes through the transfer/allocate/assign helper rather than a bare CreateAccount, so a stray lamport on a derivable address cannot block pool creation (see feat(merkle-tree-token-claimer): add pinocchio example #714).

Tests

10 LiteSVM tests: the full lifecycle from AMM through pool, deposit, swap, ratio-trimmed deposit and withdrawal, plus fee, slippage and mint-substitution rejections. The swap test recomputes the expected output from the curve in the test rather than asserting a hardcoded number. Verified locally: tsc --noEmit, pnpm test, prettier --check, cargo fmt --check, cargo clippy -D warnings.

@MarkFeder
MarkFeder requested a review from dev-jodee as a code owner August 31, 2026 14:04
@greptile-apps

greptile-apps Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds a Pinocchio implementation of the token-swap example, including AMM and pool creation, liquidity management, swaps, tests, and workspace integration. The latest account-binding changes rederive the pool authority, canonical reserve accounts, and liquidity mint before those accounts participate in settlement.

  • Adds five on-chain token-swap instruction handlers and shared PDA/account validation.
  • Adds LiteSVM lifecycle, slippage, fee, ratio, and account-substitution tests.
  • Adds Rust workspace, package, CI, and README integration.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the current canonical reserve and liquidity-mint checks address all three previously reported account-substitution paths before pricing, share accounting, or settlement.

Important Files Changed

Filename Overview
tokens/token-swap/pinocchio/program/src/instructions/mod.rs Centralizes pool validation and now binds both reserve accounts and the LP mint to their canonical pool-derived addresses.
tokens/token-swap/pinocchio/program/src/instructions/swap_exact_tokens_for_tokens.rs Performs fee-aware constant-product swaps after validating both canonical pool reserves, resolving the previously reported reserve-substitution path.
tokens/token-swap/pinocchio/program/src/instructions/deposit_liquidity.rs Validates canonical reserves and the pool LP mint before reading balances, transferring deposits, or minting shares.
tokens/token-swap/pinocchio/program/src/instructions/withdraw_liquidity.rs Validates the canonical LP mint before using its supply and releasing proportional reserves.
tokens/token-swap/pinocchio/tests/test.ts Covers the full pool lifecycle and verifies rejection of substituted reserves, counterfeit liquidity mints, and mismatched pool mints.

Reviews (2): Last reviewed commit: "token-swap: bind the pool vaults and liq..." | Re-trigger Greptile

Comment thread tokens/token-swap/pinocchio/program/src/instructions/deposit_liquidity.rs Outdated
PoolSeeds::load rederived the pool and its authority but never checked
the two vaults, and neither deposit nor withdraw checked the liquidity
mint. Anchor gets those from associated_token::mint/authority and the
mint's seeds constraint; dropping them in the port left three ways to
drain a pool:

  - swap with a zero-balance stand-in for the paying reserve prices the
    trade against nothing and empties the opposite vault;
  - deposit into caller-controlled reserves still mints genuine LP
    shares, redeemable against the real ones;
  - withdraw against a caller-created LP mint sets the entitlement to
    whatever they like.

Rederive both vaults as the authority's associated token accounts, and
the liquidity mint from its seeds. Each vector has a test verified to
succeed without the checks.
@MarkFeder

Copy link
Copy Markdown
Contributor Author

@amilz could you take a look at this one when you get a chance?

No open review threads left on it, so it is ready for maintainer review. It is one of 23 open Pinocchio ports I have up — they are independent and self-contained, so they can be reviewed and merged in any order: https://github.com/solana-developers/program-examples/pulls/MarkFeder

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