Overview
The Alkanes AMM (built and open-sourced by Oyl) is a port of the Uniswap V2 constant-product design to the alkanes metaprotocol on Bitcoin. Instead of Solidity contracts and ERC-20 tokens, it is a set of WASM alkanes contracts that hold and move alkane assets. Trades, deposits and withdrawals are all expressed as protostone messages (cellpack calldata) embedded in the OP_RETURN of a normal Bitcoin transaction, with the alkanes you spend attached as transaction inputs (edicts).
There are three building blocks:
- Factory / Router — the single entry point users call. It deploys pools, finds existing pools, and routes liquidity and swap operations (including multi-hop) across one or more pools.
- Pool — one contract instance per token pair. It holds the two reserves, mints/burns LP tokens, and enforces the constant-product invariant on every swap. The pool's own alkane id is its LP token.
- Library (
oylswap-library) — shared pricing math (get_amount_out,get_amount_in), 256-bit integer / square-root helpers, the reentrancy lock, and pool serialization.
On Fairmints, all swaps and liquidity actions are built against the deployed AMM router at 4:65522. We never hold your keys: the backend assembles an unsigned PSBT, your wallet signs it, and you broadcast it.
How a call is shaped
Every interaction follows the same pattern:
- The alkanes you are spending (input tokens, or LP tokens for a withdrawal) are attached to the transaction as inputs.
- A cellpack is encoded:
[router_block, router_tx, opcode, ...args], where every value is LEB128-encoded. - That cellpack becomes a protostone written into the transaction's
OP_RETURN. - The router runs, pulls the incoming alkanes, performs the operation, and pays out the resulting alkanes (output tokens, LP tokens, or leftovers) to your taproot address.
Tokens are referenced by their alkane id block:tx (e.g. DIESEL is 2:0). Pairs are order-independent: internally the two tokens are sorted with sort_alkanes, so A/B and B/A resolve to the same pool.
Factory / Router functions
This is the contract you call directly for everyday actions. Opcodes are the first value of the cellpack.
| Opcode | Function | Parameters | What it does |
|---|---|---|---|
| 1 | CreateNewPool | token_a, token_b, amount_a, amount_b | Deploys a new pool for the pair and seeds it with the two supplied amounts, minting the first LP tokens. |
| 2 | FindExistingPoolId | alkane_a, alkane_b | Returns the pool (LP) alkane id for a pair, or errors if none exists. |
| 3 | GetAllPools | — | Returns the full list of pool ids known to the factory. |
| 4 | GetNumPools | — | Returns the total number of pools. |
| 11 | AddLiquidity | token_a, token_b, amount_a_desired, amount_b_desired, amount_a_min, amount_b_min, deadline | Deposits both tokens at the current ratio and mints LP tokens; refunds any off-ratio remainder. |
| 12 | Burn | token_a, token_b, liquidity, amount_a_min, amount_b_min, deadline | Burns LP tokens and returns the proportional share of both reserves. |
| 13 | SwapExactTokensForTokens | path, amount_in, amount_out_min, deadline | Sells an exact input amount along a path (single or multi-hop) for at least amount_out_min. |
| 14 | SwapTokensForExactTokens | path, amount_out, amount_in_max, deadline | Buys an exact output amount, spending no more than amount_in_max. |
| 29 | SwapExactTokensForTokensImplicit | path, amount_out_min, deadline | Like opcode 13 but uses all of the alkane attached to the tx as the input amount. |
| 50 | Forward | — | Passes incoming alkanes straight through (utility / composition helper). |
| 0 | InitFactory | pool_factory_id, beacon_id | Admin: one-time initialization of the factory. |
| 7 | SetPoolFactoryId | pool_factory_id | Admin: update the pool template id used for new deployments. |
| 10 | CollectFees | pool_id | Protocol: realize accrued protocol fees for a pool. |
| 21 | SetTotalFeeForPool | pool_id, total_fee_per_1000 | Admin: override the swap fee for a specific pool. |
Opcodes 0, 7, 10 and 21 are admin / protocol functions and are not exposed to regular users.
Pool functions
Each pool is its own contract. The router calls these on your behalf — you rarely call a pool directly. The read-only opcodes (97–999) are what indexers and the Fairmints UI use to display reserves, price and pool metadata.
| Opcode | Function | Returns | What it does |
|---|---|---|---|
| 0 | InitPool | — | Initializes a freshly deployed pool with its two tokens and factory, then mints initial liquidity. |
| 1 | AddLiquidity | LP | Mints LP tokens for the two tokens attached to the call (invoked by the router). |
| 2 | WithdrawAndBurn | token0, token1 | Burns the attached LP tokens and returns the underlying reserves. |
| 3 | Swap | token | Low-level optimistic swap with post-hoc K check; enables flash-swaps. Not for direct use. |
| 10 | CollectFees | LP | Mints and pays out accrued protocol-fee LP tokens (factory-only). |
| 20 | GetTotalFee | u128 | Reads the pool’s current fee (per 1000). |
| 21 | SetTotalFee | — | Sets the pool fee (factory-only). |
| 50 | ForwardIncoming | — | Forwards incoming alkanes back to the caller. |
| 97 | GetReserves | (u128, u128) | Current reserves of token0 and token1. |
| 98 | GetPriceCumulativeLast | (u128, u128) | Time-weighted cumulative prices for oracle use. |
| 99 | GetName | String | The pool name, e.g. "DIESEL / FIRE LP". |
| 999 | PoolDetails | bytes | Packed token ids, reserves, total supply and name in a single call. |
The low-level pool Swap (opcode 3) optimistically sends tokens out, then verifies the K invariant afterward. It is powerful (it enables flash-swaps) but unforgiving — calling it directly without a matching repayment will simply revert. Use the router's SwapExactTokensForTokens instead.
Swap pricing & the constant-product invariant
Pricing is the classic x · y = k curve with a fee taken on the input. For an exact-input swap, the output is:
amount_in_with_fee = (1000 - total_fee_per_1000) × amount_in
amount_in_with_fee × reserve_out
amount_out = ----------------------------------------------
1000 × reserve_in + amount_in_with_feeAnd for an exact-output swap (how much you must send to receive a target amount):
1000 × reserve_in × amount_out
amount_in = ------------------------------------------------------- + 1
(1000 - total_fee_per_1000) × (reserve_out - amount_out) On every swap the pool re-checks the invariant with fees folded in. The trade is only accepted if the new, fee-adjusted product is greater than or equal to the old reserve product — otherwise it reverts with "K is not increasing":
(balance0 × 1000 - amount0_in × fee) × (balance1 × 1000 - amount1_in × fee)
≥ reserve0 × reserve1 × 1000²Multi-hop swaps (e.g. MIST → DIESEL → ARBUZ) are handled entirely by the router: you pass a path of token ids, and the router computes get_amounts_out for each consecutive pair and chains the swaps. The output of each hop becomes the input of the next, and the fee is applied once per hop.
Exact-input vs exact-output swaps
The router exposes two opposite ways to express a trade. They cover the same curve but pin down a different side of the trade:
- Exact input —
SwapExactTokensForTokens(opcode 13). "I want to sell exactly 1,000 token A; give me as much token B as that buys (at leastamount_out_min)." You fix the input; the output floats. This is the standard path used for nearly every swap. - Exact output —
SwapTokensForExactTokens(opcode 14). "I want to buy exactly 1,000 token B; spend whatever token A is needed (no more thanamount_in_max)." You fix the output; the input floats and any unused input is refunded. Useful when you need a precise amount out (e.g. to repay a debt or hit an exact target balance).
There is also SwapExactTokensForTokensImplicit (opcode 29): same as opcode 13, but instead of stating amount_in it spends all of the input alkane attached to the transaction. Handy for "sell my entire balance" flows.
What Fairmints implements today: create pool (1), add liquidity (11), remove liquidity (12), exact-input swaps (13, single & multi-hop) and exact-output swaps (14) — the swap panel automatically uses exact-output when you type into the "you receive" field. The implicit variant (29) is supported by the contract but not yet wired into the UI.
Fees
The fee is stored per pool as total_fee_per_1000 (parts per thousand). The protocol default is:
- Total fee:
10 / 1000 = 1.0%taken from the input of every swap. - Protocol fee:
2 / 1000 = 0.2%of the trade, carved out of that 1% and accruing to the protocol; the remaining0.8%stays in the pool for liquidity providers.
The protocol's share is not skimmed on every trade. Instead, like Uniswap V2, the pool tracks k_last (the value of √(reserve0 × reserve1) at the last liquidity event) and mints a small amount of new LP tokens representing the accrued protocol fee whenever liquidity is added, removed, or fees are collected (_mint_fee). A per-pool fee can be overridden by the admin via SetTotalFeeForPool (router) or SetTotalFee (pool).
Fairmints quotes routes assuming the 1% default fee. If a pool's admin has set a custom fee, the on-chain result follows the pool's stored value, not the quote.
Liquidity: minting & burning LP tokens
When you add liquidity, the pool mints LP tokens (its own alkane id). The amount depends on whether the pool is empty:
- First deposit:
liquidity = √(amount_a × amount_b) − MINIMUM_LIQUIDITY. The firstMINIMUM_LIQUIDITY = 1000units are permanently locked to prevent the first LP from manipulating the share price — this is why a brand-new pool needs amounts whose product exceeds 1,000,000 (in raw units). - Subsequent deposits:
liquidity = min(amount_a × total_supply / reserve_a, amount_b × total_supply / reserve_b). Using the minimum of the two ratios means depositing off-ratio simply mints fewer LP tokens; the router returns any unused tokens via theamount_*_minguards.
Burning is proportional: redeeming liquidity LP tokens returns liquidity × reserve / total_supply of each underlying token. Both the add and burn router functions take amount_a_min / amount_b_min slippage floors and a deadline.
Safety mechanisms
- Reentrancy lock: add, burn and swap all run inside a storage-based
Lockthat reverts with"LOCKED"on re-entry. - Deadlines: router operations take a
deadlineblock height and revert with"EXPIRED"if mined too late. Fairmints sets this to roughly current height + 144 blocks. - Slippage floors:
amount_out_min(swaps) andamount_a_min/amount_b_min(liquidity) guarantee you never get less than you accept. - K invariant: the post-swap product check is the ultimate backstop against mispriced trades.
- Input validation: the pool rejects unknown alkanes, equal-token pools, zero amounts, and sending output to one of the reserve token ids (
INVALID_TO).
Reading pool state
The Fairmints UI builds quotes and charts from the pool's read-only opcodes:
GetReserves(97) — current(reserve0, reserve1), the basis for spot price and price impact.GetPriceCumulativeLast(98) — time-weighted price accumulators for TWAP-style oracles.GetName(99) — the human pool name, formatted"TOKEN_A / TOKEN_B LP".PoolDetails(999) — a packed blob with both token ids, reserves, total supply and name in one call.
At the factory level, FindExistingPoolId, GetAllPools and GetNumPools let you enumerate and discover pools.
Routers, aggregators & bots
The Oyl AMM router at 4:65522 is the canonical entry point, but it is not the only contract that trades against these pools. Because pool reserves live in the pool contracts (2:*), anyone can write a contract that calls a pool's low-level Swap directly. Several third-party swap aggregators / bots do exactly that — they bundle their own routing or MEV logic and bypass the router.
Sampling the most recent ~3,000 pool transactions on-chain, the split of entry contracts is roughly:
| Entry contract | Share | Type |
|---|---|---|
| 4:65522 | ~80% | Oyl AMM router (official) — swaps + all liquidity ops |
| 2:77603 | ~9% | third-party swap aggregator |
| 2:79320 | ~4% | third-party swap bot |
| 2:87219 | ~4% | third-party swap aggregator |
| 2:85492 | ~1% | third-party swap aggregator |
| 2:77590 | ~1% | third-party swap aggregator |
- About 80% of pool activity flows through the Oyl AMM router (
4:65522), and 100% of liquidity add/remove operations do — aggregators are swap-only. - The remaining ~20% of swaps enter through aggregators that call the pools directly. They still settle against the same reserves, so they appear in pool activity and affect price like any other trade.
4:65523is invoked in 100% of pool transactions: it is the shared pool implementation contract that every pool proxies to viadelegatecall.
Fairmints always builds swaps and liquidity actions through the official router (4:65522). Our indexer additionally recognizes the known aggregators above so their trades are classified correctly in pool history — but we never route your funds through them.
Source: Oyl-Wallet/oyl-amm (factory, pool and oylswap-library crates). This page documents the contract as deployed; always verify on-chain behavior before transacting.