# FTW Swap > FTW Swap is a constant-product AMM on Neo N3. Every pool pairs a NEP-17 token with GAS. Traders pay a 0.25% fee to the liquidity providers. Liquidity positions are NEP-11 tokens (symbol `FTW-LP`). This file is for developers, trading bots and AI agents; the same text is at `/llms.txt` and `/developers` on the web app. ## Contracts and networks | Network | FTW Swap | Network magic | Public RPC | Web app | |---|---|---|---|---| | MainNet | `not deployed yet` | 860833102 | https://mainnet1.neo.coz.io:443 | https://swap.forthewin.network | | TestNet | `0xec3ff3ccdc21e9f07d50f5e1a7d0362d2b6bb47b` | 894710606 | https://testnet1.neo.coz.io:443 | https://swap.testnet.forthewin.network | - GAS: `0xd2a4cff31913016155e38e474a2c06d08be276cf` (8 decimals). Every pool is token/GAS. - Contract manifest name: `FTW Swap`. The owner is a multisig; code updates are timelocked (see Governance). - Optional history API (indexer): https://n3-api.forthewin.network (MainNet), https://n3-api-testnet.forthewin.network (TestNet). ## Units - Amounts are integers in base units: 1 GAS = 100000000. Read a token's `decimals()` before converting. - `deadlineMs` and `lockUntilMs` are Unix time in **milliseconds**, compared with the block time. - `slippageBps` (addLiquidity only) is in basis points: 100 = 1%. Allowed: 1..4999. - Fee: `amountIn / 400` (0.25%, integer division), paid by the trader and kept in the pool for the LPs. ## Methods ### Trading The `account` argument must sign the transaction (see "Witness scope"). `swap`, `swapB` and `addLiquidity` fail while the market is stopped; `removeLiquidity` always works. - `swap(account, tokenIn, amountIn, tokenOut, amountOutMin, deadlineMs)` — sell exactly `amountIn`. Fails unless you receive at least `amountOutMin` (which must be > 0). - `swapB(account, tokenIn, tokenOut, amountOut, amountInMax, deadlineMs)` — buy exactly `amountOut`. Fails if it would cost more than `amountInMax` (which must be > 0). - `addLiquidity(account, tokenA, amountA, tokenB, amountB, deadlineMs, lockUntilMs, slippageBps)` — one of the two tokens must be GAS; the order is yours. For a **new pool** both amounts are taken as given and set the starting price. For an **existing pool** the first amount (`amountA` of the first token you pass) is exact, and the second is taken at the pool ratio: `amountA * reserveOfB / reserveOfA`. The call fails unless that is within `slippageBps` of your `amountB`. Mints one LP NFT to `account`. `lockUntilMs` = 0 means no lock; a locked position cannot be removed until the block time passes it. - `removeLiquidity(account, tokenId, deadlineMs)` — burns the LP NFT and pays out both sides pro rata. - NEP-11 `transfer(to, tokenId, data)` moves an LP position to another account. ### Reads (safe, free with `invokefunction`) - `getPairs()` — every pool: `tokenA, tokenB, amountA, amountB, totalShare, tokenASymbol, tokenBSymbol, tokenADecimals, tokenBDecimals`. - `getReserve(tokenA, tokenB)` — one pool, in the order you ask; zeros if it does not exist. - `getSwapEstimate(tokenA, tokenB, tokenIn, amountIn)` → `{estimated, fee, decimals}` — exact-in quote (`tokenA`, `tokenB` name the pool). - `getSwapBEstimate(tokenIn, tokenOut, amountOut)` → `{estimated, fee, decimals}` — exact-out cost. - `getLPTokensByUser(account)`, `properties(tokenId)` — LP positions with `amount`, `amountA`, `amountB` (current value), `sharesPercentage` (bps), `lock`, `createdAt`, symbols and decimals. - `getLP(tokenA, tokenB, n)`, `getLPs(tokenA, tokenB, perPage, page)` — a pool's LP history. - `getContractOwner()`, `getMarketStatus()`, `getUpdateDelay()`, `getPendingUpdate()`, `getTotalLiquidityHeight()`, `getLiquidityHeight(pairKey)`, `getPariKey(tokenA, tokenB)`, `searchTokensOf(owner)`. - NEP-11: `symbol` (`FTW-LP`), `decimals` (0), `totalSupply`, `balanceOf`, `ownerOf`, `tokens`, `tokensOf`. ### Owner only `stopPlatform`, `activePlatform`, `changeOwnership(newOwner)`, `proposeUpdate(nefFile, manifest)`, `cancelUpdate`, `update(nefFile, manifest, data)`, `setUpdateDelay(blocks)`. ### Events | Event | Parameters | |---|---| | `Swap` | account, tokenIn, tokenOut, amountIn, amountOut, fee | | `AddLiquidity` | account, tokenA, tokenB, amountA, amountB (pool order) | | `RemoveLiquidity` | account, tokenA, tokenB, amountA, amountB (pool order) | | `NewPoolCreated` | account, tokenA, tokenASymbol, tokenADecimals, tokenB, tokenBSymbol, tokenBDecimals | | `Transfer` | from, to, amount, tokenId (NEP-11; from is null on mint, to is null on burn) | | `Fault` | message, data (emitted just before a call fails) | | `MarketStatusChanged` | active | | `OwnerChanged` | oldOwner, newOwner | | `UpdateProposed` | codeHash, executableAt (block index) | | `UpdateCancelled` | — | | `UpdateDelayChanged` | blocks | ## Pair order and LP ids A pool's `tokenA` is the token whose script hash, read as a little-endian signed integer, is larger. (The displayed big-endian hex is that integer's magnitude, with the sign bit in its first byte.) LP ids are `symbolA-symbolB-n`, for example `FTWT-GAS-1`, where `n` counts every LP position ever minted. An id longer than 43 bytes cannot be minted, so a token with a very long symbol cannot be pooled. Tokens with 0 decimals (such as NEO) cannot be pooled. ## Math Quotes are integer math; every division truncates. The SDK reproduces the contract exactly (golden vectors in the repository's `tests/vectors/`). - Exact in: `fee = amountIn / 400; amountOut = reserveOut - (reserveIn * reserveOut / (reserveIn + amountIn - fee) + 1)` - Exact out: `pre = reserveIn * reserveOut / (reserveOut - amountOut) - reserveIn; fee = pre / 399; amountIn = pre + fee` - New pool: `share = floor(sqrt(amountA * amountB))` - Add to a pool: `amountSecond = amountFirst * reserveSecond / reserveFirst; share = amountOnSideA * totalShare / reserveA` - Remove: each side pays `reserve * lpAmount / totalShare` ## Witness scope FTW Swap takes your tokens by calling `token.transfer(account, FTW Swap, amount)`. The token checks your witness while FTW Swap is the calling contract, so **a `CalledByEntry` signature is not enough**. Sign with one of: - Recommended: scope `WitnessRules` (64) with the rule `Allow Or(CalledByEntry, CalledByContract())`. - Alternative: scope `CustomContracts` listing FTW Swap and every token the call moves. Never sign with `Global`: any contract in the transaction could then move any of your assets. NEP-11 LP `transfer` needs only `CalledByEntry`. ## Errors A failing call FAULTs. The VM message looks like `ABORTMSG is executed. Reason: ` or `unhandled exception: ""`. The SDK's `parseFault` maps it: | Message contains | SDK code | Meaning | |---|---|---| | `Market is not active` | `MARKET_STOPPED` | The market is stopped: swaps and adds fail, removals work | | `Estimated amount is less than slippage` | `SLIPPAGE` | `swap` would pay less than `amountOutMin` | | `Estimated token A amount is higher than maxTokenAAmount` | `SLIPPAGE` | `swapB` would cost more than `amountInMax` | | `Estimate token B amount is lower than slippage set`, `… higher than slippage set` | `SLIPPAGE` | The pool ratio is outside `slippageBps` of your second amount | | `Exceeded the deadline` | `DEADLINE` | The block time is past `deadlineMs` | | `This LP token is locked until ` | `LP_LOCKED` | The position is still locked | | `Pair must include GAS` | `PAIR_NEEDS_GAS` | Pools are token/GAS only | | `Reentrancy Not Allowed` | `REENTRANCY` | FTW Swap was called again from inside one of its own calls | | `Pair doesn't exist` | `PAIR_NOT_FOUND` | No pool for the pair | | `Pool doesn't have enough fund to swap` | `INSUFFICIENT_LIQUIDITY` | The exact-out amount is too large | | `decimal should be higher than 0` | `ZERO_DECIMALS` | 0-decimal tokens cannot be pooled | | `You are not owner` | `NOT_AUTHORIZED` | `account` did not sign, or does not own the LP position | | `Transfer Fail`, `balance invalid` | `TRANSFER_FAILED` | A token transfer failed or moved a different amount (fee-on-transfer tokens are not supported) | | `Cannot find LP token with given token Id` | `LP_NOT_FOUND` | The LP position no longer exists; it may already be removed | | `is out of range` | `BAD_AMOUNT` | An amount (`amountA`, `amountB`, `amountIn`, `desiredAmount`, …) is too small or invalid | | anything else | `UNKNOWN` | The raw message | ## Cautions - **Never send tokens to FTW Swap with a plain `transfer`.** They join no pool and cannot be recovered. - **FTW Swap is not a price oracle.** Several FTW Swap calls can run in one transaction, so a pool's spot price can be moved and restored inside a single transaction. A read made from a token callback during a trade also sees the reserves from before that trade. - **Exact-out on tokens with few decimals.** `swapB` rounds the input down by up to 1 base unit when the fee rounds to 0. For a token with few decimals where one base unit is worth a lot, prefer `swap` (exact in). - A pool is only as safe as its token contract. Pools are separate: a bad token can only hurt its own pool's LPs. Check a token's hash before trading it; the web app marks tokens outside its verified list. - Locked liquidity cannot be removed before its lock time, not even by its owner. The fees it earns stay locked with it. ## Several calls in one transaction Sequential FTW Swap calls in one script work: trade then trade, add then remove. A nested call — FTW Swap reached again from inside one of its own calls, for example from a token's `transfer` — fails with `Reentrancy Not Allowed`. Route X → GAS → Y in one transaction. The second leg spends only the GAS the first leg guarantees; any extra GAS stays in your wallet: ```ts const px = (await reader.getPool(X))!; const py = (await reader.getPool(Y))!; const gasOut = quoteExactIn(px.reserveToken, px.reserveGas, amountX).amountOut; const gasMin = minOut(gasOut, 50); // 0.5% slippage const yOut = quoteExactIn(py.reserveGas, py.reserveToken, gasMin).amountOut; const ctx = { swapHash, account: account.scriptHash }; const legs = [ swapExactIn(ctx, { tokenIn: X, tokenOut: GAS_HASH, amountIn: amountX, amountOutMin: gasMin }), swapExactIn(ctx, { tokenIn: GAS_HASH, tokenOut: Y, amountIn: gasMin, amountOutMin: minOut(yOut, 50) }), ]; const script = toScript(legs); // both calls, in order const signers = toNeonSigners(legs); // then simulate, sign and send as in the SDK example below ``` An arbitrage bot works the same way with its own limits: if any leg would fall short of its minimum, the whole transaction FAULTs and no tokens move. Read before you trade: send the exact script to the RPC method `invokescript` first. It needs no signature and sends nothing; it returns HALT or FAULT, the FAULT message and the GAS the script uses. ## SDK `@forthewin/ftw-swap-sdk` is TypeScript (ESM) for browsers and Node 18+. Its only dependency is `@cityofzion/neon-core`. It lives in the FTW Swap repository (`sdk/`); an npm release will follow. Amounts are `bigint` base units. Quote, build the invocation, simulate, sign and send with neon-js: ```ts import { rpc, tx, u, wallet } from "@cityofzion/neon-core"; import { GAS_HASH, NETWORKS, SwapReader, minOut, parseFault, quoteExactIn, swapExactIn, toNeonSigners, toScript } from "@forthewin/ftw-swap-sdk"; const net = NETWORKS.testnet; const swapHash = net.swapHash!; const reader = new SwapReader({ swapHash, rpcUrl: net.rpc }); const client = new rpc.RPCClient(net.rpc); const account = new wallet.Account(process.env.WIF); // your key; never share it // 1. Quote: 1 GAS → FTWT (the TestNet test token) const FTWT = "0x6c446ac0ef07cfb06a3db2931a91199785d96acf"; const pool = await reader.getPool(FTWT); if (!pool) throw new Error("no pool"); const { amountOut } = quoteExactIn(pool.reserveGas, pool.reserveToken, 100000000n); // 2. Invocation: 0.5% slippage, deadline now + 10 minutes (the default) const inv = swapExactIn({ swapHash, account: account.scriptHash }, { tokenIn: GAS_HASH, tokenOut: FTWT, amountIn: 100000000n, amountOutMin: minOut(amountOut, 50) }); // 3. Simulate, then sign and send const script = toScript(inv); const signers = toNeonSigners(inv); const sim = await client.invokeScript(u.HexString.fromHex(script), signers); if (sim.state !== "HALT") throw new Error(parseFault(sim.exception ?? "").message); const t = new tx.Transaction({ script, signers, validUntilBlock: (await client.getBlockCount()) + 100, systemFee: u.BigInteger.fromNumber(Math.ceil(Number(sim.gasconsumed) * 1.01)) }); t.witnesses = [new tx.Witness({ invocationScript: "", verificationScript: u.HexString.fromBase64(account.contract.script).toBigEndian() })]; t.networkFee = u.BigInteger.fromNumber(await client.calculateNetworkFee(t)); t.witnesses = []; t.sign(account, net.magic); const txid = await client.sendRawTransaction(t); ``` With NeoLine the invocation is exactly the argument of `invoke`: ```ts const neoline = new (window as any).NEOLineN3.Init(); const { address } = await neoline.getAccount(); const inv = swapExactIn({ swapHash, account: wallet.getScriptHashFromAddress(address) }, { tokenIn: GAS_HASH, tokenOut: FTWT, amountIn: 100000000n, amountOutMin: minOut(amountOut, 50) }); const { txid } = await neoline.invoke(inv); ``` Main exports: - Networks and ids: `NETWORKS`, `GAS_HASH`, `normHash`, `isTokenA`, `sortPair`, `gasPairLpId` - Math: `quoteExactIn`, `quoteExactOut`, `minOut`, `maxIn`, `priceImpactBps`, `priceGas`, `quoteAdd`, `addBounds`, `newPoolShare`, `quoteRemove`, `shareBps` - Invocations: `swapExactIn`, `swapExactOut`, `addLiquidity`, `removeLiquidity`, `transferLp`, `swapSigner`, `customContractsSigner`, `toScript`, `toNeonSigners` - Reads: `SwapReader` (`getPools`, `getPool`, `getPairs`, `getReserve`, `getSwapEstimate`, `getSwapBEstimate`, `getLpPositions`, `getLpPosition`, `getStatus`, `tokenMeta`, `balanceOf`), `parseReceipt` - Errors and tokens: `parseFault`, `TOKENS`, `isVerified` - Indexer: `createIndexer` (every call returns `null` on failure), `fillCandleGaps` ## Indexer (optional) - `GET /prices` → `{"GAS": {"usd": "1.54", "ts": "…"}, "NEO": {"usd": "…", "ts": "…"}}` - `GET /swap/pools`, `GET /swap/pools/{token}`, `GET /swap/pools/{token}/trades?limit=50&before=`, `GET /swap/pools/{token}/candles?interval=5m|1h|1d&from=&to=` — `{token}` is the `0x` script hash. Integers are decimal strings in base units; `priceGas` is GAS per whole token. Every response carries `indexedBlock`. The indexer can lag or be down. The chain is the source of truth. ## Governance The owner is a multisig. It can stop the market (removals keep working), restart it, hand over ownership and update the code. A code update must first be announced with `proposeUpdate`, which emits `UpdateProposed(codeHash, executableAt)` with `codeHash = sha256(nef || manifest)`. `update` then installs exactly that code, and only after `getUpdateDelay()` blocks (5760 blocks, about 24 hours; the delay can only grow). If you hold liquidity, watch for `UpdateProposed`. Live status: `/status` on the web app.