
Preparing a Solana AMM for an Audit and a Jupiter Review
18 min
On this page▾
TL;DR
- Jupiter's AMM integration guide asks for an implementation of the
Ammtrait, an SDK Jupiter can fork, no network calls anywhere in that implementation, quote parity proven withjupiter-amm-test-kit, and a security audit (checked 9 Nov 2026). - The test kit compares
Quote::out_amountwith the balance change of the user's destination token account after your program's own swap instruction runs in LiteSVM. One unit off and the test fails. - A green run covers only the swaps you registered, at one pinned clock, against whatever
.sofile sits in your fixtures folder. It does not check the input side beyond exact funding, and it never exercises your minimum-out check. - The sections marked Zealynx suggestion are our checks, not Jupiter's. Jupiter does not ask for them.
- The one-page sheet near the end is free to copy. There is no form.
The test kit gives your quote no slack at all. It creates the user's token accounts, writes exactly the swap amount into the source account, calls your
Amm::quote, sends your program's native swap instruction through LiteSVM and then compares one number: quote.out_amount against how much the destination account actually received (parity.rs, lines 129 to 195).We changed a single function in a local copy of the kit's own reference suite, so that the SDK folded the trade fee and the owner fee into one fee and floored it once. The two round-number swaps that suite registers still passed. Swaps of 333 and of 123,456,789 base units of USDC failed. Details are further down.
The kit catches exactly the swaps you ask it about, and no others. And if the audit report points at one commit while the parity suite ran against a
.so built from another, your submission tells two stories about one program. The sections below keep Jupiter's published requirements apart from our suggestions, so the auditor and the integration reviewer end up looking at the same artifact.Nothing here is endorsed by Jupiter or the Solana Foundation. Jupiter's guide is the source of truth, so read it again before you submit. For the general audit preparation (documentation, tests, code freeze, scope) use our pre-audit checklist and the audit preparation guide. For program-level Solana checks such as signer, owner, PDA and CPI validation, use the Solana security checklist. This page covers only what an automated market maker needs on top of that before an aggregator routes to it.
What Jupiter publishes
Read on 9 November 2026 from the integration guide and its Market Listing page. The middle column is Jupiter's, paraphrased closely; follow the links for the exact wording.
| Area | What Jupiter's guide says | Section |
|---|---|---|
| What it weighs | Code health, a security audit, traction, and the team and backers. | Integration Prerequisites |
| Interface | Implement the Amm trait from the jupiter-amm-interface crate. The jup-ag/jupiter-amm-interface repository is the source of truth for the trait and its guidance. | Step 1 |
| Forkable SDK | Jupiter must be able to fork your SDK, so that it can maintain it and fix integration bugs. | Step 1 |
| Scope of the implementation | Limit the Amm implementation to deserializing state and quoting. Keep the detailed math in your SDK. Do heavy deserialization and precomputation in update, not in quote. | Step 1 |
| No network calls | Accounts named by get_accounts_to_update are fetched, batched and cached by Jupiter and delivered through update. quote may be called many times on the same cache, so "we do not allow any network calls" anywhere in the implementation. | Step 1, note |
| Quote parity | Before submitting, prove with jupiter-amm-test-kit that your quotes match what the on-chain program executes, down to the exact token deltas. | Step 2 |
| What the kit covers | ExactIn swaps that run as a single top-level instruction with one signer. If a documented limitation blocks you, open an issue on the repository instead of working around it silently. | Step 2 |
| Submission | Through the AMM integrators support form, with a link to the SDK repository (parity suite and committed fixtures included, fork access granted), your security audit, traction metrics and information on the team and backers. | Step 3 |
| After integration | Markets must keep meeting the liquidity criteria to stay routable. Under normal routing each market is re-checked every 30 minutes and must pass one of two tests: less than 30% price difference on a $500 buy and sell back, or less than 20% price impact between a $1000 buy and a $500 buy. | Market Listing |
| Frontend flow (optional) | Swaps from the Jupiter frontend are signed by sighWH8KaiT7QhtV4w29ReVF8kG6D5yG3EQP1KYyGVF. A propAMM that prices this flow differently should verify that signature, not the mere presence of the address. | More |
The guide does not say which auditors it accepts, what the audit must cover or whether the SDK has to be in its scope. We don't know either, so the sheet below asks you to state it rather than leave the reviewer to guess.
What the test kit checks, read from its code
Everything in this section comes from jup-ag/jupiter-amm-interface at commit
70f56e0 (22 Oct 2026), the head of main on 9 November. On that day we ran the reference suite, cargo test -p jupiter-amm-test-kit --test spl_token_swap, offline from the committed fixtures, and it passed.Funding.
create_funded_ata creates each user token account through the Associated Token Account program, so Token-2022 account extensions are initialized as a real user would have them, then writes the swap amount straight into the source account (svm.rs, lines 82 to 126). The source holds exactly amount, so a program that pulls more than the input fails the transaction.The assertion. There is one
assert_eq!, between quote.out_amount and the destination balance change (parity.rs, lines 190 to 195). quote.in_amount, fee_amount and fee_mint are never compared. A program that takes less than the quoted input would still pass.The instruction. Your encoder closure turns the
Swap::Placeholder { data } your Amm returned, plus the amount, into native instruction data. The kit sends it as the only instruction after a compute budget instruction that sets the limit to 1,400,000 units (parity.rs, lines 173 to 187). Compute usage is not measured.Minimum out. The reference encoder for SPL Token Swap writes
minimum_amount_out as 0 and says in its comment that asserting the exact output is the kit's job (spl_token_swap.rs, lines 47 to 65). Copy that pattern and the parity run tells you nothing about your on-chain slippage check.The clock. The SVM takes its clock from the snapshot's clock sysvar, and the
AmmContext handed to from_keyed_account is built from the same snapshot (parity.rs, lines 94 to 96). Every swap in a run sees one moment in time.The program under test. Your program loads from
tests/fixtures/<label>.so, where the label is Amm::label() lowercased with spaces turned into underscores. Programs you CPI into come from Amm::program_dependencies() (svm.rs, lines 30 to 56). When fixtures are regenerated with RPC set, a missing .so is dumped from chain but an existing one is kept, so a local build is never replaced by the deployed program (rpc.rs, lines 128 to 137).State discovery. The snapshotter asks
get_accounts_to_update again after each of three update rounds, so accounts that only appear once the pool state is read, tick arrays for example, get captured (rpc.rs, lines 45 to 52). Before quoting, update runs three times over the snapshot (snapshot.rs, lines 84 to 89).The README's limitations list the rest: ExactIn only, one native instruction per swap, a plain top-level instruction (no instructions-sysvar gate that only accepts CPIs from approved callers), account metas used verbatim in on-chain order, one signer, freshly created token accounts where transfer logic needing extra per-account state such as transfer-hook side accounts may not work, and default values for the flow type and referrer. The README calls these the common case for now rather than hard design limits.
So a green run shows one thing. For the swaps you registered, against that snapshot and that
.so, the output matched.One rounding rule, two results
The SPL Token Swap math that the reference suite vendors floors the trade fee and the owner fee separately, and charges at least one unit for any non-zero fee rate (
calculate_fee, spl_token_swap.rs, lines 77 to 84). An SDK that folds both fees into one rate and floors once is right for most inputs and wrong for some. The rounding direction of each step matters, not only the formula.A synthetic pool shows where. Reserves of 1,000,000,000 and 2,000,000,000 base units, a 0.25% trade fee and a 0.05% owner fee, computed by us with that math:
| Input | On-chain fees (trade + owner) | SDK fee (combined 0.30%) | On-chain output | SDK quote |
|---|---|---|---|---|
| 333 | 1 + 1 | 0 | 661 | 665 |
| 1,999 | 4 + 1 | 5 | 3,987 | 3,987 |
| 1,000,000 | 2,500 + 500 | 3,000 | 1,992,013 | 1,992,013 |
| 123,456,789 | 308,641 + 61,728 | 370,370 | 219,193,140 | 219,193,139 |
Then we ran the same mistake through the real kit. In a local copy of
spl_token_swap.rs we replaced only the SDK's fee arithmetic with the combined-fee version and registered six single-swap tests against the kit's committed SOL/USDC fixture. Both reference swaps (0.1 SOL in, 50 USDC in) passed, and so did 333 and 123,456,789 lamports of SOL in. Both USDC-side failures were exact:| Swap (base units in) | SDK quote | On-chain output | Result |
|---|---|---|---|
| 333 USDC to SOL | 4,506 | 4,479 | failed |
| 123,456,789 USDC to SOL | 1,662,262,210 | 1,662,262,223 | failed |
The kit did its job on both. It could only do it because those amounts were registered.
Zealynx suggestion: checks to add before either review
Everything from here to the sheet is ours. Jupiter's guide does not ask for it.
Quote parity beyond the default run
- Register swaps in both directions at dust sizes, at the amounts where your fee and price rounding changes, and close to the largest trade the pool can fill. Cover every pool type and fee mode you ship; the kit's own crate docs ask for as many pool types and swap paths as possible (
lib.rs, lines 12 to 13). - Write one test of your own, outside the kit, that checks the input side: the source account ends at
amount - quote.in_amountand the pool vaults move by what your quote implies. - Test the minimum-out check on its own. The same swap with
minimum_amount_outset toquote.out_amount + 1must fail on chain. - If price or fees depend on time (dynamic fees, decaying volatility, oracle staleness windows), commit snapshots taken at more than one moment and point extra
PoolTests at them throughfixtures_dir. - If either mint carries a
TransferFeeConfig, the amount that arrives is reduced by the fee (Solana docs, transfer fees). The kit reads the destination's raw balance, so your quote has to be net of that fee. Transfer hooks need their own review; our Solana audit guide for 2026 covers them. - Record the compute units each swap path uses. Implement
get_accounts_lenas well: the trait's default returns 32, and its comment says that default exists to penalize AMMs that don't implement it (interfacelib.rs, lines 250 to 252).
The SDK itself
- No network calls is Jupiter's rule. We would apply the same idea to anything else that makes
quoteorupdatenon-deterministic: no host system time, no randomness, no file reads. Take time from theClockRefthe interface passes inAmmContext. quotereturnsResult<Quote, AmmError>. Return an error for empty reserves or overflow instead of panicking.- Pin the interface version. At
70f56e0the repository's interface crate is 0.7.0,QuoteParamscarriesflow_type,AmmrequiresClone, andSwap::Placeholderexists. The newest releases on crates.io, 0.6.1 and 1.0.0-beta.0, still havefee_modeand aclone_ammmethod, and neither hasSwap::Placeholder(checked 9 Nov 2026). The test kit is not published on crates.io, and its ownCargo.tomltakes the interface by path. Build the SDK and the parity suite against one git revision of the repository and name that revision in your submission. - Keep the committed
Cargo.lock. The kit's README warns against runningcargo updateonsolana-clockand related sysvar crates past 3.2.0 without moving LiteSVM too. - Make the fork boring: a licence file, a build that works from a clean clone, no private git dependencies, and fixtures committed so the suite runs offline.
Accounts and authority in the swap instruction
SwapParams::token_transfer_authorityis documented as either the user or a program authority over the source token account (interfacelib.rs, lines 108 to 109). Don't write the on-chain swap as if the signer were always the end user.- Validate every pool-side account the instruction receives: pool state, vaults, mints, fee accounts, token program. In Anchor,
has_oneties a key stored in the pool state to the account passed in,address =pins a known key,owner =checks the owning program, andSigner<'info>requires a signature (Anchor account constraints). The Solana security checklist has the full set of signer, owner and PDA checks. - If the program only accepts CPIs from approved callers, the kit cannot drive it yet. Say so in the submission and open the issue Jupiter asks for.
- If you quote Jupiter frontend flow tighter, grant the better price only on a path where the frontend address arrives as a
Signer<'info>with anaddress =constraint, never because the key is present. Jupiter's guide spells out why: anyone can add the address to a transaction, and only Jupiter can sign for it.
Upgrade authority and which program was tested
The Solana security checklist already covers recording the program ID and upgrade authority and matching the deployed program to a reproducible build (Solana docs, verified builds). Add the fixture
.so to that comparison:1solana program show <PROGRAM_ID> # Authority, ProgramData address, last deployed slot2solana-verify get-program-hash -u <RPC_URL> <PROGRAM_ID> # what is deployed3solana-verify get-executable-hash tests/fixtures/<label>.so # what the parity suite ran4solana-verify get-executable-hash target/deploy/<lib>.so # built from the audited commit with solana-verify build
solana-verify strips trailing zero bytes before it hashes (get_binary_hash, solana-verifiable-build), so a padded ELF dumped from chain and a local build compare cleanly. If the hashes disagree, the parity suite tested a different program from the one deployed or the one audited, and the sheet should say which.Note who holds the upgrade authority and what happens after an upgrade: delete the stale
.so, refresh the fixtures with REFRESH=1 RPC=<url>, rerun parity and tell the integrator. Solana's docs note that once solana program set-upgrade-authority --final makes a program immutable, it can never be updated or closed (Solana docs, deploying programs).The audit evidence
Jupiter asks for your security audit and does not define its scope. Tie the report to the same revision as everything above: the reviewed commit, the hash of the build from that commit, the deployed hash and the SDK commit used for parity. State whether the SDK's quote code was in the audit's scope. If it was not, the quote math has been checked only by your own tests, the parity swaps you registered among them.
The one-page sheet
Zealynx suggestion. This is not a Jupiter form, an application or a certificate. Tags in brackets show where each line comes from. Fill it with public material only, never keys or secret configuration, and keep it in your repository (as
INTEGRATION.md, say) so the auditor and the integrator read the same file.1# <AMM name>: audit and integration sheet2Last updated: YYYY-MM-DD Owner: <name, role>34## Target [Zealynx suggestion]5- Program ID(s) and network:6- Audited commit:7- Deployed hash (solana-verify get-program-hash):8- Audited build hash (solana-verify get-executable-hash):9- Fixture .so hash (tests/fixtures/<label>.so):10- All equal? yes / no, and why:1112## Authority [Zealynx suggestion]13- Upgrade authority (solana program show): keypair / multisig / none14- Other privileged roles and what each can change:15- After an upgrade we: refresh fixtures, rerun parity, notify integrators1617## SDK [Jupiter: Amm trait, forkable, no network calls]18- Repository and commit:19- jupiter-amm-interface git revision:20- Fork access granted to Jupiter: yes / no21- Network calls in the Amm implementation: none (evidence):22- Work done in update vs quote:2324## Parity [Jupiter: test kit, committed fixtures]25| Pool | Direction | Amount | Why this amount | Passed at commit |26|------|-----------|--------|-----------------|------------------|27| | | | | |28- Own tests beyond the kit [Zealynx suggestion]:29 input side / min-out fails at quote + 1 / compute units per path / snapshots at other times30- Kit limitations that apply to us:31- Issues opened on jup-ag/jupiter-amm-interface:3233## Audit [Jupiter: security audit]34- Report link, auditor, reviewed commit:35- SDK quote code in scope: yes / no36- Findings fixed, and fix-review evidence:37- Changes since the reviewed commit:3839## Project [Jupiter: traction, team and backers]40- Traction metrics, with dates and source:41- Team and backers (publicly verifiable):4243## Open questions44- Question / owner / internal target date (not an agreed date):
Use the sheet as it is. If something on this page no longer matches Jupiter's guide or the kit, or you have a precise scope question about your own AMM, write to [email protected].
FAQ
1. Does passing the Jupiter test kit mean my AMM will be integrated?
No. The guide lists code health, a security audit, traction, and the team and backers among the factors Jupiter weighs, and after integration each market has to keep passing the Market Listing liquidity criteria to stay routable. A passing parity suite is one item in the submission, not the decision.
2. What exactly does jupiter-amm-test-kit compare?
At commit
70f56e0 it compares quote.out_amount with the balance change of the user's destination token account after your program's native swap instruction runs in LiteSVM. The source account is funded with exactly the input amount, so a program that pulls more fails. The quoted input and fee fields are not compared, and the swap runs at the snapshot's clock.3. Can a Jupiter AMM SDK make RPC calls?
No. Jupiter fetches the accounts your
get_accounts_to_update names, caches them and passes them in through update, and the guide does not allow network calls anywhere in the Amm implementation. Put heavy deserialization and precomputation in update and keep quote a pure calculation over that cached state.4. Does the security audit have to cover the SDK?
Jupiter's guide asks for your security audit without defining its scope. We suggest stating plainly whether the quote code was reviewed and tying the report to the same commit and build hash as the parity suite, so a reviewer doesn't have to work it out.
5. Does the test kit support ExactOut swaps or Token-2022 mints?
ExactOut is not asserted yet. Token-2022 mints work because the kit creates the user's accounts through the Associated Token Account program, but transfer logic that needs extra per-account state, transfer-hook side accounts for example, may not. If a limitation blocks you, Jupiter asks you to open an issue on the repository rather than work around it.
Glossary
| Term | Definition |
|---|---|
| Automated Market Maker | A protocol that prices trades from pool reserves and a formula instead of an order book. |
| Exact Input Swap | A swap where the input amount is fixed and the output is whatever the pool returns. The only mode the test kit asserts. |
| Slippage | The gap between the quoted and executed price, bounded on chain by a minimum-out check. |
| Rounding Direction | Whether each division rounds up or down, and in whose favour. A one-unit difference fails quote parity. |
| Token-2022 | Solana's token program with extensions such as transfer fees and transfer hooks. |
| Program Derived Address (PDA) | An address derived from seeds and a program ID that only that program can sign for. |
Sources
All checked 9 November 2026.
- Jupiter, Integrate AMM into Metis and Market Listing.
- jup-ag/jupiter-amm-interface at
70f56e0:test-kit/README.md,test-kit/src/parity.rs,test-kit/src/svm.rs,test-kit/src/snapshot.rs,test-kit/src/rpc.rs,test-kit/src/lib.rs,test-kit/tests/spl_token_swap.rs,interface/src/lib.rs,interface/Cargo.toml,test-kit/Cargo.toml. - crates.io, jupiter-amm-interface versions (0.6.1 and 1.0.0-beta.0 inspected) and a search for
jupiter-amm-test-kit(not published). - Solana docs source at
31f66e0:programs/deploying.mdx,programs/verified-builds.mdx,tokens/extensions/transfer-fees.mdx. - Ellipsis-Labs/solana-verifiable-build at
ea53dce,src/main.rs. - Anchor, account constraints reference.
Working auditors in your corner, all year
Zealynx Insiders: weekly live sessions, 1:1 advisory, pair-auditing, and Krait runs on your code, from an audit firm that publishes its reports.
No spam. Unsubscribe anytime.
Building a protocol? Start with a three-day pre-audit, credited toward your audit →
Keep reading


