RAZEX9pxDuRCrtwR5wxUPAX3pWwAkBzvM8hF2fKaRE9
This is the program the raze-router binary calls when it builds a swap. The
binary quotes and assembles routes off-chain; this program executes them on
chain, in one transaction, under one uniform envelope.
It holds no per-venue knowledge. Pool layouts and instruction encodings live
in the client, which hands over each hop as a fully-formed CPI
(program_id, accounts, data). Adding or updating a venue is therefore an
off-chain change plus an add_venue allowlist entry — never a redeploy.
The instance above is deployed and the router points at it out of the box, so the common case needs no configuration at all. But nothing binds the binary to our deployment: the router derives the config PDA, the swap-authority pool and the invoked program itself from whichever id it is given, so pointing it at your own copy is a one-line change, not a fork.
| variable | default | effect |
|---|---|---|
RAZE_ROUTER_PROGRAM |
this program id | the RAZEX9 instance every route is built against. Set it to your own deployment and every derivation follows. |
CPI_PROGRAM |
on | route swaps through the program at all. Absent means on — only 0/false/no/off disables it. |
RAZE_ROUTER_PROGRAM is read once at startup. A value that is not a valid
pubkey stops the process rather than falling back, so a typo can never quietly
route your swaps through someone else's program.
With CPI_PROGRAM=0 the router falls back to its local assembler for every
swap: routes still build, but each hop settles independently instead of
chaining on measured output, so multi-hop quotes get looser.
The router tries every candidate plan through this program first. Only if no plan can be expressed or fit in a transaction does it retry the same plans with the local assembler. If neither pass produces a transaction the swap is a miss — there is no third-party aggregator behind it.
A route falls out of the CPI pass mainly on size: concentrated-liquidity venues (CLMM, DLMM, Whirlpool) carry tick/bin arrays that move with price, so they cannot live in a lookup table and are inlined at 32 bytes each. Two such legs in one route overflow the 1232-byte transaction limit.
The feeWallet / feeBps / feeOnInput you send to the router become
RouteArgsV2.fee_bps + fee_on_input and the platform_fee_account account on
this instruction. The program charges the fee in kind, on chain, and:
- caps it at 1000 bps (10%) —
FeeTooHighabove that; - takes it from the input or the output according to
fee_on_input; - pays it to whatever account you named — there is no pinned recipient;
- enforces
min_returnon the net, after the fee.
A fee only exists on hops this program builds. A route that fell back to the local assembler carries no percentage fee at all.
Failures surface as custom program error: 0x…. Anchor numbers them from
6000 (0x1770), in declaration order. The ones you are most likely to meet:
| code | hex | meaning |
|---|---|---|
| 6000 | 0x1770 |
Paused — swaps are halted by the admin; admin instructions still work |
| 6005 | 0x1775 |
MinReturnNotReached — the net output missed your floor. Slippage, or a quote built on stale state |
| 6008 | 0x1778 |
VenueNotAllowed — that venue program is not in the allowlist |
| 6013 | 0x177d |
InvalidHopAccounts — a hop's source and destination are the same account |
| 6014 | 0x177e |
UnexpectedSaTokenAccount — the venue CPI listed a protected account that is not this hop's source or destination |
| 6015 | 0x177f |
InvalidActualAmountIn — the venue consumed less than 90%, or more than it was given |
| 6018 | 0x1782 |
FeeTooHigh — fee_bps over the 10% cap |
| 6031 | 0x178f |
MaxAmountInExceeded — exact-out spent more than max_amount_in |
| 6032 | 0x1790 |
ExactOutNotReached — exact-out produced less than amount_out |
| 6033 | 0x1791 |
InvalidSwapAuthority — the passed authority is not SA_POOL[sa_pool_index(payer)] |
The full list is src/error.rs, in order. Note that a custom error 101 is
not from this program: it is Anchor's "instruction not found", meaning the
caller sent a discriminator this deployment does not implement.
| instruction | what it does |
|---|---|
route_universal(RouteArgsV2) |
exact-in swap: deposit → hops → fee → min_return → withdraw |
route_exact_out(RouteArgsExactOut) |
single-hop exact-out: the venue produces amount_out, input capped at max_amount_in, remainder refunded |
route_unified(RouteArgsUnified) |
both directions behind one discriminator, chosen by mode (0 = exact-in, 1 = exact-out) |
initialize_config(fee_authority, fee_bps) |
create the singleton config; the caller becomes admin |
add_venue / remove_venue(program_id) |
edit the venue allowlist (admin) |
set_fee / set_paused / set_admin / set_fee_authority |
admin |
set_fee and set_fee_authority write config fields that routes do not read —
live routes take the fee and its destination per call.
The program is half a route; the client owns everything venue-specific.
- Encode the venue instruction yourself and pass it as
HopMeta.data. Setamount_in_offsetto the byte offset of theu64input amount inside it, or-1to pass the data verbatim. The program splices the measured output of the previous hop there, which is what makes chaining exact. - List every account the venue needs in
remaining_accounts, in the venue's own order, and index into it fromHopMetaV2.accounts. - Derive the swap authority the way the program does:
SA_POOL[sa_pool_index(payer)]. Anything else is rejected with 6033. The shared test vector is insrc/constants.rs. - Create the token accounts first, one per distinct mint, in the same transaction — the program does not create them. Routes whose boundary is the user's own account skip this.
- Budget the transaction before building. The args envelope is 31 bytes
(so an encoded instruction is
39 + Σ hopswith the discriminator) and each hop is23 + 2·A + D, whereAis the number of account metas andDthe venue data length. If you mirror the wire types by hand, mirrorwire_envelope_and_hop_payload_are_pinnedtoo — a hand-written mirror shares no compiler with this crate, so only matching assertions catch a drift.
The wire types live in src/shared.rs; their borsh layout is the contract.
Everything downstream of the program id is derived from it, so a second deployment is a configuration, not a fork. Four steps, and the fourth is what makes the binary you already have use it.
- Generate a program keypair and point
declare_id!at it. TheSA_POOL,SA_WSOL_ATAandCONFIG_ADDRESStables insrc/constants.rsare PDAs derived under that id, so they must be regenerated too. Thepda_tables_match_derivationtest fails loudly if you forget — that is what it is there for. - Build and deploy:
cargo-build-sbf --manifest-path Cargo.toml solana program deploy target/deploy/raze_router.so --program-id <your-keypair.json> - Call
initialize_configin the same slot as the deploy. It has static seeds and the first caller becomes admin. Preferfee_bps = 0: routes take their fee per call, so the config value is inert. - Point the router at it:
From that line on, every route the binary builds — the invoked program, the config PDA it reads, the swap authority it deposits into — is derived under your id. Nothing else changes and nothing needs rebuilding.
RAZE_ROUTER_PROGRAM=<your program id> - Allowlist your venues with
add_venue, one program id at a time. A fresh config starts empty, so until you do this every route failsVenueNotAllowed(6008). An allowlisted program is invoked with the authority's signature, so list only programs you have vetted. The token, system and ATA programs and the router itself are refused outright.
Your instance is independent: its own admin, its own venue allowlist, its own pause switch, its own swap authorities. It shares nothing with ours but the code.
A flat crate — no Anchor programs/ nesting. Anchor 0.32.1 is a dependency but
the build is plain cargo-build-sbf; Anchor.toml carries only the program ids
and the forked-validator clone list.
cargo-build-sbf --manifest-path Cargo.toml # build the .so FIRST
cargo test # unit + in-SVM
.so from
target/deploy, so cargo test alone will happily exercise a stale binary and
still pass.
keys/ is gitignored except PROGRAM_ID.txt; no keypair is, or has ever been,
committed here.
Apache-2.0. See NOTICE. Contributions are accepted under the same license (Apache-2.0 §5) — no separate CLA.