Repository navigation
Conversation
There was a problem hiding this comment.
Every commit in a Solana Foundation repository needs signature verification, and these are unverified:
80799d2feat(tokens): add native flavor for tokens/token-swap (Fixes #755)
Set up signing, then sign the existing commits (git rebase --exec 'git commit --amend --no-edit -S' main) and force-push. This review is dismissed automatically once every commit verifies.
|
Hi @sehaha, Thanks for the pull request. We ask that every change references an issue a maintainer has accepted, and there isn't one linked here. These examples are a teaching resource and each one has to stay small enough to read in one sitting, so whether a new example or framework flavor belongs here is worth settling before anyone writes it. Agreeing on the approach in an issue first means nobody writes code that can't be merged. See CONTRIBUTING.md for the full workflow. Please open an issue describing the change, or comment on the existing one. Once a maintainer has labelled it |
[Medium impact] The new program affects the token-swap example and pools created through it. This PR should not merge until the reserve-theft paths, locked-liquidity calculation, and deploy path are fixed. Findings
SummaryAdds a native Rust token-swap example with five instructions, TypeScript clients, LiteSVM tests, and build scripts.
Reviews (1) · Last reviewed commit: "feat(tokens): add native flavor for toke..." · Reviewed by Greptile |
| let mint_lp_state = MintState::unpack(&mint_liquidity.data.borrow())?; | ||
|
|
||
| let amount = if args.amount > dep_lp_state.amount { dep_lp_state.amount } else { args.amount }; | ||
| let amount_u128 = amount as u128; | ||
| let pool_mint_supply_u128 = mint_lp_state.supply as u128; |
There was a problem hiding this comment.
Unrelated tokens drain both reserves
mint_liquidity is never checked against the pool's LP mint address. An attacker can pass an unrelated SPL mint whose entire supply they hold, alongside the genuine pool and reserves. Burning that supply makes both withdrawal amounts equal the full reserve balances, and pool_authority signs both payments. Check the expected LIQUIDITY_SEED address before reading the supply or burning tokens.
How this was verified: The withdrawal divides by the caller's mint supply, burns tokens from that same mint, and signs transfers from the genuine reserves.
Knowledge Base Used: Token escrow, swaps, and fundraising
| let pool_a_state = TokenAccountState::unpack(&pool_account_a.data.borrow())?; | ||
| let pool_b_state = TokenAccountState::unpack(&pool_account_b.data.borrow())?; |
There was a problem hiding this comment.
Replacement reserves allow token theft
Swaps and deposits never check that pool_account_a and pool_account_b are the pool's reserve accounts. This allows two ways to take genuine pool assets:
- For an A-to-B swap, an attacker supplies their empty A account and the genuine B reserve. Positive input buys the entire B balance. Both reserve products are zero, so the final check passes.
- For a deposit, transfers between attacker-owned accounts can mint genuine LP tokens without funding the pool.
Check both reserve addresses, mints, and authority before using their balances or moving tokens.
How this was verified: Caller-selected reserves feed the swap price and deposit amounts, while the genuine pool authority signs the output transfer or LP mint.
Knowledge Base Used: Token escrow, swaps, and fundraising
|
|
||
| let amount = if args.amount > dep_lp_state.amount { dep_lp_state.amount } else { args.amount }; | ||
| let amount_u128 = amount as u128; | ||
| let pool_mint_supply_u128 = mint_lp_state.supply as u128; |
There was a problem hiding this comment.
The withdrawal denominator drops MINIMUM_LIQUIDITY, unlike the Anchor version. A first deposit of 1,000,000 units per asset mints 999,900 LP tokens. Burning all 999,900 now returns all 1,000,000 units of each asset, leaving nothing locked. Include MINIMUM_LIQUIDITY in the denominator, or mint permanently locked shares and account for them consistently.
| let pool_mint_supply_u128 = mint_lp_state.supply as u128; | |
| let pool_mint_supply_u128 = mint_lp_state.supply as u128 + MINIMUM_LIQUIDITY as u128; |
Knowledge Base Used: Token escrow, swaps, and fundraising
| "test": "mocha --import=tsx -t 1000000 ./tests/test.ts", | ||
| "build-and-test": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./tests/fixtures && pnpm test", | ||
| "build": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./program/target/so", | ||
| "deploy": "solana program deploy ./program/target/so/program.so" |
There was a problem hiding this comment.
Deploy uses the wrong filename
deploy points to program.so, but this crate builds token_swap_native_program.so, which is also the filename the tests load. Running pnpm build followed by pnpm deploy therefore fails to find the program. Update the deploy path to match the build output.
| "deploy": "solana program deploy ./program/target/so/program.so" | |
| "deploy": "solana program deploy ./program/target/so/token_swap_native_program.so" |
| programId: programId.address, | ||
| })); | ||
|
|
||
| console.log("Token swap executed successfully!"); |
There was a problem hiding this comment.
The new deposit, swap, and withdrawal test checks only that transactions succeed. It never checks token balances or LP supply. Incorrect payments or missing transfers can therefore pass. Assert the user and reserve balances after each action, including the fee-adjusted swap output and remaining locked liquidity.
| // Verify Pool PDA | ||
| let (expected_pool_key, pool_bump) = Pubkey::find_program_address( | ||
| &[amm_account.key.as_ref(), mint_a.key.as_ref(), mint_b.key.as_ref()], |
There was a problem hiding this comment.
process_create_pool accepts any account as amm_account and stores its key without checking that it is an initialized Amm owned by this program. Passing a normal wallet creates a pool that accepts deposits, but every swap fails when it tries to decode that wallet as Amm. Check the account owner, data, and expected address before creating the pool.
Knowledge Base Used: Token escrow, swaps, and fundraising
🛡️ Security Hardening & Greptile Audit Resolution Update (Commit
|
Linked issue
Fixes #755
What and why
Adds
tokens/token-swap/native/— providing the pure Solana Native Rust implementation of the token swap pattern, maintaining full parity with the existinganchor/variant.tokens/token-swap/native/program/:CreateAmm: AMM account initialization and fee constraint validation (fee < 10000).CreatePool: PDA-derived pool, authority, LP token mint (decimals 6), and dual-sided ATAs.DepositLiquidity: Proportional reserve checks, initial liquidity burn (MINIMUM_LIQUIDITY = 100), and LP token minting.WithdrawLiquidity: Proportional LP token burning and dual-sided asset redemption.SwapExactTokensForTokens: Constant-product invariant checkmin_output_amount), and fee accounting.Cargo.toml.tokens/token-swap/native/tests/:@solana/kitandlitesvm 1.x.tokens/token-swap/native/package.json:build-and-testscript and standalone locked dependencies.tokens/token-swap/native/cicd.sh:Testing
Verified with:
cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./tests/fixtures pnpm testResult: 2/2 passing tests in 28ms.
AI disclosure