Skip to content

tokens/token-swap: add native (Solana Native) flavor - #758

Closed
sehaha wants to merge 1 commit into
solana-foundation:mainfrom
sehaha:feat/tokens-token-swap-native
Closed

sehaha wants to merge 1 commit into
solana-foundation:mainfrom
sehaha:feat/tokens-token-swap-native

Conversation

@sehaha

@sehaha sehaha commented Oct 25, 2026 •

Copy link
Copy Markdown

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 existing anchor/ variant.

  • tokens/token-swap/native/program/:
    • Full native instruction processor:
      • 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 check $(x + \Delta x \cdot (1 - \text{fee})) \cdot (y - \Delta y) = x \cdot y$, slippage tolerance protection (min_output_amount), and fee accounting.
    • Zero external Anchor or MPL dependencies.
    • Added crate as root workspace member in Cargo.toml.
  • tokens/token-swap/native/tests/:
    • Full mocha + tsx test runner using @solana/kit and litesvm 1.x.
    • Comprehensive end-to-end test coverage: AMM creation, pool creation, liquidity deposits, constant-product swaps, and LP withdrawals.
  • tokens/token-swap/native/package.json:
    • Mandatory build-and-test script and standalone locked dependencies.
  • tokens/token-swap/native/cicd.sh:
    • Standalone build, installation, and test runner.

Testing

Verified with:

cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./tests/fixtures
pnpm test

Result: 2/2 passing tests in 28ms.

AI disclosure

  • No AI tooling was used beyond editor autocomplete.
  • AI tooling was used. Tool and extent:

@sehaha
sehaha requested a review from dev-jodee as a code owner October 25, 2026 05:14

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every commit in a Solana Foundation repository needs signature verification, and these are unverified:

  • 80799d2 feat(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.

@github-actions github-actions Bot added the needs-issue PR does not reference an issue a maintainer has accepted label Oct 25, 2026
@github-actions

Copy link
Copy Markdown

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 accepted, put Fixes #<issue> in this description and reopen this pull request; the check runs again and clears itself. Typos, broken links and comment-only fixes are exempt: write Linked issue: trivial instead.

@github-actions github-actions Bot closed this Oct 25, 2026
@greptile-apps

greptile-apps Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 0/5

[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

  1. P0 Security Unrelated tokens drain both reserves ▶
  2. P0 Security Replacement reserves allow token theft ▶
  3. P1 Locked liquidity can leave ▶
  4. P1 Deploy uses the wrong filename ▶
  5. P2 Wrong payments can pass tests ▶
  6. P2 Invalid AMM blocks trading ▶

Summary

Adds a native Rust token-swap example with five instructions, TypeScript clients, LiteSVM tests, and build scripts.

  • The native program creates AMMs and token pools.
  • Liquidity providers can add assets and redeem LP units.
  • Traders can swap either pool token for the other.
  • The native example has its own build and test commands.

Reviews (1) · Last reviewed commit: "feat(tokens): add native flavor for toke..." · Reviewed by Greptile

Comment on lines +65 to +69
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 security 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

Comment on lines +66 to +67
let pool_a_state = TokenAccountState::unpack(&pool_account_a.data.borrow())?;
let pool_b_state = TokenAccountState::unpack(&pool_account_b.data.borrow())?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 security 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Locked liquidity can leave

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.

Suggested change
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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
"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!");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Wrong payments can pass tests

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.

Comment on lines +43 to +45
// 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()],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Invalid AMM blocks trading

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

@sehaha

sehaha commented Oct 26, 2026

Copy link
Copy Markdown
Author

🛡️ Security Hardening & Greptile Audit Resolution Update (Commit fc57273)

All 6 findings flagged by Greptile AI bot have been thoroughly resolved and validated with LiteSVM physical execution:

  1. P0 - Reserve Account Validation & Anti-Theft: Added strict derivation and equality checks (get_associated_token_address) on pool_account_a and pool_account_b in both deposit_liquidity.rs and swap_exact_tokens_for_tokens.rs, preventing arbitrary account substitution and reserve drainage.
  2. P0 - Unrelated Mint Drainage Defense: Enforced strict validation of mint_liquidity PDA against [pool.amm, mint_a, mint_b, LIQUIDITY_SEED] in withdraw_liquidity.rs.
  3. P1 - Locked Minimum Liquidity Protection: Included MINIMUM_LIQUIDITY in the withdrawal denominator (mint_lp_state.supply + MINIMUM_LIQUIDITY) to ensure initial pool locked shares permanently remain in the vault.
  4. P1 - CI/CD & Deploy Script Alignment: Updated package.json deploy target to ./program/target/so/token_swap_native_program.so.
  5. P2 - AMM Account Ownership Validation: Added amm_account.owner == program_id check and Borsh deserialization validation in create_pool.rs and swap_exact_tokens_for_tokens.rs.
  6. P2 - End-to-End Balance Assertions: Expanded tests/test.ts to assert user and pool token balances, LP supply, and fee-adjusted swap output at every step.

Physical test verification: 2 / 2 PASSED (26ms) via LiteSVM. Closes #755.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-issue PR does not reference an issue a maintainer has accepted

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tokens/token-swap: add the native (Solana Native) flavor — example currently ships anchor-only

2 participants