# Earn as a cat holder (/pve/earn) One rule decides everything on this page: **a staked cat earns, a cat in your wallet does not.** ## How you get paid [#how-you-get-paid] When a PvE launch credits the cat pool, the drop is split across every staked cat at that moment, cat for cat. Five staked cats collect five shares. The snapshot is taken at the block before the credit, so: * Staked before the drop: you are in it. * Staked after the drop: you are not in that one, and you are in every drop after. There is no way to see a drop land and stake into it. Because the snapshot sits one block behind the credit, the split is already decided by the time anyone can react. ## Claiming [#claiming] Your shares collect as launches happen, and you claim them on Motoswap's [staking page](https://motoswap.org/stake/motocats), open from launch day. Claiming is a straight withdrawal: * Claim a single token or a batch of them in one transaction. You choose which and when. * Claims never expire. A share from the first PvE launch ever is still yours years later. * You get the launched token itself. No conversion, no swap, no fee on the claim. * You do not need to still be staked to claim. The snapshot decided your share; unstaking afterward does not take it back. A drop is a fixed amount, settled at the snapshot. It does not grow while you wait and it does not shrink if you claim late. There is no APR here and nothing to compound. ## What this asks of you [#what-this-asks-of-you] Almost nothing. Keep your cats staked, and check in when new tokens launch. The drops accumulate whether you watch or not. You can unstake any time with no wait, but a cat that is unstaked when a coin graduates gets no share of it. Drops pay in the launched tokens themselves, and their value depends on each coin's price. Some will be worth nothing. # What PvE mode is (/pve) PvE mode is a launch option on [moto.fun](/fun). When a coin launches with PvE on, part of the buying goes to staked [Motocats](/user/motocats) instead of into the creator's wallet. It is not a team allocation or an insider round. The tokens are bought on the curve at the going price. The name is the idea: players versus environment, not players versus players. On most launches, bots that buy in the first block take the early supply. A PvE launch changes who the early supply goes to. The people holding staked cats get the slice, every cat equally, and nobody can front-run their way into it. PvE is opt-in at launch. The creator decides how much of their buy feeds the cats A public PvE buy tops up the slice on any bonding coin, from any wallet Every staked cat gets an equal share of every drop. Ten cats, ten shares ## Where the slice comes from [#where-the-slice-comes-from] Two ways, and a coin can carry both: 1. **The creator's launch.** A creator points some or all of their own launch buy at the cat pool. The tokens are bought on the curve at the going price and go straight into escrow. 2. **A public PvE buy.** Anyone can buy a bonding coin at the going rate and donate every token to the cat pool, any time before graduation. Any wallet, any amount, as many times as people want. Both land in the same escrow and pay out together. ## When stakers get paid [#when-stakers-get-paid] At graduation, in one drop. The escrow builds up while the coin bonds and credits to staked cats in the graduation transaction, not before. A coin that trades for months without graduating holds its escrow for months. That is deliberate: the cats get paid when the coin makes it. ## The loop [#the-loop] A coin launches with PvE on. Staked cats split the slice when it graduates. Cat holders find the coin, and some of them trade it or provide liquidity. The launch gets holders it did not have to buy, and the cats get paid for showing up first. ## The rules [#the-rules] Five rules run PvE: 1. **One credit per coin.** Everything the escrow collected pays out once, at graduation. No second helping, no ongoing emission. 2. **The snapshot is strictly earlier than the credit.** Split by staked balances at the block before the drop lands, equal per cat. Nobody sees a drop coming and jumps in front of it. 3. **Entitlement is flat and final.** Staking more later grows nothing for that coin; unstaking after shrinks nothing. 4. **Claims never expire.** Claim on Motoswap's [staking page](https://motoswap.org/stake/motocats), from launch day. Batch any number of coins in one transaction and receive the tokens themselves, with no swap, no fee, and no need to still be staked. 5. **Nothing burns on timing.** A drop landing while no cats are staked is held and folds in once stakers exist. No streaming, no APR, and no cap on how many coins PvE can carry. Start with [earning as a cat holder](./earn.mdx), or [launching with PvE on](./launch.mdx) if you are shipping a coin. # Launch with PvE on (/pve/launch) With PvE on, early supply goes to staked Motocat holders instead of whoever snipes fastest. Every staked cat gets a claimable share of your coin when it graduates. | | | | --------------------- | -------------------------------------------------------------------------- | | Set at | Launch, as a share of your own buy | | Who pays | You. The same ETH, the same 1.2% fee as any buy | | Price | The curve price. One fill, so your slice and the cats' slice cost the same | | Where the tokens go | Straight into escrow, never through your wallet | | Topping up | Anyone can add to it, any time before graduation | | When cats get paid | At graduation, in one drop | | If it never graduates | The escrow stays parked | ## Choosing the split [#choosing-the-split] At launch you decide what your buy is for: all of it to the cats, all of it a dev buy, or a percentage split. You pay the same ETH either way. The setting only steers where the bought tokens land. It is one buy at one price. The curve fills once and the two halves are separated afterward, in proportion to the ETH each represents, so neither side gets a better fill than the other and neither front-runs the other. The cats' tokens never pass through your wallet. They go from the curve straight into the escrow. A buy that would take the curve past graduation fills only up to the line and refunds the rest. When that happens the donation is filled first, out of what was actually spent, and your own slice absorbs the shortfall. Promising 1 ETH to the cats means 1 ETH to the cats, not a proportion of whatever the curve happened to take. ## Anyone can top it up [#anyone-can-top-it-up] A public PvE buy is open to any wallet on any bonding coin, whether or not you turned PvE on at launch. The buyer pays the going price and the normal fee and keeps nothing: every token goes to the escrow. They can do it repeatedly, and so can anyone else. If a coin's holders want the cat pool behind it, they can put it there themselves without asking you. ## Graduation is the payout moment [#graduation-is-the-payout-moment] Everything the escrow collected, from your launch and from every public PvE buy, credits to staked cats in one drop inside the graduation transaction. Not before, and not in pieces. An active curve holds its escrow until it graduates. If a coin never sells out its curve, the escrow stays parked. That is by design: the cats get paid when the coin makes it, and the promise of the slice is part of why it makes it. Graduation can never be blocked by the cat credit, so if the credit fails the coin still graduates and the escrow stays put rather than being destroyed. A keeper retries it. Nothing is lost and there is nothing for you to do. ## What you get [#what-you-get] * **Holders at graduation.** One buy at curve price gives every staked cat a claimable share of your coin. * **The credit under your name.** The wallet that signed the launch is what the drop is attributed to, whoever you set as your fee recipient. PvE costs nothing beyond the slice itself: same launch flow, same fees, same mechanics. It stacks freely with [creator fees](/fun/creator-fees) and a dev buy, and the only thing it changes is who gets the early supply. The moto.fun side of it is on [PvE on the curve](/fun/pve). # Contract Addresses (/dev/addresses) > **Read addresses at runtime from `GET https://api.motoswap.org/config`.** That endpoint is the > canonical source. Every address below was checked against it and against code on chain > (`eth_getCode` plus one identifying call). If this page and `/config` ever disagree, `/config` wins. The plain-language list for users, with Etherscan links and one line on what each contract does, is [Contracts](/user/contracts). ## Deployment status on Ethereum (chain 1) [#deployment-status-on-ethereum-chain-1] Status is per contract. A contract is either deployed at the address shown, or not deployed yet. A contract that is not deployed reads back from `/config` as `0x0000000000000000000000000000000000000000`. The key is always present. Branch on the address being `0x0`, never on a key being absent, never send a transaction to a `0x0` address, and do not integrate against any other address for a contract this table lists as not deployed. `/config` returns addresses lowercase; the checksummed form is shown here. | Contract | `/config` key | Status | Address | | ---------------------------------------------------------------- | ------------------------------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------- | | `MotoToken` (MOTO, 18 decimals) | `motoToken` | Deployed | `0xBd965230588EAA536dE6aA45E8ebbc01638535e0` | | `VampChef` (the launch farming contract) | `vampChef`, and `masterChef` while `launchPhase` is `vamp` | Deployed | `0x51e648f08a9a08A724591938d9cbc483C809aCA4` | | `UniswapZap` (zap into a Uniswap V2 LP and stake it in the farm) | `uniswapZap` | Deployed | `0x532bFB5b8c9e3CeF20d36910B90cfbD919b18C31` | | `RewardVestingEscrow` | `rewardVestingEscrow` | Deployed | `0xB94bFadc535D96462854175D776Afc168196976b` | | `MotoStaking` | `motoStaking` | Deployed | `0xCE88F2C6B49EfBb92555eE5475311f0e250C2528` | | `TreasuryVesting` | `treasuryVesting` | Deployed | `0x5730ABdF3C06c37940FB8B5291277e62D2c15278` | | Motocat NFT collection | `motocatNft` | Deployed | `0x0B024b3D4ad38BFf1Acd303cd308D2C49c936E0A` | | `MotocatStaking` | `motocatStaking` | Deployed | `0x11890Ee9eE6099Ac33fd4dfb702f8E8f6d651f17` | | `MotoSwapFactory` | `factory` | Deployed | `0x81C9CBC47d700dA1777aBd831D8dA3f526DfAe24` | | `MotoSwapRouter02` | `router` | Deployed | `0x6F0b6a0D84C3cFf6f273D5294CDA147885225D5E` | | `FeeRouter` | `feeRouter` | Deployed | `0x9f846ef584FD44d075B5E8dF00dDB4416da61a80` | | `MotoSwapPair` (every Motoswap pair) | none, derive from `factory` | Deployed per pair | none, one address per pair from `factory` | | `MotoSwapZap` | `zap` | Deployed | `0x26BffD661C3d97B3285AfA2a9F05232526A89129` | | `Collector` | `collector` | Deployed | `0xC13307272bBf73f2191cE57d0Fb714C2A9200cF3` | | `QuoteAssetRegistry` | `quoteRegistry` | Deployed | `0xFb8CA710B9B242e9f5027272592ef4C19D37E200` | | `RakebackV2` | `rakeback` | Deployed | `0x89E2E1819Fc373e3cD840A0ceb2Ef1eAE79E2dB8` | | `MasterChef` (deployed; no sustain farm is running) | `masterChef` (holds the `VampChef` while `launchPhase` is `vamp`) | Deployed | `0x939f348b6658cE4DB7CDe088341b00DE341Fe085` | | `CreatorFeeRegistry` | `creatorFeeRegistry` | Deployed | `0x6CdEC7F49036Ccf1bba940011040fA044cFe6D08` | | `CreatorFeeVault` | `creatorFeeVault` | Deployed | `0x8cC7C8E76D9698E5B1026c720a94C1807e613e1f` | | `PveVault` | `pveVault`, `pveVaultV2` (two instances of one contract; either can read `0x0`) | Deployed | `0x84303a885717C4dd9EffBdD2179BBE4b01756740` (`pveVault`; `pveVaultV2` reads `0x0`) | | `BuybackBurner` | `buybackBurner` | Deployed | `0x85B5A2800d7E2D948F21Cb8F6E610D2c9B19AE3d` | | moto.fun curve and lens | `launchpadCurve`, `launchpadLens` | Deployed | curve `0xb65b67e986d7B097652920d72202F2c78319BC2C`, lens `0xb631697124c8e6Bb6C221Cc017e47D4168bd3411` | Third-party contracts that `/config` also serves: | Contract | `/config` key | Address | | ----------------------------------------------------------------------- | ------------------ | -------------------------------------------- | | MOTO/WETH pair for the phase (a Uniswap V2 pair during `vamp`) | `motoUniswapPair` | `0xAD1a21F61d653c6101b92c335f61140459C81C79` | | Uniswap V2 Router02 (canonical, the farm's trading venue during `vamp`) | `uniswapV2Router` | `0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D` | | Uniswap V2 factory (canonical) | `uniswapV2Factory` | `0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f` | | WETH (canonical) | `weth` | `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` | | USDC (canonical, 6 decimals) | `usdc` | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | | USDT (canonical, 6 decimals) | `usdt` | `0xdAC17F958D2ee523a2206206994597C13D831ec7` | | Permit2 (canonical) | `permit2` | read the value `/config` serves, see below | `/config.launchPhase` summarises the same thing. It reads `dex` once the DEX contracts are deployed, and it reads `vamp` only while the farm is deployed and the DEX is not, which is how it read before the DEX opened. Read `/config.addresses.masterChef` rather than assuming what is behind it. While `launchPhase` was `vamp` it held the same address as `vampChef`, and that contract is a `VampChef`, not a `MasterChef`: `VampChef.withdraw` returns your LP in one call, it has no `claimWithdrawal` and no deposit fee, and its `PoolAdded` and `PoolSet` events carry no `depositFeeBps`. Every row of `GET /farms` carries a `chef` field, so use that as the deposit target instead of assuming a key. `VampChef`, `RewardVestingEscrow`, `MotoStaking`, `TreasuryVesting` and `MotocatStaking` are ERC-1967 proxies. `MotoToken` is not upgradeable and has no owner. Verify any of these yourself with one call each: `symbol()` on `motoToken` returns `MOTO`, `rewardToken()` on `vampChef` returns the MOTO address, `vestingEscrow()` on `vampChef` returns the `rewardVestingEscrow` address, and `collection()` on `motocatStaking` returns the `motocatNft` address. ### Notes on the zero keys [#notes-on-the-zero-keys] `permit2` is a feature switch as much as an address. Canonical Permit2 is deployed on chain `1` at `0x000000000022D473030F116dDEE9F6B43aC78BA3`, and the key carries that address once `FeeRouter`, which owns the Permit2 path, is deployed. Use the value `/config` gives you. A `0x0` there is a configuration fault worth reporting, not a phase. `launchpadCurve` and `launchpadLens` also read `0x0` whenever `motofunEnabled` is `false`, so gating on the address alone is correct. ### Keys that stay zero [#keys-that-stay-zero] | `/config` key | Why | | -------------------- | ------------------------------------------------------------------------------------------ | | `tokenLauncher` | Retired contract. It is not deployed and the key stays `0x0`. Launches happen on moto.fun. | | `buybackDistributor` | Not deployed. Nothing schedules this contract. | | `vaultCompounder` | Legacy key. No contract sits behind it. | | `vaults[0..3]` | Legacy array. `MotoStaking` holds every lock option in one contract. | ### The rest of the response [#the-rest-of-the-response] `/config` also returns `chainId`, `launchPhase` (`prelaunch`, `vamp` or `dex`), `splitSwapEnabled`, `motofunEnabled`, `motofunUrl`, `bridgeMotoEnabled`, `pveComboEnabled` (dead: nothing reads it), and `misconfigured: string[]` (empty in normal operation; it names any address a transaction would target that is unset for the current phase). It does not return an RPC URL. Bring your own. ### Not exposed by `/config` [#not-exposed-by-config] Only the `Snapshotter` (operational tooling) has no `/config` key. Everything else is served directly, with no on-chain getter chase needed. ### Deriving pairs [#deriving-pairs] Derive any Motoswap pair from the factory rather than hard-coding it: `factory.getPair(tokenA, tokenB)`. The Uniswap V2 pools the farm used during `vamp` derive the same way, from `uniswapV2Factory`. `rewardVestingEscrow` also has an on-chain getter, `vestingEscrow()` on the chef, which is worth preferring since it survives a redeploy that `/config` has not caught up with. If an address is not in `/config` and not discoverable from a `/config` contract's getters, do not integrate against it. Contracts published under Motoswap-adjacent repos are sibling experiments, not the Motoswap DEX. ## Local development [#local-development] There is no public local stack and no public test deployment of the Motoswap contracts. To work locally, fork Ethereum mainnet with your own node tooling and use the chain 1 addresses from `GET https://api.motoswap.org/config`. A fork only carries what was already deployed at the block you fork from, so pin a recent block, and read the interfaces in the [contracts inventory](/dev/reference/contracts-full-inventory) when you need the shape of a call before you make it. ## Sanity check: addresses vary per environment [#sanity-check-addresses-vary-per-environment] MOTO and every Motoswap contract have a different address on every environment, and sibling experiments carry their own mocks. Always resolve via `/config`, and check `chainId` in the same response. Never assume addresses match across projects or deploys. # REST API (/dev/api) The Motoswap REST API serves indexed reads for Ethereum mainnet (chain id 1). Almost every route is a `GET`. The few writes are wallet-signed (`POST /referrals/attribute`, the profile routes). Base URL: ``` https://api.motoswap.org ``` **API host note:** `api.motoswap.org` is the permanent public API host and the only one to build against. It answers for chain id 1. Do not substitute any other hostname you may find: temporary hosts get removed. **Full endpoint reference:** [`reference/api-full-inventory`](/dev/reference/api-full-inventory). ## Which routes carry data [#which-routes-carry-data] Deployment status is per contract. The status table is on the [contract addresses page](/dev/addresses), and `GET /config` is the machine-readable version: a key holds the zero address while its contract is unset for the current phase. Read every address from `/config` and never integrate against one it did not give you. Every route answers `200` with its final shape regardless. What differs is whether there is data behind it. `GET /config` reports the phase as `launchPhase`. It reads `dex` with the DEX deployed and open, and it read `vamp` before the DEX opened. | Surface | While `launchPhase` is `dex` | | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `/config` | Every deployed key carries an address, and `launchPhase` reads `dex`. A key that is unset for the phase reads the zero address. Read addresses from `/config`, never hardcode. | | `/tokens`, `/token/{addr}/price`, `/holdings/tokens`, `/tokens/{addr}/stats` | Answer with data, from the Motoswap pairs the indexer tracks. | | `/dex/pools`, `/dex/pools/{pair}`, `/tokens/{addr}/pools`, swaps lists | Answer with data for the Motoswap pairs. | | `/farms`, `/explore/farms`, `/pnl/{addr}/farm/{chef}/{pid}` | Answer with data for the farms the chef in `/config` owns. | | `/stake/vaults`, `/moto/overview`, `/tokens/moto/supply` | Answer with data. | | `/analytics/*`, `/stats/series`, `/rakeback/*`, `/creator/*`, `/pve/*`, `/portfolio/{addr}/lp-fees` | Answer with data. Each one is driven by the protocol fee, so a wallet or token with no fee-paying trades gets an empty answer rather than an error. | | `/points/*`, `/referrals/{addr}` | Answer with the wallet's balances and the leaderboard. | Every empty answer has the same shape as a populated one, so you can build and test parsers against it. Responses can carry fields that are not listed here. Ignore fields you do not recognise; do not build a parser that rejects them. ## Endpoint groups [#endpoint-groups] | Group | Purpose | Key endpoints | | ------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **System** | Liveness, build, config, chain head. | `GET /health/live`, `GET /version`, `GET /config`, `GET /chain/head` | | **Tokens** | Directory (with the `verified` curation flag), price, chart, per-token pools/swaps, MOTO supply. | `GET /tokens`, `GET /token/{addr}/price`, `GET /tokens/{addr}/chart`, `GET /tokens/{addr}/stats`, `GET /tokens/{addr}/swaps`, `GET /tokens/{addr}/pools`, `GET /tokens/moto/supply` | | **MOTO** | Where MOTO sits: staked by lock option, in pools. | `GET /moto/overview` | | **DEX** | Pool list / detail / chart / holder. | `GET /dex/pools`, `GET /dex/pools/{pair}`, `GET /dex/pools/{pair}/chart`, `GET /dex/pools/{pair}/swaps`, `GET /dex/pools/holder/{addr}/pairs` | | **Explore** | Discovery lists sorted by cross-pool aggregates. | `GET /explore/tokens`, `GET /explore/farms`, `GET /explore/creators` | | **Analytics** | Global totals, breakdowns, series. | `GET /analytics/overview`, `GET /analytics/breakdown`, `GET /analytics/revenue/series`, `GET /analytics/revshare/series`, `GET /analytics/creator-fees/series`, `GET /stats/series` | | **Farms** | All farm pools (bounded, unpaginated). | `GET /farms` | | **Stake** | Lock option meta only. Per-user positions are chain-direct. | `GET /stake/vaults` | | **Portfolio** | Per-user meta, LP fees, activity watermark. | `GET /portfolio/{addr}`, `GET /portfolio/{addr}/lp-fees`, `GET /portfolio/{addr}/lp-fees/{pair}`, `GET /{addr}/meta` | | **Holdings** | User-agnostic token universe + price book. | `GET /holdings/tokens` | | **Activity** | Global cursor-paginated event feed. | `GET /activity` | | **Swap** | Cacheable candidate pair set for a route pair. | `GET /swap/candidates?tokenIn=&tokenOut=` | | **Points** | Per-user points, leaderboard, public season config. | `GET /points/{addr}`, `GET /points/leaderboard`, `GET /points/weeks`, `GET /points/leagues`, `GET /points/rules`, `GET /points/fees`, `GET /points/pair/{pair}` | | **Rakeback** | Per-user accrual + merkle claim proofs. | `GET /rakeback/stats`, `GET /rakeback/{addr}`, `GET /rakeback/{addr}/epoch/{epoch}`, `GET /rakeback/{addr}/proof` | | **PnL** | Per-user PnL, per token and per farm. | `GET /pnl/{addr}/token/{token}`, `GET /pnl/{addr}/farm/{chef}/{pid}` | | **Referrals** | Per-user bindings + attribution. | `GET /referrals/{addr}`, `POST /referrals/attribute` | | **Motocats** | Motocat ids held in a wallet. | `GET /motocats/{addr}` | | **Profiles** | Public wallet profile (Motocat picture, X handle). | `GET /profile/{addr}`, `GET /profiles?addresses=` | | **Creator** | Per-creator and per-token creator-fee accruals. | `GET /creator/{addr}`, `GET /creator/token/{token}` | | **PvE** | PvE vault claims + per-token escrow. | `GET /pve/wallet/{addr}`, `GET /pve/creator/{addr}`, `GET /pve/token/{token}` | | **Launches** | Launched-token metadata and logos. | `GET /launchpad/recent`, `GET /launchpad/token/{addr}`, `GET /launchpad/token/{addr}/logo` | A farm position is addressed by chef and pid: `GET /pnl/{addr}/farm/{chef}/{pid}`. The older `/pnl/{addr}/farm/{pid}` form answers `404`. Take `chef` from the pool's row in `GET /farms`. ## Pool chart ranges [#pool-chart-ranges] `GET /dex/pools/{pair}/chart` takes `range`, one of `24h`, `7d`, `30d` or `1y`, defaulting to `24h`. Anything else fails validation. Bucket width follows the range, so a long window stays a bounded response rather than a year of minutes. The width is echoed back as `bucketMinutes`. | `range` | Bucket | `bucketMinutes` | Rows | | ------- | ---------- | --------------- | ----------- | | `24h` | 1 minute | `1` | up to 1,441 | | `7d` | 15 minutes | `15` | up to 673 | | `30d` | 1 hour | `60` | up to 721 | | `1y` | 1 day | `1440` | up to 366 | Every range arrives whole. Rows are sparse: a bucket with no trade has no candle. Candles are OHLC only. `minute` is a number, the bucket's start **in unix minutes, not seconds**, so multiply by 60 for a timestamp. Read the spacing off `bucketMinutes` rather than assuming it. ```json { "bucketMinutes": 1, "candles": [ { "minute": 29828778, "open": "2.4", "high": "2.6", "low": "2.3", "close": "2.5" }, { "minute": 29828779, "open": "2.5", "high": "2.7", "low": "2.5", "close": "2.6" } ] } ``` The per-token chart, `GET /tokens/{addr}/chart`, is a different route with its own frames. These four ranges are the pool chart only. ## Non-public routes (do not integrate against) [#non-public-routes-do-not-integrate-against] The backend also mounts operational routes: deep health checks, scheduled jobs, admin controls, an indexer webhook and the web app's own RPC proxy. They are token-gated or rate-limited for the app, and they change without notice. Build only against the routes listed on this page and in the full inventory. ## Wire types cheat sheet [#wire-types-cheat-sheet] | Domain | Runtime | Wire (JSON) | Example | | --------- | ------------------------------- | --------------------- | ----------------------- | | `Address` | `` `0x${string}` `` (lowercase) | string | `"0xa0b8…eb48"` | | `TxHash` | `` `0x${string}` `` | string | `"0x…"` | | `Wei` | `bigint` | decimal string | `"1000000000000000000"` | | `Usd` | `bigint` (µ-USD) | decimal-dollar string | `"2500.50"` | | `Seconds` | `number` | number | `1723456789` | | `Percent` | `number` | number | `10.5` | | `*Bps` | `number` | integer | `16261` (162.61%) | * Parse `Wei` strings with `BigInt(value)`, never `Number(value)`. * USD `0` is `"0"`, never `null`. Uncomputable USD (unknown price → 0) is also `"0"`. * Truly-unknown metric-space fields (`priceUsd`, `aprBps`, delta `*Pct`, `decimals`) stay `null` when uncomputable - rendering `0` would mislead. (Exception: `/holdings/tokens` serves a curated set whose `decimals` is always known, so that field is non-nullable there.) ## Caching / ETag semantics [#caching--etag-semantics] Cached GETs emit `Cache-Control` and a weak `ETag`. Send the tag back verbatim in `If-None-Match` and expect `304`. `/config` carries `Cache-Control` but no `ETag`. `/health/live`, `/version` and every write are `private, no-store`. Tiers: | Tier | max-age | swr | Where | | ----------- | ------- | ---- | ----------------------------------------------------------------------------------------------------- | | `HEARTBEAT` | 1s | 9s | `/chain/head` | | `BLOCK` | 3s | 9s | Indexer-driven routes (incl. prices) | | `CRON_FAST` | 15s | 45s | `/profiles` | | `CRON` | 30s | 90s | `/analytics/breakdown`, `/swap/candidates`, `/explore/creators`, points leaderboard / weeks / leagues | | `STATIC` | 300s | 900s | `/config`, `/points/rules`, `/rakeback/stats`, past-epoch Rakeback, Rakeback proofs | The header is `public, max-age=N, s-maxage=N, stale-while-revalidate=M`. Per-user routes key on the wallet's `last_activity_block`, so polling is free until the user transacts. See [`guides/caching-and-etags`](/dev/guides/caching-and-etags). ## Pagination [#pagination] Cursor lists return `` `{ items, nextCursor, hasMore }` `` (`/explore/farms` names its array `farms`, `/explore/creators` names it `creators`). The cursor is an HMAC-signed token bound to the endpoint * filters. Cannot be replayed across queries. The per-token and per-pool swap and pool lists take `?page=` instead. See [`guides/cursor-pagination`](/dev/guides/cursor-pagination). ## Error envelope [#error-envelope] Handler errors: ```json { "error": "message string" } ``` A path or query value that fails schema validation (a malformed address, for example) can answer with the validator's own shape instead. Branch on the status code, not the body: ```json { "success": false, "error": { "name": "ZodError", "message": "..." } } ``` | Status | Meaning | | ------ | ------------------------------------------------------------- | | `400` | Bad request (validation failed, invalid address / range). | | `403` | Tampered/replayed cursor, signature mismatch, forbidden. | | `404` | Not found (unknown pair, unknown route). | | `429` | Per-IP rate limit. Carries `Retry-After`. | | `503` | Temporarily degraded (indexer lag, missing config, upstream). | `4xx` and `5xx` responses are **never cached** (middleware overrides `Cache-Control` to `no-store`). ## CORS [#cors] The API's CORS policy is set for the Motoswap app origin. From another origin, a plain `GET` with no custom headers is readable in a browser, but any request the browser preflights is blocked: the preflight answer carries no `Access-Control-Allow-Origin` for your origin. That covers a `fetch` where your script sets `If-None-Match`, and every JSON `POST`. Conditional polling with an explicit `If-None-Match` must run server-side, or rely on the browser's own HTTP cache, which revalidates without a preflight. ## Rate limiting [#rate-limiting] Routes are rate-limited per IP, and under heavy load the backend sheds requests. Both refusals are `429` or `503` carrying `Retry-After`. Honor the header and back off. In steady state you should never see it - ETag caching keeps polls near zero. # Architecture (/dev/architecture) Motoswap has three parts: contracts, an indexer and a REST API. You will use all three, and knowing how they relate explains why API data can trail the chain by a block. Deployment status is per contract, and [Contract addresses](/dev/addresses) owns the table. On Ethereum (chain `1`), MOTO, the launch farming contract (`VampChef`), `UniswapZap`, `RewardVestingEscrow`, `MotoStaking`, `TreasuryVesting` and `MotocatStaking` went out first. The DEX contracts (factory, router, `FeeRouter`, pairs, zap, `Collector`, `RakebackV2`, `MasterChef`, the creator fee pair, the moto.fun contracts) went out with the DEX. Read every address from `/config`: a key that reads `0x0` is unset for the current phase. ## The three parts [#the-three-parts] ### 1. Contracts (on-chain, canonical) [#1-contracts-on-chain-canonical] Every write goes to a Solidity contract. Every state change emits an event. There is no off-chain database of writes; the chain is the database. The contracts split into families: * **DEX core** - `MotoSwapFactory`, `MotoSwapPair`, `MotoSwapRouter02`, `MotoSwapLibrary`. * **Fees** - `FeeRouter` (user front door), `Collector` (revenue splitter), `QuoteAssetRegistry` (which tokens are fee-side), `RakebackV2` (merkle claim), `BuybackBurner` / `BuybackDistributor` (buyback + Merkle drop), `CreatorFeeRegistry` / `CreatorFeeVault` (per-token creator fees). * **Farming** - `VampChef` (launch farming, ended: Uniswap V2 LP or MOTO → MOTO, one flat window), `MasterChef` (deployed, not running: no emission campaign, none planned), `RewardVestingEscrow` (180-day vest, shared by both). * **Staking** - `MotoStaking` (locked MOTO + multi-token revshare), `MotocatStaking` (NFT escrow). * **PvE** - `PveVault` (escrows PvE tokens and pays them to Motocat stakers). * **Token / vesting** - `MotoToken`, `TreasuryVesting`. * **Peripherals** - `MotoSwapZap` (single-sided LP on Motoswap), `UniswapZap` (deployed: zap into a Uniswap V2 LP and stake it in the `VampChef`), `Snapshotter`. Full surface: [`contracts`](/dev/contracts). ### 2. Indexer (off-chain, derived) [#2-indexer-off-chain-derived] A QuickNode Streams webhook forwards new blocks (with receipts) to the backend. A per-block handler decodes every event of interest and writes into Postgres in a single transaction. Some tables are then CDC-streamed into RisingWave, which computes derived aggregates (points, leaderboard, pool/token stats, Rakeback per epoch) via materialised views that sink back into Postgres for cheap reads. You never see the indexer directly. But it means: * **All API data is derived from on-chain events.** There is no data on the API that you couldn't reconstruct from the chain yourself. The API just makes it fast and cheap. * **Indexer lag ≈ 1 block**, which is about 12 seconds on Ethereum. `GET /chain/head` lets you check the indexer's tip vs your own RPC's tip if you need to gate on freshness. * **Reorg-safe.** Raw events are block-numbered and in the rollback set; aggregates re-derive from them. So a reorg does not leave the API's numbers "stuck". ### 3. REST API (off-chain, cached) [#3-rest-api-off-chain-cached] A Hono backend, base URL `https://api.motoswap.org`. Serves reads only (list, chart, per-user metric, price, config). Writes go to the chain. * **Cached GETs are ETag-versioned.** Send `If-None-Match` and expect `304`. `/config` is the exception: it carries `Cache-Control` and no `ETag`. Cache tiers: `HEARTBEAT` (1s, chain head), `BLOCK` (3s, indexer-fresh, incl. prices), `CRON_FAST` (15s, `/profiles`), `CRON` (30s, analytics/rakeback), `STATIC` (300s, config). * **List endpoints are cursor-paginated** with HMAC-scope-bound cursors - a cursor cannot be replayed across queries. * **CORS.** Simple GET requests work from a browser on any origin. Requests that need a CORS preflight, such as one where your script sets `If-None-Match`, only pass for the Motoswap app origin, so do conditional polling and any write from a server. ## Write path - a swap end-to-end [#write-path---a-swap-end-to-end] ``` 1. User calls FeeRouter.swapExactETHForTokens 2. FeeRouter: - identifies the "fee side" via QuoteAssetRegistry (WETH here) - skims 0.70% of the WETH amount → Collector - calls MotoSwapRouter02.swap* 3. MotoSwapRouter02: - permissioned via factory.swapAllowed[msg.sender] - calls MotoSwapPair.swap(...) for each hop 4. MotoSwapPair.swap emits Swap + Sync 5. FeeRouter emits Swap (indexed: user, tokenIn, tokenOut; also feeToken, feeAmount). `user` is msg.sender, the payer, not the `to` recipient. --- meanwhile off-chain --- 6. QuickNode Streams forwards the new block + receipts to /webhook. 7. The dispatcher processes the block in a single Postgres tx: - decodes FeeRouter.Swap → writes points_events (actor + referral), rakeback_accrual_events - decodes pair Swap → writes activity (display feed) + pool stats - advances the indexer tip 8. RisingWave CDC-reads the raw tables and re-derives: - rakeback_epoch(user, token, epoch) aggregate - leaderboard sink (points sum) - pool_row / token_row (24h aggregates) 9. Backend routes read from the sinks; ETag = f(chain head + head hash). ``` Your wallet sees the tx receipt long before steps 6 to 8; if you want to know "is the indexer caught up to my tx", poll `GET /chain/head` until `blockNumber >= receipt.blockNumber`. ## Read path - the API is a cache over derived views [#read-path---the-api-is-a-cache-over-derived-views] The API doesn't compute on the request. It reads a materialised `pool_row` / `token_row` / `rakeback_epoch` / etc. row and serialises. This is why: * **A read does no aggregation work.** It is one indexed row read plus serialisation. * **ETag hits are near-free.** The version fold is a tiny SQL read. * **You should cache clientside** - the API tells you exactly when the cache is safe (`Cache-Control` + `ETag`). ## The FeeRouter is the intended front door [#the-feerouter-is-the-intended-front-door] Two paths reach a `MotoSwapPair.swap`: * **FeeRouter → Router02 → Pair** - the intended user path. Charges the 0.70% protocol fee. * **Router02 → Pair** - bypasses `FeeRouter`, but `MotoSwapRouter02` is perimeter-gated via `factory.swapAllowed[msg.sender]`. Only whitelisted callers (of which `FeeRouter` is one) can call it. Direct calls revert `"MotoSwapRouter: SWAPPER_NOT_ALLOWED"` (router) or `"MotoSwap: SWAPPER_NOT_ALLOWED"` (pair) - note the different prefixes if you string-match reverts. **Add-liquidity and remove-liquidity are permissionless** on `Router02` - the perimeter only gates swaps. ## The Collector splits fees dynamically [#the-collector-splits-fees-dynamically] `Collector` doesn't have a hardcoded split. It has a `Bucket[]` list of `(recipient, weight, notify)`. At deploy the split is `2:2:2:1` (staking : treasury : Rakeback : buyback and burn). The owner can reweight, add, or replace buckets - a bucket whose payout reverts is "earmarked" and skipped so the healthy ones aren't reweighted by a broken destination. ## Ownership boundaries [#ownership-boundaries] * **`MotoToken`** - fixed supply, no owner, no mint, no pause. * **UUPS-upgradeable** (with a one-way `renounceUpgradeability`): `MotoSwapFactory` (also renounces beacon separately), `MotoSwapPair` (via beacon), `FeeRouter`, `RakebackV2`, `Collector`, `MasterChef`, `VampChef`, `MotoStaking`, `PveVault`, `MotocatStaking`, `BuybackBurner`, `RewardVestingEscrow`, `TreasuryVesting`. * **Not upgradeable** (`Ownable2Step`): `QuoteAssetRegistry`, `CreatorFeeRegistry`, `CreatorFeeVault`, `BuybackDistributor`, `Snapshotter`. * **Not upgradeable, no owner**: `MotoSwapZap`, `UniswapZap`. Every UUPS contract exposes `renounceUpgradeability()` - after that call, the implementation is frozen. Track it via the `Upgraded` event on the proxy or by reading the implementation slot. ## Time is Unix seconds, everywhere [#time-is-unix-seconds-everywhere] Every timestamp on the API is a Unix-seconds integer (not `Date`, not `timestamptz`). This mirrors `block.timestamp` (chain-native). Format for display only. The full domain-type cheat sheet lives in [addresses, config and events](/dev/reference/addresses-config-events). ## The indexer and database [#the-indexer-and-database] The indexer and database are internal. You don't run them; the REST API is your interface. # Brand Kit (/dev/brand-kit) Click any asset to download it. ## Wordmark [#wordmark]
Motoswap wordmark, light background Motoswap wordmark, dark background
* Download `light-logo-text.svg` - for light backgrounds * Download `dark-logo-text.svg` - for dark backgrounds ## Icon mark [#icon-mark]
Motoswap icon, light background Motoswap icon, dark background
* Download `light-logo.svg` - for light backgrounds * Download `dark-logo.svg` - for dark backgrounds ## MOTO token icon [#moto-token-icon]
MOTO purple Purple MOTO dark purple Dark purple MOTO black Black
* Download `purple-token-icon.svg` - primary variant * Download `dark-purple-token-icon.png` - on-dark * Download `black-token-icon.svg` - monochrome ## Motocats [#motocats] Motocat * Download `motocat.png` # Contracts (/dev/contracts) Every Solidity contract in the Motoswap system, grouped by concern. Some are not deployed (retired or unscheduled contracts); the [addresses](/dev/addresses) page has the status of each. Full per-contract surface (functions, events, errors, structs, access controls, notes) is in [`reference/contracts-full-inventory`](/dev/reference/contracts-full-inventory). Fetch live addresses from `GET /config` (see [`addresses`](/dev/addresses)). `/config.addresses` has a key for every contract in this inventory: the DEX core, fees, staking, the Motocat set, the buyback and vesting peripherals, and the phase venues (`vampChef`, `uniswapZap`, `uniswapV2Router`, `uniswapV2Factory`). The only contract not exposed is the `Snapshotter` (operational tooling). A zero address means the contract is unset for the current `launchPhase`. Never integrate against a zero address. ## Deployment status [#deployment-status] Status is per contract. The [deployment status table](/dev/addresses#deployment-status-on-ethereum-chain-1) on the addresses page says, for each one, whether the page pins its address or you read it from `/config`. While `launchPhase` was `vamp`, `/config` served the launch farm address under both `vampChef` and `masterChef`. The launch farm is a `VampChef`, not a `MasterChef`: use the `VampChef` ABI for it. The reference describes the contracts at the current source. Some deployed proxies run an implementation that predates it, and the inventory marks the functions that are not on the deployed implementation yet. They arrive with a contract upgrade. ## DEX core [#dex-core] | Contract | What it does | Upgradeable | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | | `MotoSwapFactory` | Deploys pairs (beacon proxies at deterministic CREATE2 addresses; the init code hash is `pairCodeHash()`, read it live). Holds the swap perimeter (`swapAllowed`) and the pair fee (`swapFeeBps`). | UUPS (factory + separately renounceable pair beacon). | | `MotoSwapPair` | Per-pair AMM. Uniswap-V2 math with an EIP-1153 transient-storage reentrancy lock and the fee read live from the factory. `swap` is perimeter-gated and flash swaps are closed (`MotoSwap: FLASH_SWAPS_CLOSED`). | Beacon (all pairs upgrade in one call). | | `MotoSwapERC20` | LP-token base (inherited by pair). ERC-20 + EIP-2612 permit. | Baked into pair impl. | | `MotoSwapRouter02` | Uniswap-V2-shape router. Add/remove liquidity permissionless; every swap entrypoint requires `factory.swapAllowed(msg.sender)`. | Immutable. | | `MotoSwapLibrary` | Pure/view helpers (`sortTokens`, `pairFor`, `quote`, `getAmountOut`/`In`, `getAmountsOut`/`In`). | Immutable. | ### The swap perimeter [#the-swap-perimeter] `MotoSwapPair.swap` and every `MotoSwapRouter02` swap entrypoint require `factory.swapAllowed(msg.sender)`. The deploy admits two addresses: the core router and the `FeeRouter`. Outside integrators swap through the `FeeRouter` and pay the full fee. See [`swap-via-fee-router`](/dev/guides/swap-via-fee-router). ### The pair fee [#the-pair-fee] `factory.swapFeeBps` is one global value that every pair reads live on each swap. The contract default, set in `initialize`, is 100 bps. The deploy scripts then call `setSwapFee(30)` and `setFeeTo(address(0))`, so a pair charges 0.30% and all of it accrues to LPs. The factory admin can move it anywhere from 0 to `MAX_SWAP_FEE_BPS = 200`, so read `swapFeeBps()` live rather than quoting a number, and use `MotoSwapRouter02.getAmountsOut`, which already applies it. ## Fees [#fees] | Contract | What it does | Upgradeable | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | | `FeeRouter` | The swap entrypoint for users and integrators. Exact input only, no quote view. Charges the protocol fee (`protocolFeeBps`, owner-set, launch value 70 bps, cap 100 - read it live) on the quote leg, then routes through `MotoSwapRouter02`. Also skims the creator fee. | UUPS. | | `Collector` | Fee splitter. Receives quote-asset fees, splits into weighted buckets (staking notify / treasury / Rakeback / buyback and burn). Handles per-bucket earmarks on failed payouts. | UUPS. | | `QuoteAssetRegistry` | The allowlist + rank of quote assets (WETH/USDT/USDC/MOTO). | Immutable Ownable2Step. | | `RakebackV2` | Merkle claim contract for accrued Rakeback. One root per weekly epoch, posted on chain by an automated poster; a claim is a proof against it, with no signature and no off-chain service on the claim path. | UUPS. | | `BuybackBurner` | MOTO buyback and burn to `0x…dEaD`, driven by a keeper role. Every sale runs a pinned route and must clear the contract's own TWAP floor. | UUPS. | | `BuybackDistributor` | Merkle-tree distributor with a fixed allocation and a claim deadline. | Immutable Ownable2Step. | | `CreatorFeeRegistry` | Allowlist of tokens eligible for creator fees + their creator addresses. | Immutable Ownable2Step. | | `CreatorFeeVault` | Per-token, per-quote-asset accrual escrow for creator fees. | Immutable Ownable2Step. | ## Farming [#farming] | Contract | What it does | Upgradeable | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | `VampChef` | The launch farming contract: stake Uniswap V2 LP tokens, earn MOTO. One flat 14-day campaign (`DURATION`), instant withdraw, no deposit fee. Rewards paid 50% liquid / 50% streamed. | UUPS. | | `MasterChef` | Deployed; no emission campaign is running and none is planned. Contract reference: fixed-supply LP-farming with halving emissions (halving period is settable state, seeded at 42 days; read it live). 24-hour withdraw cooldown while the pool earns (none at weight 0); withdraw is queue then claim. Rewards paid 50% liquid / 50% streamed. | UUPS. | | `RewardVestingEscrow` | One 180-day linear schedule per wallet for the streamed half of farm rewards. Each new deposit banks what was released and re-spreads the rest over a fresh 180 days. | UUPS. | ## Staking [#staking] | Contract | What it does | Upgradeable | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ | | `MotoStaking` | Locked MOTO staking (3/6/12/24 mo, 1x/2x/4x/8x boost) with multi-token revshare from `Collector`. | UUPS. | | `MotocatStaking` | Pure NFT escrow with per-block checkpoints; no rewards surface (PvE airdrops pay from `PveVault` against its snapshots). Feeds the Points boost. | UUPS, with a one-way `renounceUpgradeability()`. | | `PveVault` | Escrow holding PvE-bought tokens for Motocat stakers. One credit per launched token; stakers claim pull-based via `claimable` / `claim` / `claimMany`. | UUPS. | ## moto.fun [#motofun] The bonding-curve launch surface. Deployed separately from the DEX set, and served by its own `/config` keys. Full detail: [moto.fun contracts](/dev/fun/contracts). | Contract | What it does | Upgradeable | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | | `LaunchpadCurve` | The singleton. Every coin's curve lives here: launch, buy, sell, graduate, PvE escrow, fee routing. Graduation seeds TOKEN/WETH + TOKEN/MOTO and burns both LP positions. | Immutable Ownable2Step. | | `LaunchpadToken` | The coin template: 1B fixed supply, no owner, no tax, deployed as an EIP-1167 clone per launch. | Immutable. | | `LaunchpadLens` | Read-only companion: quotes, spot price, bonding progress, graduation readiness. Split out to fit EIP-170. | Immutable. | `CreatorFeeRegistry` and `CreatorFeeVault` above are shared: the curve is a registrar on the first and a depositor on the second, and the `FeeRouter` is a depositor too, so a coin's creator claims curve fees and DEX fees through the same vault. ## Token & vesting [#token--vesting] | Contract | What it does | Upgradeable | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | `MotoToken` | Fixed 10B supply MOTO with EIP-2612 permit. No owner (`owner()` returns the zero address), no mint. Carries a launch gate that closed for good when trading opened: `tradingOpen()` returns `true` on the deployed token and cannot go back, so it behaves as a plain ERC-20. | Immutable. | | `MotoTokenUnwrapper` | Helper deployed by `MotoToken`. Unwraps WETH to ETH for the token's own `sell` path. Callable by the token only. | Immutable. | | `TreasuryVesting` | Step-vester: 10 equal tranches, one every 180 days, about 5 years in total (1,800 days). Lazy funding (top-ups join the schedule). | UUPS. | ## Peripherals [#peripherals] | Contract | What it does | Upgradeable | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | | `MotoSwapZap` | Single-sided liquidity entry into Motoswap pairs + optional zap-and-stake into `MasterChef`, which has no emission campaign running. Swap leg goes through the `FeeRouter`. Rejects fee-on-transfer tokens. | Immutable. | | `UniswapZap` | The same for Uniswap V2 pairs, staking into the launch farm. No owner, no state. Note its `AndStake` argument order differs from `MotoSwapZap`. | Immutable. | | `Snapshotter` | One-shot on-chain snapshot marker (block + timestamp) for off-chain census jobs. | Immutable Ownable2Step. | ## Ownership + upgrade model [#ownership--upgrade-model] ### UUPS contracts [#uups-contracts] Have a one-way `renounceUpgradeability()` - call it and the implementation is frozen forever. `upgradesRenounced()` reads the state. Seven of them split the upgrade path from ownership: `RakebackV2`, `Collector`, `BuybackBurner`, `MasterChef`, `RewardVestingEscrow`, `MotoStaking` and `PveVault` expose `upgradeAuthority()` and `setUpgradeAuthority(address)`. While the authority is zero the owner upgrades. Once set, only the authority can upgrade, renounce, or move the authority, and it can never be set back to zero. The upgrade authority is a timelock. Its delay is zero for the first 72 hours after the DEX opens, then 24 hours. `FeeRouter`, `VampChef` and `TreasuryVesting` keep upgrades on the owner, and the factory keeps them on `feeToSetter`. ### Beacon (pair) [#beacon-pair] `MotoSwapFactory.renouncePairUpgradeability()` freezes every live pair at once by renouncing beacon ownership. ### Ownable2Step [#ownable2step] Ownership transfers are two-step (`transferOwnership` → beneficiary calls `acceptOwnership`) or, for the factory, `setFeeToSetter` → `acceptFeeToSetter`. In the current source every ownable contract is two-step, `FeeRouter`, `RewardVestingEscrow` and `TreasuryVesting` included. The deployed `RewardVestingEscrow` and `TreasuryVesting` implementations predate that change: they have no `pendingOwner()` until a contract upgrade. ## Where each contract lives in the docs [#where-each-contract-lives-in-the-docs] * **Deep dive** → [`reference/contracts-full-inventory`](/dev/reference/contracts-full-inventory) has every function, event, error, struct, constant per contract. * **Guides** → [Swap via FeeRouter](/dev/guides/swap-via-fee-router) and the other guides walk each end-to-end flow through the relevant contracts. * **Fee constants** → [`reference/addresses-config-events#4-fee-schedule--rakeback-constants`](/dev/reference/addresses-config-events#4-fee-schedule--rakeback-constants). * **Event signatures** → [`reference/addresses-config-events#5-events---integrator-reference`](/dev/reference/addresses-config-events#5-events---integrator-reference). # Glossary (/dev/glossary) Terms used across Motoswap docs and code. Grouped alphabetically. ## A to C [#a-to-c] ### A to B [#a-to-b] **Actor Points** - Points the trader earns on their own fee-paying swap, boosted by staked Motocats. The scoring internals are not published; read balances from `GET /points/{address}` after the daily drop. **Address (branded)** - `Address` type in the SDK/API is always a lowercased 0x-prefixed 20-byte hex string. Never mixed-case (checksum) on the wire. **Alloc points** - pool weight in `MasterChef`. A pool earns `allocPoint / totalAllocPoint` of the current emission rate. `MasterChef` is deployed; no emission campaign is running and none is planned. **Beacon proxy** - the pattern Motoswap uses for pairs. Every pair is a minimal ERC-1967 proxy pointing at one shared beacon. One `upgradePairImpl` call retargets every live pair at once. **Boost (Motocat)** - the multiplier a swap's points earn from the wallet's Motocat count (staked only - a held cat counts for nothing). Read by the indexer at the block before the swap. **Boost (MotoStaking)** - multiplier on effective stake per lock duration: 1x / 2x / 4x / 8x for 3/6/12/24 months. **Bps** - basis points; 100 bps = 1%. **Bucket (Collector)** - one destination in `Collector.buckets[]`, with a `recipient`, `weight`, and `notify` flag. **Bytes32 / hex32** - a 32-byte value. Used for tx hashes, salts, permit digests. Wire type: `0x`-prefixed 64 hex chars. ### C [#c] **Campaign (MasterChef)** - a single emission run (`startEmission → emissionStart..emissionEnd`). Only one campaign at a time. None is running and none is planned. **Chain head** - the current highest processed block on the indexer. Served by `GET /chain/head` - used to gate freshness on client reads. **Collector** - the fee-splitter contract. Receives protocol fees from `FeeRouter`, splits into weighted buckets on `distribute(token)`. **Cooldown (MasterChef)** - 24 hours between `withdraw` (queue) and `claimWithdrawal` (claim) while the pool earns. None when no campaign is emitting or the pool's weight is 0. The deployed `VampChef` has no cooldown: its `withdraw` returns principal in the same call. **CREATE2** - deterministic contract deployment (address = `hash(deployer, salt, initcode)`). Used by `MotoSwapFactory` (pair addresses). **Creator (fees)** - the entity registered in `CreatorFeeRegistry` for a given token. Earns `creatorFeeBps` (max 50 bps = 0.50%) on every swap of that token, in the quote asset. **Cron version** - ETag component derived from `max(cron_state.last_run_at)` for a set of cron jobs. Used to version routes that depend on cron-refreshed data. **Cursor** - opaque HMAC-signed pagination token. Scope-bound to the endpoint + filters - cannot be replayed across queries. ## D to F [#d-to-f] **Decimals** - the `decimals()` value on an ERC-20. USDC = 6, MOTO/WETH/most = 18. **Deposit fee (MasterChef)** - 0-1000 bps (up to 10%) skimmed on `deposit`, paid to `feeRecipient`. Set per pool by owner. **Effective stake (MotoStaking)** - `amount × (10_000 + boostBps) / 10_000`. Drives reward share. **EIP-712** - typed structured data signing standard. Used for permit and for moto.fun launch signatures. Rakeback does not use it. **Epoch (Rakeback)** - one Sunday-anchored week: `floor((seconds − 259200) / 604800)` (the offset shifts the Thursday Unix epoch start to Sunday 00:00 UTC). Each epoch gets its own merkle root; the `50 / 25 / 25` vest is baked into the leaf, and the claim window is 30 days from the slice's activation. **ETag** - HTTP cache validator on cached GETs. Send `If-None-Match` with the previous value and expect `304`. `/config` carries `Cache-Control` and no `ETag`. **FeeRouter** - user front door for swaps. Charges the protocol fee (`protocolFeeBps`, 0.70%, owner-settable, so read it live) on the quote leg, then routes through `MotoSwapRouter02`. **FeeToSetter** - admin role on `MotoSwapFactory` (two-step transferable). Also the beacon owner. Can set swap fee, pair implementation, swap allow-list. **FoT** - fee-on-transfer token. A token that taxes its own `transfer` / `transferFrom`. Handled by dedicated `*SupportingFeeOnTransferTokens` variants; explicitly rejected by `MotoSwapZap`. ## H to L [#h-to-l] **Halving curve (MasterChef)** - the contract's emission math. No campaign is running and none is planned. Period k emits `initialPeriodReward >> k` over `halvingPeriod` (default 42 days). 8 periods default → \~336 days total. **HMAC** - the pagination cursor is HMAC-SHA-256 signed. **Indexer** - the QuickNode → Postgres → RisingWave pipeline that turns on-chain events into the API's derived tables. **Launch farming** - Motoswap's launch farm, now ended. You staked Uniswap V2 LP tokens into the `VampChef` and earned MOTO; your liquidity stayed on Uniswap. It ran for 14 days (`DURATION()` on the chef). There was no protocol fee before the DEX opened, because `FeeRouter` was not deployed. **launchPhase** - the `/config` field that says which step of the launch the deployment is in: `prelaunch`, `vamp` (the launch farming contract is deployed, the DEX contracts are not) or `dex` (the DEX contracts are deployed). A contract that is unset for the current phase reads `0x0` from `/config`. **JIT (just-in-time)** - an attacker stakes right before a reward event and unstakes right after. Motocat PvE defeats it with the snapshot rule: a credit is sized against staked balances at the **block before** it landed (`stakedBalanceOfAt(wallet, creditBlock - 1)`), so a same-block stake earns nothing. No warmup window exists or is needed. **Keyset pagination** - cursor-based pagination on a sort key (e.g. `(tvl_usd, pair_address)`). Alternative to offset. Consistent under concurrent writes. **LP token** - the ERC-20 receipt from adding liquidity. In Motoswap, the `MotoSwapPair` itself IS the LP token (`MotoSwapERC20`, symbol `MOTO-V1-LP`). ## M to O [#m-to-o] **µ-USD** - 6-decimal-integer USD. Wire representation is a decimal string of dollars (e.g. `"2500.50"` for $2,500.50). Runtime is bigint µ-USD (`2500500000n`). **MOTO** - the protocol reward token, an ERC-20 on Ethereum mainnet with 18 decimals. Fixed supply. No mint, no owner, no pause. Deployed; read its address from `/config.addresses.motoToken`. **Motocat** - the collectible NFT. Stakeable at `MotocatStaking`; staked cats receive PvE airdrop credits (claimed from `PveVault`) and boost swap Points. **Multicall** - batch RPC read pattern; Multicall3 is available at its canonical address. **Notify** - the "reward push" call flag on a Collector bucket. `true` = the bucket recipient is `IStakingRewards` and Collector calls `notifyReward()`. ## P to R [#p-to-r] ### P [#p] **Pair** - the AMM (Uniswap-V2-shape) for one token pair. Address is deterministic via CREATE2 with sorted `(token0, token1)`. **Perimeter (swap-allowed)** - the `factory.swapAllowed[caller]` allowlist. Only listed callers can call `MotoSwapRouter02.swap*`. The deploy lists the core router and `FeeRouter`; outside integrators swap through `FeeRouter`. **Permit** - EIP-2612 signed approval. Supported by every Motoswap LP token (`MotoSwapERC20.permit`) and MOTO (`MotoToken.permit`). **Permit2** - Uniswap's batched-approval router-signature system. Used by `FeeRouter.swap*Permit2` variants. **PoolInfo** - per-pool `MasterChef` state: `` `{ lpToken, allocPoint, lastRewardTime, accRewardPerShare, lpSupply, depositFeeBps }` ``. **Points** - indexer-derived per-user counter, scored per swap into the `points_events` ledger and published once a day at the 00:00 UTC drop. Per-swap deltas are never observable via the API. **Protocol fee** - `protocolFeeBps` on `FeeRouter`, charged on the quote leg. Owner-set (≤ 100 bps ceiling), 70 bps. Read it live. **PvE mode** - a moto.fun launch option. The creator's own ETH buys a slice of the token at the curve price, the slice is escrowed, and it is credited to Motocat stakers in one drop when the token graduates. Shares are claimed from `PveVault`, sized by the staked-cat snapshot at the block before the credit, and never expire. ### Q to R [#q-to-r] **Quote asset** - a token registered in `QuoteAssetRegistry`. The set is WETH/USDT/USDC/MOTO with rank order (`WETH > USDT > USDC > MOTO`) that determines fee-side selection when a path has quote at both ends. **Rakeback** - the trader's share of the protocol fee. `2 / 7` of every `FeeRouter.Swap` fee is earmarked, paid by merkle proof against a weekly epoch root. The `50 / 25 / 25` vest is baked into the leaf amount at post time, and the claim window is 30 days from the slice's activation. Anything unclaimed after that is swept to the treasury. **Referral (Points)** - the referrer earns 10% of a referee's Points, one level, added on top. This covers trading Points on moto.fun too. **RewardVestingEscrow** - 180-day linear vesting for the streamed half of farm rewards (50% liquid / 50% vested). Folding grant: repeated deposits re-spread the tail without resetting the head. **RisingWave** - the streaming SQL engine that CDC-reads from Postgres and maintains derived views (points sum, rakeback per epoch, pool/token 24h stats). You don't see it directly; it powers the API's aggregate reads. **Ratio** - a pool spot ratio (`token0` priced in `token1`). Runtime is a `bigint` at 18-decimal fixed point (`1.0` = `10^18`); the wire form is a decimal string. ## S to Z [#s-to-z] ### S [#s] **Salt** - 32-byte CREATE2 salt. For pairs, `keccak256(sorted token0, token1)`. **Seconds** - Unix seconds (integer). Every timestamp in the API and DB. **Snapshotter** - one-shot on-chain marker (block + timestamp) for off-chain snapshots. **Sync (event)** - `MotoSwapPair.Sync(reserve0, reserve1)`. Fires after every state-changing pair call. Canonical reserves snapshot for indexers. **Swap-allowed** - see `Perimeter`. ### T to Z [#t-to-z] **Total effective stake** - sum of all `MotoStaking` positions' effective stake. Denominator for reward share. **TVL** - total value locked. On the API it is a decimal-dollar string (for example `"2500.50"`), never `null`; an unknown value reads `"0"`. **Unstake ticket (MotoStaking)** - one entry per `requestUnstake`. Streams principal linearly over `UNSTAKE_DRIP = 30 days`. **UUPS** - upgradeable proxy pattern. Owner authorizes upgrades; each UUPS contract has a one-way `renounceUpgradeability` to freeze. **VampChef** - the launch farming (ended) contract. A trimmed `MasterChef`: Uniswap V2 LP tokens (or MOTO on its own, in pool 1) in, MOTO out, one flat 14-day emission window (`DURATION()` is the source of truth), no withdraw cooldown, no deposit fee. While `launchPhase` was `vamp`, the `vampChef` and `masterChef` keys of `/config` both held its address. **Vault (MotoStaking)** - colloquial for one of the four lock duration options. Not a separate contract - `MotoStaking` has all four. **Watermark (user activity)** - `user_activity.last_activity_block`. The indexer bumps it on any event the user is a party to. Per-user API routes ETag on it, so polls are `304` until the user transacts. **WETH** - canonical wrapped ether. Mainnet: `0xC02a…6Cc2`. Always resolve it from `/config.addresses.weth`. **Wire type** - the JSON serialization of a domain type. `Address` → string. `Wei`/`Usd` → decimal string. Numbers → number. Full cheat sheet: [addresses, config and events](/dev/reference/addresses-config-events). **Zap** - `MotoSwapZap`. Single-sided liquidity provisioning - swap optimal half, add balanced LP, refund dust. # Overview (/dev/overview) Motoswap is a Uniswap-V2-style AMM plus a set of surrounding modules (farming, staking, Rakeback, token launches, NFT staking) with a public REST API served from a Hono backend. Everything is open to external integrators. ## Which contracts are deployed [#which-contracts-are-deployed] Deployment status is per contract, and [Contract addresses](/dev/addresses) owns the table. In short, on Ethereum (chain `1`): * **Deployed:** `MotoToken` (MOTO), the launch farming (ended) contract (`VampChef`, which took Uniswap V2 LP tokens or MOTO on its own and paid MOTO over one 14-day window), `UniswapZap`, `RewardVestingEscrow`, `MotoStaking`, `TreasuryVesting`, the Motocat collection and `MotocatStaking`. * **Deployed with the DEX:** `MotoSwapFactory`, `MotoSwapRouter02`, `FeeRouter`, pairs, `MotoSwapZap`, `Collector`, `QuoteAssetRegistry`, `RakebackV2`, `MasterChef`, the creator fee pair, `PveVault`, `BuybackBurner`, and the moto.fun contracts. Read their addresses from `GET /config`. A key that reads `0x0000000000000000000000000000000000000000` from `GET /config` is unset for the current phase. Do not integrate against any address `/config` did not give you, and never send a transaction to a zero address. ## What you can do [#what-you-can-do] * **Trade** - quote and execute swaps through `FeeRouter` (the fee-enforcing front door) or `MotoSwapRouter02` (the raw V2 router - gated by a per-caller perimeter, so integrators route through `FeeRouter` in practice). * **Provide liquidity** - call `addLiquidity` / `removeLiquidity` on `MotoSwapRouter02`, or use `MotoSwapZap` for single-sided entry. * **Farm** - no farm program is running or planned. `VampChef`, the launch farming (ended) contract for Uniswap V2 LP or MOTO on its own, ran its one flat 14-day window, and its rewards paid half liquid and half through `RewardVestingEscrow` over 180 days. `MasterChef` is deployed but not running: no emission campaign is running and none is planned. LP already staked in it can be withdrawn at any time. * **Stake MOTO** - lock MOTO in `MotoStaking` (3 / 6 / 12 / 24 months, boost 1× / 2× / 4× / 8×) and collect multi-token revshare from `Collector`. The deployed implementation has no reward tokens registered yet; registering them arrives with a contract upgrade, and until then the staking share is held at the `Collector` (see the [MOTO staking guide](/dev/guides/stake-moto)). * **Stake Motocat NFTs** - escrow-stake into `MotocatStaking`; staked cats receive PvE airdrop credits (claimed from `PveVault`, snapshot-sized, no expiry) and boost your swap Points. * **Launch tokens** - on moto.fun. * **Claim Rakeback** - 2/7 of each protocol fee goes to Rakeback, paid weekly (epochs roll Sunday) against an on-chain merkle root. The 50/25/25 vest is baked into the leaf. Unclaimed Rakeback expires after 30 days and is swept to the treasury. * **Read state** - every pool / token / farm / vault / user metric is served from the REST API (`https://api.motoswap.org`) with ETag caching. Chain state is also readable directly via RPC. ## The mental model [#the-mental-model] ``` ┌──────────────────────────────────────────────────────────┐ │ User's wallet │ └───────────┬──────────────────────────────┬───────────────┘ │ │ writes: swap, LP, stake, claim… reads: prices, TVL, positions, points │ │ ▼ ▼ ┌───────────────────────┐ ┌────────────────────────┐ │ FeeRouter (front door)│ │ Backend REST API │ │ ├── protocol fee │ │ api.motoswap.org │ │ └── router.swap … │◀──────▶│ /config /chain/head │ └────────┬──────────────┘ │ /tokens /dex /farms │ │ │ /portfolio /rakeback… │ │ └──────────┬─────────────┘ ▼ │ ┌───────────────────────┐ │ │ Motoswap V2 core │ emits events │ │ Factory · Pair · Rtr │─────────────────▶ │ └──────────┬────────────┘ │ │ split fees │ ▼ │ ┌───────────────────────┐ │ │ Collector │─────┐ │ └─┬────┬────┬────┬─────┘ │ │ ▼ ▼ ▼ ▼ │ │ Stake Treas Rake Buy │ the indexer streams events into -ing -ury -back back │ Postgres + RisingWave; the API │ reads from those. ┌──────────────────────────────────────────────────────────┐ │ MasterChef / MotoStaking / etc. │ │ (independent modules) │ └──────────────────────────────────────────────────────────┘ ``` Every meaningful state change happens **on-chain**, is picked up by the indexer, and is queryable via REST within seconds. There is no off-chain state you can't reconstruct from the chain. ## The three surfaces [#the-three-surfaces] ### 1. Smart contracts (write path) [#1-smart-contracts-write-path] Solidity contracts on Ethereum (see [`addresses`](/dev/addresses)). Call them directly with viem / ethers. Most-used entrypoints: | Contract | What it does | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `FeeRouter` | User-facing swap entrypoint. Charges the 0.70% protocol fee on the quote leg, then routes through `MotoSwapRouter02`. | | `MotoSwapRouter02` | Uniswap-V2-shaped router (liquidity add/remove; swap variants are perimeter-gated). | | `MotoSwapPair` | Per-pair AMM. Emits `Swap`/`Mint`/`Burn`/`Sync`. | | `MotoSwapZap` | Single-sided LP entry. Optionally stake into a farm in one call. | | `VampChef` | The launch farming (ended) contract. Uniswap V2 LP (or MOTO on its own, in pool 1) in, MOTO out, one flat 14-day emission window (`DURATION()`), no withdraw cooldown. | | `MasterChef` | Motoswap LP farm contract. Deployed; no emission campaign is running and none is planned. | | `MotoStaking` | MOTO lock/boost + multi-token revshare from `Collector`, once a contract upgrade registers the reward tokens on the deployed implementation. | | `MotocatStaking` | Motocat NFT escrow with per-block checkpoints; PvE airdrops pay from `PveVault` against its snapshots. | | `RakebackV2` | Merkle-proof claim of accrued Rakeback, one root per weekly epoch. | Every contract's full ABI, functions, events, errors and access controls are in [`reference/contracts-full-inventory`](/dev/reference/contracts-full-inventory). ### 2. REST API (read path) [#2-rest-api-read-path] Every list, chart, price, per-user metric and derived stat is served from the backend. Base URL: ``` https://api.motoswap.org ``` Highlights: | Endpoint | Purpose | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET /config` | Every contract address (`0x0` when unset for the phase), `chainId` and `launchPhase`. | | `GET /chain/head` | Current indexer tip (block # + timestamp). | | `GET /tokens` | Token directory (address, symbol, name, decimals). | | `GET /token/{addr}/price` | One token's USD price. | | `GET /tokens/{addr}/chart` | Price candles + volume window. Empty for a token with no Motoswap trading history. | | `GET /dex/pools` | Cursor-paginated pool list, TVL-sorted. | | `GET /explore/tokens` | Cursor-paginated token discovery. | | `GET /farms` | All farm pools (unpaginated - bounded set). | | `GET /holdings/tokens` | User-agnostic token universe + prices. | | `GET /{addr}/meta` | Per-user activity watermark (`lastActivityBlock`). | | `GET /portfolio/{addr}/lp-fees` | LP fees earned, one row per pair. | | `GET /rakeback/{addr}` | Accrued Rakeback (per epoch), with claim proofs. Empty for every wallet until the first weekly root is posted, and after that for a wallet that paid no protocol fee. | | `GET /rakeback/{addr}/proof` | Merkle proofs for every published leaf. | | `GET /points/{addr}` | Per-user points + rank. | | `GET /swap/candidates` | Candidate pool set for a token→token route. | Full endpoint reference: [`reference/api-full-inventory`](/dev/reference/api-full-inventory). Cached GETs carry `Cache-Control` and a weak `ETag`: send the tag back in `If-None-Match` and expect `304`. Two exceptions: `/config` carries `Cache-Control` but no `ETag`, and `/health/live` and `/version` are `private, no-store`. See [caching](/dev/api#caching--etag-semantics). ### 3. TypeScript SDK [#3-typescript-sdk] The Motoswap TypeScript SDK - pure functions used by the frontend and backend. It has no network I/O; feed it live reserves from your own RPC / API reads and it returns the same math the app uses. The package is not published yet, so the [quickstart](/dev/quickstart) examples do not depend on it. Highlights: * `bestRoute(tokenIn, tokenOut, pairs, amountIn, maxHops)` - the canonical multi-hop router. * `optimalSwapInput` - closed-form zap math. * `priceImpactBps` - sandwich-safe price-impact bps. * `amountOutMin`, `amountInMax`, `submitAmountOutMin` - slippage helpers. * `computeMasterChefPoolAPR`, `computeVaultAPR` - APR math. Full SDK reference: [`reference/sdk-full-inventory`](/dev/reference/sdk-full-inventory). ## Fee model [#fee-model] These are the values the contracts launched with. Several are owner-settable, so read the live ones from the contracts rather than trusting this page. On a `FeeRouter` trade: * **LP fee: 0.30%** - kept by liquidity providers (V2 pair invariant fee). * **Protocol fee: 0.70%** - charged by `FeeRouter` on the **quote leg only** (`WETH > USDT > USDC > MOTO`). Forwarded to `Collector`, which splits it by weight into four buckets: * **2 / 7 → MotoStaking** (revshare to locked MOTO stakers). * **2 / 7 → Treasury**. * **2 / 7 → Rakeback** (paid to the trader by merkle claim, 50/25/25 vest). * **1 / 7 → buyback and burn**. * **Creator fee: 0% to 0.50% (contract cap)** - optional per-registered-token; same quote leg. Motoswap sets it to `0.30%`; the range here is `MAX_CREATOR_FEE_BPS`, the registry's hard ceiling. **Total: 1.00%** on a one-hop route with no registered moto.fun token: the 0.30% pair fee plus the 0.70% protocol fee. The pair fee is paid per hop and the protocol fee once per trade. Each path endpoint that is a registered moto.fun token adds the creator fee (0.30%), so a route between two registered tokens pays it twice. The trader gets \~0.20% back via Rakeback. The [fee table on the aggregators page](/dev/integrations/aggregators#the-fee-model-in-basis-points) has every combination. Full fee constants: [`reference/addresses-config-events`](/dev/reference/addresses-config-events#4-fee-schedule--rakeback-constants). ## Gas [#gas] This page carries no gas figures. Estimate gas per call against the deployed contracts (`eth_estimateGas`, or viem's `estimateContractGas`) rather than budgeting from a published number. ## Where to go next [#where-to-go-next] * **Building an aggregator, bot, scanner, or AI agent?** → [pick your path](/dev/integrations). * **New here?** → [`quickstart`](/dev/quickstart). * **Need to call a specific contract?** → [`reference/contracts-full-inventory`](/dev/reference/contracts-full-inventory). * **Building a UI on top of Motoswap data?** → [`reference/api-full-inventory`](/dev/reference/api-full-inventory). * **Doing swap math?** → [`reference/sdk-full-inventory`](/dev/reference/sdk-full-inventory). * **Watching events?** → [`guides/watch-events`](/dev/guides/watch-events). # Quickstart (/dev/quickstart) Deployment status is per contract. On Ethereum (chain `1`): | Status | Contracts | How you tell | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | | Deployed first | MOTO, the launch farming contract (a 14-day farm: Uniswap V2 LP tokens or MOTO in, MOTO out), `UniswapZap`, `MotoStaking`, `RewardVestingEscrow`, `MotocatStaking` | The `/config` address is non-zero | | Deployed with the DEX | The DEX contracts: factory, router, `FeeRouter`, pairs, zap, `RakebackV2`, the creator fee pair, and the moto.fun contracts | The `/config` address is non-zero and `/config.launchPhase` reads `dex` | The table with every address is on [Contract addresses](/dev/addresses). Every section below runs as written, with one wait built in: Rakeback pays on weekly epochs, so section 8 answers empty until the first root is posted. A `/config` key still reads `0x0000000000000000000000000000000000000000` when its contract is unset for the current phase, so every example that sends a transaction checks for that first. You need: * Node 18 or later (for the built-in `fetch`) and `viem`. The examples are plain ES modules: save one as `example.mjs` and run `node example.mjs`. * An RPC endpoint for chain `1`. You supply your own: the examples read it from the `RPC_URL` environment variable and stop with a clear error when it is unset. Never ship a keyed RPC URL in a browser bundle. * A funded wallet, only for the examples that send a transaction. You do **not** need to run any Motoswap software locally. **API host:** `https://api.motoswap.org` is the permanent public API host and the only one to build against. Simple GET requests work from a browser on any origin. Requests that need a CORS preflight, such as one where your script sets `If-None-Match`, only pass for the Motoswap app origin, so do conditional polling and any write from a server. *** ## 1. Discover addresses [#1-discover-addresses] Ask the backend, and take the chain id from the same response instead of assuming one: ```ts const API = "https://api.motoswap.org"; const ZERO = "0x0000000000000000000000000000000000000000"; const cfg = await fetch(`${API}/config`).then((r) => r.json()); console.log(cfg.chainId); // 1 console.log(cfg.launchPhase); // "dex" const { motoToken, vampChef, feeRouter } = cfg.addresses; console.log("MOTO:", motoToken); // 0xbd965230588eaa536de6aa45e8ebbc01638535e0 console.log("farm:", vampChef); // 0x51e648f08a9a08a724591938d9cbc483c809aca4 console.log("FeeRouter deployed:", feeRouter !== ZERO); // true ``` Addresses come back lowercase. A key that reads `0x0` means that contract is unset for the current `launchPhase`. Never send a transaction to an address you have not compared against `ZERO`. The full list of addresses is on [Contract addresses](/dev/addresses). The response is `Cache-Control: public, max-age=300, s-maxage=300, stale-while-revalidate=900` with no `ETag`, so cache it locally and refresh every 5 minutes. *** ## 2. Fetch a price [#2-fetch-a-price] ### Option A: REST [#option-a-rest] ```ts const API = "https://api.motoswap.org"; const cfg = await fetch(`${API}/config`).then((r) => r.json()); const moto = cfg.addresses.motoToken; const price = await fetch(`${API}/token/${moto}/price`).then((r) => r.json()); console.log(price.priceUsd); // priceUsd: "", or null when unknown console.log(price.source); // where the price came from, e.g. "onchain" ``` Every token the indexer tracks, with `priceUsd` and `ethPrice`, comes back from `GET /holdings/tokens` in one call. `GET /tokens/{address}/chart?range=24h` serves candles and the 24h volume window. Accepted ranges: `1m`, `5m`, `10m`, `30m`, `24h`, `7d`, `30d`. Its `price` is nullable and `candles` can be empty for a token with no Motoswap trading history, so fall back to `/token/{address}/price` when you get a null. Send `If-None-Match` with the previous ETag on subsequent calls and you get `304` almost always. See [`guides/caching-and-etags`](/dev/guides/caching-and-etags). ### Option B: chain-direct (fresh reserves) [#option-b-chain-direct-fresh-reserves] Read `getReserves()` on a pair and price it yourself. The MOTO/WETH pair for the current phase is served as `motoUniswapPair`. Motoswap pairs expose the same `getReserves()` shape, so the code below works against either. ```ts import { createPublicClient, defineChain, formatUnits, http, parseAbi } from "viem"; const API = "https://api.motoswap.org"; const RPC_URL = process.env.RPC_URL; if (!RPC_URL) throw new Error("Set RPC_URL to your own Ethereum mainnet RPC endpoint."); const cfg = await fetch(`${API}/config`).then((r) => r.json()); const { motoToken, motoUniswapPair } = cfg.addresses; const chain = defineChain({ id: cfg.chainId, name: "Motoswap target chain", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: [RPC_URL] } }, }); const client = createPublicClient({ chain, transport: http(RPC_URL) }); const pairAbi = parseAbi([ "function token0() view returns (address)", "function getReserves() view returns (uint112, uint112, uint32)", ]); const token0 = await client.readContract({ address: motoUniswapPair, abi: pairAbi, functionName: "token0" }); const [r0, r1] = await client.readContract({ address: motoUniswapPair, abi: pairAbi, functionName: "getReserves" }); // Both tokens have 18 decimals, so no decimal rescale is needed here. const motoIsToken0 = token0.toLowerCase() === motoToken; const [motoReserve, wethReserve] = motoIsToken0 ? [r0, r1] : [r1, r0]; const ethPerMoto = (wethReserve * 10n ** 18n) / motoReserve; // 18-decimal fixed point, bigint console.log(formatUnits(ethPerMoto, 18)); // "", ETH per MOTO ``` ### Option C: candidate pairs for a route [#option-c-candidate-pairs-for-a-route] ```ts const API = "https://api.motoswap.org"; const cfg = await fetch(`${API}/config`).then((r) => r.json()); const { motoToken, weth } = cfg.addresses; const { pairs } = await fetch( `${API}/swap/candidates?tokenIn=${motoToken}&tokenOut=${weth}`, ).then((r) => r.json()); console.log(pairs); // [{ address, token0, token1 }, ...] ``` The response carries pair addresses and their tokens, not reserves. Fetch live reserves yourself (multicall), then price the paths. See [`guides/read-prices`](/dev/guides/read-prices) for the read-path tradeoffs. *** ## 3. Read the chain [#3-read-the-chain] One MOTO read and one farm read, straight from an RPC: ```ts import { createPublicClient, defineChain, http, parseAbi } from "viem"; const API = "https://api.motoswap.org"; const RPC_URL = process.env.RPC_URL; if (!RPC_URL) throw new Error("Set RPC_URL to your own Ethereum mainnet RPC endpoint."); const cfg = await fetch(`${API}/config`).then((r) => r.json()); const { motoToken, vampChef } = cfg.addresses; const chain = defineChain({ id: cfg.chainId, name: "Motoswap target chain", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: [RPC_URL] } }, }); const client = createPublicClient({ chain, transport: http(RPC_URL) }); const erc20 = parseAbi([ "function symbol() view returns (string)", "function decimals() view returns (uint8)", "function totalSupply() view returns (uint256)", ]); const chef = parseAbi([ "function poolLength() view returns (uint256)", "function poolInfo(uint256 pid) view returns (address lpToken, uint256 allocPoint, uint256 lastRewardTime, uint256 accRewardPerShare, uint256 lpSupply)", "function emissionEnd() view returns (uint64)", ]); const [symbol, decimals, supply] = await Promise.all([ client.readContract({ address: motoToken, abi: erc20, functionName: "symbol" }), client.readContract({ address: motoToken, abi: erc20, functionName: "decimals" }), client.readContract({ address: motoToken, abi: erc20, functionName: "totalSupply" }), ]); console.log(symbol, decimals); // MOTO 18 console.log(supply); // totalSupply: const pools = await client.readContract({ address: vampChef, abi: chef, functionName: "poolLength" }); const [lpToken, allocPoint, , , lpSupply] = await client.readContract({ address: vampChef, abi: chef, functionName: "poolInfo", args: [0n], }); const end = await client.readContract({ address: vampChef, abi: chef, functionName: "emissionEnd" }); console.log(pools); // bigint pool count console.log(lpToken, allocPoint); // pool 0 stakes the MOTO/WETH Uniswap V2 LP token console.log(lpSupply); // bigint, LP tokens staked in pool 0 console.log(new Date(Number(end) * 1000).toISOString()); // when farm emissions stop ``` All token amounts are `bigint`. Never convert a wei amount to a JS `Number`. *** ## 4. Stake LP into a farm [#4-stake-lp-into-a-farm] The deployed farm is the launch farming contract, a `VampChef`. Its pools take Uniswap V2 LP tokens. Every row of `GET /farms` names the chef that owns the pool, so deposit to `pool.chef` instead of hard-wiring a `/config` key. While `launchPhase` was `vamp`, `/config.addresses.masterChef` held the `VampChef` too, which has a one-step withdraw and its own event set. ```ts import { createPublicClient, createWalletClient, defineChain, erc20Abi, http, parseAbi } from "viem"; import { privateKeyToAccount } from "viem/accounts"; const API = "https://api.motoswap.org"; const RPC_URL = process.env.RPC_URL; if (!RPC_URL) throw new Error("Set RPC_URL to your own Ethereum mainnet RPC endpoint."); const cfg = await fetch(`${API}/config`).then((r) => r.json()); const chain = defineChain({ id: cfg.chainId, name: "Motoswap target chain", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: [RPC_URL] } }, }); const PRIVATE_KEY = process.env.PRIVATE_KEY; if (!PRIVATE_KEY) throw new Error("Set PRIVATE_KEY to a funded test wallet key."); const account = privateKeyToAccount(PRIVATE_KEY); const client = createPublicClient({ chain, transport: http(RPC_URL) }); const wallet = createWalletClient({ chain, account, transport: http(RPC_URL) }); // 1. Find the pool for the LP token you hold. This one is MOTO/WETH. const myLp = cfg.addresses.motoUniswapPair; const farms = await fetch(`${API}/farms`).then((r) => r.json()); const pool = farms.pools.find((p) => p.lpToken === myLp.toLowerCase()); if (!pool) throw new Error("no farm for that LP token"); // 2. Stake your whole LP balance: approve the chef, then deposit. const amount = await client.readContract({ address: myLp, abi: erc20Abi, functionName: "balanceOf", args: [account.address], }); if (amount === 0n) throw new Error("this wallet holds no LP tokens"); const approveHash = await wallet.writeContract({ address: myLp, abi: erc20Abi, functionName: "approve", args: [pool.chef, amount], }); await client.waitForTransactionReceipt({ hash: approveHash }); const depositHash = await wallet.writeContract({ address: pool.chef, abi: parseAbi(["function deposit(uint256 pid, uint256 amount)"]), functionName: "deposit", args: [BigInt(pool.pid), amount], }); console.log(depositHash); ``` On the `VampChef`, `withdraw(pid, amount)` settles rewards and returns your LP in the same call. `harvest(pid)` pays pending MOTO: half liquid, half into `RewardVestingEscrow` over 180 days. `MasterChef` is deployed but not running: no emission campaign is running and none is planned. LP already staked in it can be withdrawn at any time. `emergencyWithdraw(pid)` returns it in one call. The regular withdraw is two steps: queue it with `withdraw(pid, amount)`, then claim with `claimWithdrawal(pid)`. With no campaign running there is no cooldown between them. `VampChef` has no `claimWithdrawal`. Full recipe: [`guides/stake-lp-into-farm`](/dev/guides/stake-lp-into-farm). *** ## 5. Read a user's state [#5-read-a-users-state] Every per-user view has a REST endpoint that 304s on the wallet's activity watermark, so polling is free once it is cached: ```ts const API = "https://api.motoswap.org"; const addr = "0x0000000000000000000000000000000000000001"; // any wallet, lowercase or checksummed // Activity watermark (note: no /user prefix, the path is /{address}/meta) const meta = await fetch(`${API}/${addr}/meta`).then((r) => r.json()); console.log(meta.lastActivityBlock); // Points and rank, as of the last daily drop const points = await fetch(`${API}/points/${addr}`).then((r) => r.json()); console.log(points.totalPoints, points.rank); // Rakeback per token and epoch. One row per published leaf, with its merkle proof. const rb = await fetch(`${API}/rakeback/${addr}`).then((r) => r.json()); console.log(rb.currentEpoch, rb.epochs.length); ``` Portfolio, holdings, farm positions, staking positions and referrals are one call each. See [`api`](/dev/api). *** ## 6. Make your first swap [#6-make-your-first-swap] Swap **ETH for MOTO** through `FeeRouter`. The example refuses to send if `/config` hands back a zero address or a phase other than `dex`. ```ts import { createPublicClient, createWalletClient, defineChain, http, parseAbi, parseEther } from "viem"; import { privateKeyToAccount } from "viem/accounts"; const API = "https://api.motoswap.org"; const ZERO = "0x0000000000000000000000000000000000000000"; const RPC_URL = process.env.RPC_URL; if (!RPC_URL) throw new Error("Set RPC_URL to your own Ethereum mainnet RPC endpoint."); // 1. Fetch addresses and refuse to continue unless the DEX surface is there. const cfg = await fetch(`${API}/config`).then((r) => r.json()); const { feeRouter, router, weth, motoToken } = cfg.addresses; if (cfg.launchPhase !== "dex" || feeRouter === ZERO || router === ZERO) { throw new Error("The DEX surface is not available: /config reads 0x0 or a phase other than dex."); } const chain = defineChain({ id: cfg.chainId, name: "Motoswap target chain", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: [RPC_URL] } }, }); const PRIVATE_KEY = process.env.PRIVATE_KEY; if (!PRIVATE_KEY) throw new Error("Set PRIVATE_KEY to a funded test wallet key."); const account = privateKeyToAccount(PRIVATE_KEY); const client = createPublicClient({ chain, transport: http(RPC_URL) }); const wallet = createWalletClient({ chain, account, transport: http(RPC_URL) }); // 2. Quote it (chain-direct, single hop). `getAmountsOut` lives on the raw // router. FeeRouter skims `protocolFeeBps` from the path's quote leg. For // WETH -> MOTO that is the INPUT side (WETH outranks MOTO), so quote on the // net input. The side moves per pair; the general rule and the output-side // case are in the aggregator guide (or ask feeRouter.feeOnOutput(path)). const path = [weth, motoToken]; const amountIn = parseEther("0.01"); const feeBps = await client.readContract({ address: feeRouter, abi: parseAbi(["function protocolFeeBps() view returns (uint256)"]), functionName: "protocolFeeBps", }); const netIn = amountIn - (amountIn * feeBps) / 10_000n; const amounts = await client.readContract({ address: router, abi: parseAbi(["function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[])"]), functionName: "getAmountsOut", args: [netIn, path], }); const quotedOut = amounts[amounts.length - 1]; // 3. Apply slippage tolerance: 50 bps = 0.5%. const minOut = (quotedOut * (10_000n - 50n)) / 10_000n; // 4. Submit. const hash = await wallet.writeContract({ address: feeRouter, abi: parseAbi([ "function swapExactETHForTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline) payable returns (uint256[])", ]), functionName: "swapExactETHForTokens", args: [minOut, path, account.address, BigInt(Math.floor(Date.now() / 1000) + 600)], value: amountIn, }); console.log(hash); ``` You get **one `FeeRouter.Swap` event** with `(user, tokenIn, tokenOut, amountIn, amountOut, feeToken, feeAmount)` and **one `MotoSwapPair.Swap`** per hop. `user` is `msg.sender`, the payer, not the `to` recipient. More on `FeeRouter` and the perimeter it fronts: [`contracts#fees`](/dev/contracts#fees). *** ## 7. Add liquidity [#7-add-liquidity] For token-token, call `MotoSwapRouter02.addLiquidity` directly (permissionless). `router` is `cfg.addresses.router`; check it against `ZERO` first, as in section 6. `wallet` and `account` are the clients from section 6, and the amounts are `bigint`. ```ts const deadline = BigInt(Math.floor(Date.now() / 1000) + 600); const addHash = await wallet.writeContract({ address: router, abi: parseAbi([ "function addLiquidity(address tokenA, address tokenB, uint256 amountADesired, uint256 amountBDesired, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline) returns (uint256, uint256, uint256)", ]), functionName: "addLiquidity", args: [tokenA, tokenB, amountA, amountB, minA, minB, account.address, deadline], }); ``` For ETH-token, `addLiquidityETH` (payable). For single-sided, use `MotoSwapZap.zapInToken` / `zapInETH` (`cfg.addresses.zap`, checked against `ZERO` the same way). Full recipes: [`guides/add-remove-liquidity`](/dev/guides/add-remove-liquidity) (router paths) and [`contracts#peripherals`](/dev/contracts#peripherals) (Zap). *** ## 8. Claim Rakeback [#8-claim-rakeback] Rakeback accrues from the protocol fee, and the protocol fee is charged by `FeeRouter`. A wallet that has paid no protocol fee in an epoch gets an empty `epochs` array from `GET /rakeback/{address}`. Epochs close Sunday `00:00 UTC` and open to claim about a day later, so a wallet that has traded gets an empty array too until its first epoch opens. Claims are merkle proofs against a weekly root posted on chain. The wallet's proofs come back with its accrual, so one fetch is enough. `wallet` and `account` are the clients from section 6. ```ts // rb.epochs: one row per published leaf, carrying its own merkle preimage: // { epoch, token, status, amount, index, proof, root, ... } const rakeback = cfg.addresses.rakeback; if (rakeback === ZERO) throw new Error("/config serves no rakeback address for this phase."); const rb = await fetch(`${API}/rakeback/${account.address}`).then((r) => r.json()); const l = rb.epochs.find((x) => x.status === "claimable"); if (l) { const claimHash = await wallet.writeContract({ address: rakeback, abi: parseAbi([ "function claim(uint256 epoch, address token, uint256 index, address account, uint256 amount, bytes32[] proof) returns (uint256)", ]), functionName: "claim", args: [BigInt(l.epoch), l.token, BigInt(l.index), account.address, BigInt(l.amount), l.proof], }); console.log(claimHash); } ``` Batch with `claimMany(epochs[], tokens[], indexes[], accounts[], amounts[], proofs[])`. It is atomic, so send only leaves that are still `claimable`. Recipe: [`guides/claim-rakeback`](/dev/guides/claim-rakeback). *** ## Next steps [#next-steps] * **[Pick your integration path](/dev/integrations)**: aggregator, bot, scanner, or AI agent. * **[Contract addresses](/dev/addresses)**: every chain `1` address, and which keys stay `0x0`. * **[Contracts](/dev/contracts)**: each contract's ABI. * **[Reference](/dev/reference/contracts-full-inventory)**: the full inventories. * **[Swap via FeeRouter](/dev/guides/swap-via-fee-router)** and the other guides: end-to-end recipes for every flow. # SDK - @motoswap/sdk (/dev/sdk) TypeScript library of pure functions used by the Motoswap frontend and backend. No network I/O, no wallet state. Feed it live values (reserves, prices, positions) and it returns the same math the app runs. Full API reference: [`reference/sdk-full-inventory`](/dev/reference/sdk-full-inventory). ## Availability [#availability] `@motoswap/sdk` is not published yet. It is not on any public registry and there is no install path for an outside integrator. The samples on the SDK pages show call shapes; they cannot run until the package is published. Until then, treat this page and the [full inventory](/dev/reference/sdk-full-inventory) as the specification. Every function is a few lines of `bigint` math with its formula written out, so you can port what you need. The constant-product quote, the slippage floor and the price impact formula are all you need for a single-pool swap. The guides inline this math or use viem directly, so they run without the package. ## Modules [#modules] | Module | What it does | Key exports | | ----------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | | `routing` | Multi-hop swap routing (the same router the Motoswap app and API use). | `bestRoute`, `bestSplitRoute`, `bestMidRate`, `pathMidRate`, `splitMidRate`, `getAmountsOut` | | `pricing` | Price a token from stable-paired reserves. | `priceFromReserves`, `toUsd` | | `slippage` | Slippage floors, execution-vs-mid price impact. | `amountOutMin`, `amountInMax`, `submitAmountOutMin`, `midPriceBps`, `executionPriceBps`, `priceImpactBps` | | `zap` | Single-sided LP math. | `optimalSwapInput`, `getAmountOut`, `sqrtBigInt` | | `apr` | APR math for farm/pool + vault. | `computeMasterChefPoolAPR`, `computeVaultAPR` | | `positions` | Position aggregation + lock helpers. | `effectiveStake`, `isUnlocked`, `summarizeUserPositions` | | `vanity` | CREATE2 salt grinding for the retired `TokenLauncher` contract. Legacy; do not build against it. | `grindVanitySalt` | ## Constants live in `@motoswap/shared` [#constants-live-in-motoswapshared] `@motoswap/sdk` exports functions and types only - no constants. The constants live in `@motoswap/shared`: * `SWAP_FEE_BPS = 30n` - per-pool V2 fee (0.30%). Prefer reading `factory.swapFeeBps()` live; the constant is the shipped default. * `BLOCKS_PER_YEAR` - for APR annualization. (`VAULT_IDS` / `VAULT_CONFIG` still exist in `@motoswap/shared`. Their multipliers are static values: `MotoStaking` holds one position per wallet with a lock option 0 to 3, and the source of truth is `MotoStaking.boostBps(durationOption)`, the extra on top of a 1x base, which governance can change. Do not build against the constants.) ## Typical integrator recipe [#typical-integrator-recipe] Call shapes, with real reserves so you can check a port of the math against the printed values (`75871730613401515247916n 6n 75492371960334507671676n`). ```ts import { bestRoute, amountOutMin, priceImpactBps, type RoutePair } from "@motoswap/sdk"; const WETH = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; const MOTO = "0xbd965230588eaa536de6aa45e8ebbc01638535e0"; // Reserves you read yourself with getReserves(), as bigint. const pairs: RoutePair[] = [ { token0: MOTO, token1: WETH, reserve0: 133_292_018_598_175_373_111_408_005n, reserve1: 175_053_998_838_797_275_747n }, ]; const amountIn = 10n ** 17n; // 0.1 WETH const feeBps = 30n; // the pool's LP fee: factory.swapFeeBps(), or 30 on Uniswap V2 // 1. Quote a multi-hop trade. const route = bestRoute(WETH, MOTO, pairs, amountIn, 2, feeBps); if (!route) throw new Error("no route"); // 2. Show price impact for the first hop. feeBps is required. const impact = priceImpactBps({ amountIn, reserveIn: pairs[0].reserve1, reserveOut: pairs[0].reserve0, feeBps, }); // 3. Apply user slippage tolerance for calldata. const minOut = amountOutMin(route.amountOut, 50n); // 0.5% console.log(route.amountOut, impact, minOut); ``` ## Design principles [#design-principles] ### Pure & deterministic [#pure--deterministic] No fetches, no wallet, no `Math.random`. Same inputs → same outputs. Safe to run in a worker / edge / node. ### `bigint` everywhere [#bigint-everywhere] Token amounts and USD (µ-USD) are bigints. Doubles are only for `Percent` / `Apr` / `Ratio` (display). ### One math implementation [#one-math-implementation] `bestRoute` is the exact same function the Motoswap frontend uses. No client/server drift. ### Branded types [#branded-types] `Address`, `Wei`, `Usd`, `Percent`, `Apr`, `Ratio`, `VaultId` come from `@motoswap/shared/domain`. Use them; the compiler catches shape errors before runtime. ## When to use REST vs SDK [#when-to-use-rest-vs-sdk] * **REST** for historical windows, TVL, per-user metrics, leaderboard, the full pool universe. The backend has already computed these. * **SDK** for live-quote math on reserves you fetched yourself (chain-direct), and for anything you want to render deterministically at pre-submit time (slippage floor, price impact, split fill, zap balance). # The board and your coins (/fun/board) The board is the front page: every coin on the network, newest first, with the pending ones that nobody has bought yet sitting alongside the ones already trading. ## What a card shows [#what-a-card-shows] Ticker, name, picture, creator, and how far along the curve the coin is. A coin that has graduated is marked as graduated and prices from its pools rather than the curve. Trades and volume on a card are the last 24 hours. Coins with no buys yet appear as pending. They have an address already, they show who signed them, and buying one is what deploys it. A pending coin whose signature has expired shows as expired rather than quietly disappearing. ## The live feed [#the-live-feed] A ticker strip runs the most recent trades and the most recent launches across the whole network. It is every coin, not a filtered set, and it updates as blocks land. ## Coin pages [#coin-pages] A coin's page carries the chart, the trade panel, the recent trades, and the largest holders with a true holder count. After graduation the chart is DexScreener's chart of the coin's Motoswap pool. Before graduation, and for a graduated coin DexScreener has not indexed yet, the chart comes from Defined (a charting data provider). Our own chart is the fallback when neither of those has data for the coin. The holders list nets out coins that went to the PvE escrow, so the escrow does not read as a whale. ## Creators [#creators] The creators board ranks by fees earned, folded per creator rather than per coin, with each creator's best-earning coin shown next to their total. Creating a hundred coins nobody trades does not move you up. ## Your pages [#your-pages] **Your coins** lists what you launched, what it has earned, and what is claimable, with the claim button next to it. **Your positions** lists the coins you hold from curve trading, with what you put in and what the position is worth at the current price. Dust is filtered out. **Rewards** covers what your staked [Motocats](/user/motocats) have coming from [PvE](/fun/pve) drops and what you have claimed. You claim on Motoswap's [staking page](https://motoswap.org/stake/motocats). ## Sharing [#sharing] Every coin has a share link that unfurls with the coin's picture, ticker, and progress on X and anywhere else that reads link previews. The card image is generated from the coin's own committed metadata, so a share card cannot show art the coin does not actually carry. ## Moderation [#moderation] Coins can be hidden from the board and from the site's listings. Hiding a coin changes nothing on chain: the contract is permissionless, the coin still trades, and anyone holding it can still sell. The site's listings are curated; the chain is not. # Creator fees (/fun/creator-fees) Every coin on moto.fun pays its creator a cut of every trade, from the first buy, in the quote asset of the trade. You never hold your own coin to earn it and you never sell anything to collect it. | | | | ---------------- | ------------------------------------------------------------------------------------------------------------------------ | | While bonding | 0.5% of every buy and every sell | | After graduation | 0.3% of every swap on your coin's pools | | Paid in | The other side of the trade: WETH on the curve and the TOKEN/WETH pool, MOTO on the TOKEN/MOTO pool. Never your own coin | | Turned on | Always, from the first trade. There is no switch to forget | | Recipient | Set at launch, defaults to the wallet that signed. The current payout wallet can move it later | | Claiming | Any time, any number of coins, from the vault | | Expiry | None. It sits in the vault until you claim it | ## How it accrues [#how-it-accrues] The fee comes out of the non-coin side of every trade on your coin (ETH on the curve, WETH or MOTO in the pools) and lands in the creator fee vault, credited to your coin. The vault holds it per coin and per asset. Nothing is pushed to your wallet and nothing is streamed: the balance sits there until you claim. Trading does not stop paying you when your coin graduates. The rate changes from 0.5% to the standard 0.3% creator fee, and it keeps accruing into the same vault from your coin's new pools for as long as anyone trades your coin. ## Claiming [#claiming] Claim from your coin's page or from **Your coins**. A claim is a normal transaction, you pick which assets to take, and you can take WETH as ETH in the same transaction. Claiming several coins at once is one transaction. Only the coin's current payout address can claim its fees: the one set at launch, or the one it was moved to since. There is no signature service, no backend approval, and no one who can withhold it. ## Choosing the recipient [#choosing-the-recipient] At launch the payout address is your connected wallet, filled in for you. Change it in the form before you sign if the fees should go somewhere else, as an address or an ENS name. It cannot be one of the protocol's own contracts, and the launch refuses one that is. ## Changing it later [#changing-it-later] You can move the payout address any time after launch, from your coin's page or from **Your coins**, with one transaction from the current payout wallet. It takes effect at once, and any fees you have not claimed yet move with it to the new address. Only the current payout wallet can make the change. If you lose that key, contact support. Recovering a lost payout wallet is the one change that goes through us. Whoever holds the payout address holds the coin's fee income, including anything that accrued before. Treat it like a treasury address, not like a throwaway. ## When the fee is rerouted [#when-the-fee-is-rerouted] If the registry that names your coin's creator cannot be reached at trade time, or the curve's permission to deposit into the vault has been withdrawn, the whole 1.2% goes to the protocol Collector for that trade instead of splitting. The trader always pays the same 1.2% either way, and the event is recorded on chain, so a rerouted fee is visible rather than silent. This is a failure path, not a policy. It is documented because a creator looking at their vault should be able to tell the difference between "no trades" and "trades that did not pay me". ## Not the same as your dev buy [#not-the-same-as-your-dev-buy] The creator fee is income from other people's trading. A [dev buy](/fun/launch) is you buying your own coin at the curve price, and it sits in your wallet like anyone else's position. They are unrelated, and you can take one, both, or neither. # FAQ (/fun/faq) Nothing, if you do not buy your own coin. Creating is a signature, not a transaction, so there is no gas and no fee. Whoever buys first pays the gas that deploys the coin. If you want a dev buy, you send the transaction and pay for the buy and the gas. No. Tickers are not exclusive and never were. Two coins can carry the same one, and the address is what identifies yours. Only `MOTO`, `WETH`, `ETH`, `USDC`, and `USDT` are refused outright. There is no liquidity to rug. While a coin bonds, the ETH sits in the curve contract and there is no function that lets anyone withdraw it, including us. At graduation the LP tokens for both pools are burned, so nobody holds a position to remove. A creator can still sell coins they bought, like any holder. That is the risk you price. The fee is 1.2% per trade, taken on the way in and on the way out, and the curve price moves down when you sell. Buying and immediately selling with no other trades in between returns less than you put in, by design. This is how a bonding curve works, not a bug. Your buy crossed the graduation line. It filled exactly up to the line and refunded the remainder in the same transaction. Check the transaction: the refund is in it. A keeper graduated the coin first. Only one graduation can land, and yours arrived second. The coin is graduated either way and nothing is wrong. There is nothing to report. It is in the frozen window, waiting for a graduation transaction. That normally lasts under a minute. If the MOTO price on the pool has been pushed away from its recent average, graduation reverts and retries until it is safe, which can add a few blocks. If it stays frozen, selling reopens on its own 15 minutes after the freeze, and selling walks the coin back to trading. Your coins are never stuck. It keeps trading on its curve, in both directions, indefinitely. There is no deadline and no expiry. Its [PvE](/fun/pve) escrow, if it has one, stays escrowed until it graduates. Yes, at the same rate as a DEX swap of the same size, on the same balances. See [Points and Rakeback on the curve](/fun/points-and-rakeback). Whenever you claim them. They accrue into a vault from the first trade and sit there until you take them. Nothing expires and nothing is streamed. See [creator fees](/fun/creator-fees). The name and picture, no. They are committed on chain as a hash, and the site refuses to serve metadata that does not match it, so nobody can change them, including us. The fee recipient, yes: from your coin's page or from **Your coins**, with one transaction from the current payout wallet. It takes effect at once, and any fees you have not claimed yet move with it. No. moto.fun is where launches happen. No. There is no presale, no allocation list, and no team allocation. The whole supply starts on the curve and the only way to hold a coin is to buy it. A dev buy is a buy at the curve price, in public, like everyone else's. The chain is permissionless and your coin trades regardless. The site's listings are curated, and a coin can be hidden from them. Hiding changes nothing on chain: the coin still trades and holders can still sell. # Graduation (/fun/graduation) When 80% of a coin's supply has been bought, the curve stops and the coin graduates into real Motoswap pools. It is one transaction, sent by a keeper or anyone else. The creator does not have to do anything. | | | | --------------------- | --------------------------------------------------------- | | Trigger | 200,000,000 of the 1,000,000,000 supply left on the curve | | Roughly | 4 ETH of net buying in the curve, about 20 ETH market cap | | Who graduates it | Anyone. A keeper does it within about a minute | | Reward for graduating | A bounty out of the curve's ETH | | Pools created | TOKEN/WETH and TOKEN/MOTO | | Pool price | The final curve price, both pools | | LP tokens | Burned. Not locked, not held, not vested | | Leftover coins | Burned | ## The sequence [#the-sequence] The buy that crosses the line fills only up to the line and refunds the rest. The coin freezes: buying and selling both stop, and the page shows that the curve is complete. Graduation is then open to anyone. In practice a keeper picks it up within about a minute, and the button on the page is there in case it does not. If you press it and the keeper lands first, your transaction reverts and the coin is graduated either way. That revert is not a problem to report. Inside the graduation transaction, in order: 1. A bounty comes out of the curve's ETH for whoever sent the transaction. 2. What is left splits in half. 3. One half market-buys MOTO through Motoswap. 4. The coin's transfer restrictions lift. 5. Part of the coins left on the curve pair with the WETH half to open a TOKEN/WETH pool. The LP is burned. 6. The MOTO just bought pairs with coins into a TOKEN/MOTO pool, priced to match the pool opened one step earlier. The LP is burned. 7. Anything left over on the coin side is burned. Either both pools open or the transaction reverts and the keeper tries again in a later block. There is no state where a coin graduates with only one pool. ## Why the MOTO leg exists [#why-the-moto-leg-exists] Every coin that graduates gets a MOTO pool, and the ETH that buys the MOTO for it is a real MOTO buy at every graduation. Half of every graduated curve's raise goes through that buy. The buy is checked against a short recent average of MOTO's price before it goes through. If MOTO's price on the pool has been pushed away from that average, the graduation reverts and retries rather than filling at a manipulated rate. This can add a few blocks to a graduation and costs nothing but time. ## After graduation [#after-graduation] The coin is an ordinary ERC-20 in ordinary Motoswap pools. Its supply is fixed, it has no owner and no tax, and the liquidity behind it cannot be pulled because the LP that owns it was burned. The curve keeps no ETH and no coins. Trading moves to the pools, the fee becomes the normal Motoswap 1% plus the coin's 0.3% [creator fee](/fun/creator-fees), and any [PvE](/fun/pve) escrow that built up on the curve credits to Motocat stakers in the same transaction. If that credit fails, the coin still graduates and a keeper retries the credit. ## Coins that never graduate [#coins-that-never-graduate] Nothing forces a coin to graduate and nothing expires. A coin that stops trading at 10% sold stays on its curve, tradeable in both directions, indefinitely. Its PvE escrow, if any, stays escrowed until it graduates. # What moto.fun is (/fun) moto.fun is where coins start on Motoswap. You create one with a signature, it trades on a bonding curve from the first buy, and when the curve fills it graduates into real Motoswap pools with the LP burned. There is no presale, no allocation list, and no liquidity for you to raise. | | | | ------------------------ | --------------------------------------------------------------------- | | Cost to create | Free. A signature, no transaction, unless you want a dev buy | | Supply | 1,000,000,000 per coin, fixed, 18 decimals, no mint, no owner, no tax | | Fee while bonding | 1.2% per trade: 0.7% protocol, 0.5% to the creator | | Fee after graduation | The normal Motoswap 1%, plus the 0.3% creator fee on your coin | | Graduation trigger | 80% of supply sold, which is about 4 ETH of net buying | | Market cap at graduation | About 20 ETH | | What graduation creates | A TOKEN/WETH pool and a TOKEN/MOTO pool, both with the LP burned | | Ticker rules | 1 to 16 letters or numbers. No reservation window, not exclusive | ## The curve [#the-curve] Every coin opens with its whole supply on one curve. Buys move the price up, sells move it back down, and the price is a function of how much of the supply has been bought. Selling for a little less than you paid a moment ago is the curve working, not a bug. Nobody seeds the curve. There is no LP to add, no pool to fund, and no way for a creator to pull liquidity out from under buyers, because there is no liquidity position to pull. The curve holds the ETH until the coin graduates. ## Graduation [#graduation] At 80% of supply sold the curve freezes and the coin graduates. The ETH that accumulated splits in half: one half seeds a TOKEN/WETH pool, the other half market-buys MOTO and seeds a TOKEN/MOTO pool. Both LP positions are burned. From that point the coin is an ordinary Motoswap token trading in ordinary Motoswap pools. [Graduation](/fun/graduation) covers the whole sequence. Coins that never fill their curve keep trading on the curve. There is no deadline and no expiry. ## What you get for launching [#what-you-get-for-launching] The creator fee is on from the first trade, at 0.5% of every buy and sell while the coin bonds, and 0.3% of every swap after it graduates. It accrues in the quote asset, never in your own coin, into a vault you claim from whenever you want. You do not have to hold or sell your coin to earn it. [Creator fees](/fun/creator-fees) covers the claim path. You can also point the launch at Motocat stakers instead of yourself with [PvE mode](/fun/pve), or take a dev buy, or do both in the same launch. ## How it connects to the rest of Motoswap [#how-it-connects-to-the-rest-of-motoswap] Curve trades are Motoswap trades. They earn [Points](/fun/points-and-rakeback) and accrue [Rakeback](/fun/points-and-rakeback) at the same rate a swap on the DEX does, they route their protocol fee to the same fee Collector contract that splits every Motoswap swap fee, and their creator fees land in the same [CreatorFeeVault](/fun/creator-fees). A graduated coin trades through the same [FeeRouter](/user/fee) as every other pool. # Launching a coin (/fun/launch) Creating a coin on moto.fun costs nothing. You sign a message, the coin gets an address, and the first buy deploys it. If you want to buy your own coin at launch, you send the transaction yourself and the deploy and your buy settle together. | | | | ------------------------------------ | ---------------------------------------------- | | Name | 1 to 48 characters | | Ticker | 1 to 16 letters or numbers, nothing else | | Reserved tickers | `MOTO`, `WETH`, `ETH`, `USDC`, `USDT` | | Ticker exclusivity | None. Two coins can share a ticker | | Supply | 1,000,000,000, fixed, minted to the curve | | Creator fee | On from the first trade, 0.5% while bonding | | Dev buy | Optional, any amount, fills at the curve price | | Launches per connection (IP address) | 5 per 10 minutes | | Unbought coins per wallet | 3 at a time | | Signature validity | Up to 7 days | ## The two paths [#the-two-paths] **No dev buy.** You sign and you are done. No transaction, no gas, no wallet balance needed. Your coin appears on the board as pending and its address is already fixed. The first person to buy it pays the gas that deploys it. If nobody ever buys, the signature expires and the coin never exists. **With a dev buy.** You send one transaction. It deploys the coin and fills your buy at the curve price in the same transaction, so nobody trades ahead of you. Your buy is a normal curve buy and pays the normal 1.2%. ## The ticker is not yours [#the-ticker-is-not-yours] Tickers are not reserved and not exclusive. Two coins can carry the same one, and the address is what identifies yours. Nothing is held for you while you fill in the form, and there is no reservation window to race. This is deliberate. The five reserved tickers above are refused outright because they name real assets. Any other ticker is open to anyone, any number of times. ## What you fill in [#what-you-fill-in] Name, ticker, and a picture are the whole required set. Description, website, and X link are optional. The picture is resized in your browser before it is sent, so a large file is not a problem. Your metadata is committed on chain as a hash at launch, and the site serves it back only after re-checking it against that hash. Nobody can change your coin's name, ticker, or picture after the fact, including us. ## Creator fee recipient [#creator-fee-recipient] The creator fee pays your connected wallet unless you point it somewhere else. The launch form fills the field with the wallet you connected; change it there before you sign, as an address or an ENS name, if the fees should go elsewhere. It cannot be one of the protocol's own contracts, and the launch refuses if you try. You can move it again later from your coin's page or from **Your coins**; see [Creator fees](/fun/creator-fees). The wallet that signed is still the coin's creator for everything else: the board, your profile, and the [PvE](/fun/pve) credit if you turned PvE on. ## PvE and the dev buy together [#pve-and-the-dev-buy-together] A launch can carry a dev buy, a [PvE](/fun/pve) slice, or a split of one buy across both. You choose the split as a percentage. One buy fills at one price and the two halves are separated after, pro rata, so the PvE slice never costs you a worse fill than the dev slice. ## Limits [#limits] Five launches per ten minutes from one connection, and three unbought coins per wallet at a time. Both exist to stop a script filling the board with coins nobody bought. Buying any of your pending coins frees the slot. # Points and Rakeback on the curve (/fun/points-and-rakeback) A trade on a bonding curve is a Motoswap trade. It earns [Points](/user/points) and accrues [Rakeback](/user/rakeback) exactly as a swap on the DEX does, on the same balances, with no separate pot and nothing to opt into. | | | | ------------------------ | ------------------------------------------------------------------ | | Points on curve trades | Yes, 1:1 with a DEX swap of the same size | | Rakeback on curve trades | Yes, `0.20%` of the trade, same as a swap | | Balance | The same one. Curve and DEX are not tracked separately | | Motocat boost | Applies to curve trades like any other | | Referrals | Your referrer earns 10% of your trading Points on curve trades too | | Rakeback schedule | The same weekly epochs rolling Sunday and the same 30-day expiry | ## Why the numbers match [#why-the-numbers-match] The 0.7% protocol fee on a curve trade goes to the same Collector that every DEX swap fee goes to, in the same asset, and is split by the same weights. Rakeback is a fixed share of that fee, so a $100 curve trade credits exactly what a $100 DEX swap credits. This is arithmetic, not a policy that could be tuned separately later. Points read the same fee. A curve trade that paid a protocol fee scores; one that somehow paid none scores nothing, which is the same rule the DEX runs. ## What is different [#what-is-different] **PvE buys.** A public [PvE](/fun/pve) buy is a real trade and pays a real fee, so it earns and accrues like any other buy. You keep no coins from it, which is the point, but the Points and Rakeback are yours. **Rakeback will be per network.** moto.fun runs on Ethereum today. Once it runs on more networks, fees you pay on one network come back from that network's pot, and curve trades follow the same rule as swaps: they credit the network they happened on. Points stay one season across all networks either way. **Points update once a day.** Balances move at 00:00 UTC, curve trades included. A curve trade you made this afternoon appears in tomorrow's number. ## Claiming [#claiming] There is no separate claim for anything you earned on a curve. Rakeback claims from the Rakeback panel, in the same epochs, with the same 30-day window. Points are a balance, not something you claim. # PvE on the curve (/fun/pve) PvE mode sends coins to Motocat stakers instead of to the creator. On the curve it is a buy like any other, at the same price, paying the same fee. The difference is where the coins go: into an escrow for staked cats rather than into a wallet. | | | | --------------------------- | --------------------------------------------------------------------------- | | Set at launch | Choose all PvE, all dev buy, or a split by percentage | | Price | The curve price, same as any buy, same 1.2% fee | | Public PvE buys | Anyone, any bonding coin, any time before graduation | | When stakers get it | In one drop, when the coin graduates | | Split | Pro rata by staked cat count, equal per cat, at the block before the credit | | Claiming | Pull-based, never expires, batches across coins | | A coin that never graduates | Holds its escrow. Nothing is lost, nothing is released | ## As a creator [#as-a-creator] At launch you decide what your buy is for. All of it can go to the cat pool, all of it can be a dev buy, or you can split it: one buy fills at one price and the two halves separate pro rata after. The alternative is the ordinary one, and it works against you.

You buy a slice of your own supply

Everyone knows one thing about it: it gets sold

Your exit is priced into the chart from the first block

The same money buys supply reserved for Motocat stakers

It goes to people who staked Motocats

Nobody expects it to be sold on day one

## As anyone else [#as-anyone-else] Any bonding coin takes a public PvE buy, whether or not its creator turned PvE on. You pay the curve price and the fee like a normal buy and you keep nothing: every coin you bought goes to the escrow. It is the community version of the same move. If a coin's holders want the cat pool behind it, they can put it there themselves, repeatedly, at any point before graduation. ## When stakers get paid [#when-stakers-get-paid] The escrow builds up on the curve while the coin bonds and credits to Motocat stakers in one drop, in the graduation transaction. Not before. A coin that trades for months without graduating holds its escrow for months, and that is the design: the drop is tied to the coin becoming real, not to a clock. At the moment of the credit, the split is snapshotted from the block before: equal per staked cat, pro rata by how many you have staked. Flat and final. Staking after that credit earns you nothing from that coin and everything from future ones. Unstaking after it keeps the share you already have. If nobody has a cat staked when a credit lands, it is held and folded in once stakers exist. Nothing burns. ## Claiming [#claiming] Holders of staked Motocats claim on Motoswap's [staking page](https://motoswap.org/stake/motocats), from launch day. Claims are pull-based and never expire. You claim the coin itself, not a swap or a conversion, you can batch any number of coins into one transaction, and you do not need to be staked at the time you claim. [PvE mode](/pve) covers the staker side in full, and [earning from PvE](/pve/earn) covers claiming. # Trading on the curve (/fun/trade) You buy and sell a bonding coin from its page. There is no pool, no LP, and no route: you trade against the curve directly, in ETH, and the price is set by how much of the supply has been bought. | | | | ----------------------------------- | ---------------------------------------------- | | Fee per trade | 1.2% total: 0.7% protocol, 0.5% to the creator | | Fee side | Off the ETH leg, on buys and on sells | | Slippage protection | A minimum you receive, set from the quote | | Deadline | None. Curve trades have no deadline | | Buy that would overshoot graduation | Partially filled, the rest refunded | | Trading while frozen | Paused, and sells reopen after 15 minutes | | Points and Rakeback | Same as a DEX swap | ## Buying [#buying] You send ETH, you get coins. The fee comes off your ETH before it reaches the curve, so a 1 ETH buy puts 0.988 ETH of buying pressure behind it. Your slippage setting becomes a minimum number of coins you accept, and the transaction reverts rather than filling below it. Curve trades have no deadline. The minimum you set protects your price however long the transaction waits. If your buy would take the curve past the graduation point, it fills exactly up to that point and the rest of your ETH comes back in the same transaction. You never overpay for the last slice. ## Selling [#selling] You send coins, you get ETH, and the fee comes off the ETH on the way out. The price you get is the curve price at that moment, which is lower than it was when you bought if nobody bought after you. That is the mechanism, not slippage. Nothing stops you selling. There is no lockup, no cooldown, no sell tax, and no per-wallet cap. ## The frozen window [#the-frozen-window] When a buy takes the curve to the graduation point the coin freezes. Buying and selling both stop while it waits for someone to graduate it, which normally happens within about a minute. See [graduation](/fun/graduation). If graduation somehow does not happen, selling reopens on its own 15 minutes after the freeze, and a sell at that point walks the coin back to trading. Your coins are never locked in the curve. ## What the fee pays for [#what-the-fee-pays-for] The 0.7% protocol share goes to the same Collector every Motoswap swap fee goes to, and is split the same way, which is why curve trades accrue [Rakeback](/fun/points-and-rakeback) exactly like DEX swaps. The 0.5% creator share goes to the coin's [creator fee](/fun/creator-fees) vault. Both rates are read from the contract at trade time rather than baked in, so what the app shows you is what the contract charges. ## Buying for the cat pool [#buying-for-the-cat-pool] A public [PvE](/fun/pve) buy is an ordinary buy at the ordinary price where you keep nothing: the coins go straight to the Motocat staker pool. It is available on any bonding coin, whether or not the creator turned PvE on at launch. # Contracts (/user/contracts) These are the contracts behind Motoswap and moto.fun on Ethereum mainnet. Each address links to Etherscan, where the source is verified. The source of truth is [`GET https://api.motoswap.org/config`](https://api.motoswap.org/config). The tables on this page are a copy of it, and a check compares every address here with `/config` and with the code on chain. If the two ever disagree, `/config` wins. Developers who want the `/config` key for each contract will find it on [contract addresses for developers](/dev/addresses). Before you approve a token or sign a transaction, check that the address in your wallet matches this page. Anything claiming to be Motoswap at another address is not Motoswap. ## Motoswap [#motoswap] | Contract | Address | What it does | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | MOTO token | [`0xBd965230588EAA536dE6aA45E8ebbc01638535e0`](https://etherscan.io/address/0xBd965230588EAA536dE6aA45E8ebbc01638535e0) | Motoswap's token, an ERC-20. | | Fee router | [`0x9f846ef584FD44d075B5E8dF00dDB4416da61a80`](https://etherscan.io/address/0x9f846ef584FD44d075B5E8dF00dDB4416da61a80) | Every swap goes through here. It takes the fee and sends the trade to the pools. | | Router | [`0x6F0b6a0D84C3cFf6f273D5294CDA147885225D5E`](https://etherscan.io/address/0x6F0b6a0D84C3cFf6f273D5294CDA147885225D5E) | Adds and removes liquidity. | | Factory | [`0x81C9CBC47d700dA1777aBd831D8dA3f526DfAe24`](https://etherscan.io/address/0x81C9CBC47d700dA1777aBd831D8dA3f526DfAe24) | Creates every Motoswap pool and keeps the list of them. | | MOTO/WETH pool | [`0x302C53B6176F750e5547D775645dc8778524fCc1`](https://etherscan.io/address/0x302C53B6176F750e5547D775645dc8778524fCc1) | The main MOTO pool. | | MOTO/USDC pool | [`0xB2924B0A238976D231c361A64A885E61B1006D15`](https://etherscan.io/address/0xB2924B0A238976D231c361A64A885E61B1006D15) | MOTO paired with USDC. | | MOTO/USDT pool | [`0x79BCD8d7003ca27bC4D53D07d3dF3847336eE670`](https://etherscan.io/address/0x79BCD8d7003ca27bC4D53D07d3dF3847336eE670) | MOTO paired with USDT. | | Zap | [`0x26BffD661C3d97B3285AfA2a9F05232526A89129`](https://etherscan.io/address/0x26BffD661C3d97B3285AfA2a9F05232526A89129) | Adds liquidity to a pool from a single token in one transaction. | | Fee collector | [`0xC13307272bBf73f2191cE57d0Fb714C2A9200cF3`](https://etherscan.io/address/0xC13307272bBf73f2191cE57d0Fb714C2A9200cF3) | Receives the `0.70%` protocol fee and splits it. | | Quote asset registry | [`0xFb8CA710B9B242e9f5027272592ef4C19D37E200`](https://etherscan.io/address/0xFb8CA710B9B242e9f5027272592ef4C19D37E200) | The list of assets fees are taken in: ETH, USDC, USDT and MOTO. | | Rakeback | [`0x89E2E1819Fc373e3cD840A0ceb2Ef1eAE79E2dB8`](https://etherscan.io/address/0x89E2E1819Fc373e3cD840A0ceb2Ef1eAE79E2dB8) | Pays out your [Rakeback](./rakeback.mdx) when you claim. | | Buyback and burn | [`0x85B5A2800d7E2D948F21Cb8F6E610D2c9B19AE3d`](https://etherscan.io/address/0x85B5A2800d7E2D948F21Cb8F6E610D2c9B19AE3d) | Buys MOTO with its share of the fee and burns it. | | MOTO staking | [`0xCE88F2C6B49EfBb92555eE5475311f0e250C2528`](https://etherscan.io/address/0xCE88F2C6B49EfBb92555eE5475311f0e250C2528) | Stake MOTO for a share of every swap fee. | | Creator fee registry | [`0x6CdEC7F49036Ccf1bba940011040fA044cFe6D08`](https://etherscan.io/address/0x6CdEC7F49036Ccf1bba940011040fA044cFe6D08) | Records which coins carry a creator fee and who receives it. | | Creator fee vault | [`0x8cC7C8E76D9698E5B1026c720a94C1807e613e1f`](https://etherscan.io/address/0x8cC7C8E76D9698E5B1026c720a94C1807e613e1f) | Holds creator fees until the creator claims them. | | MasterChef | [`0x939f348b6658cE4DB7CDe088341b00DE341Fe085`](https://etherscan.io/address/0x939f348b6658cE4DB7CDe088341b00DE341Fe085) | Deployed, not running; no farm program is planned. | | Launch farming (ended) | [`0x51e648f08a9a08A724591938d9cbc483C809aCA4`](https://etherscan.io/address/0x51e648f08a9a08A724591938d9cbc483C809aCA4) | The launch farming contract. Launch farming has ended. | | Launch farming zap (ended) | [`0x532bFB5b8c9e3CeF20d36910B90cfbD919b18C31`](https://etherscan.io/address/0x532bFB5b8c9e3CeF20d36910B90cfbD919b18C31) | Turned one token into a Uniswap V2 LP position and staked it in launch farming. | | Reward stream | [`0xB94bFadc535D96462854175D776Afc168196976b`](https://etherscan.io/address/0xB94bFadc535D96462854175D776Afc168196976b) | Streams the second half of launch farming rewards over `180 days`. You claim it on your profile. | If you still have LP in a launch farm, withdraw it on the Farm page. Your LP is safe and stays yours. Every other pool is created by the factory, so its address comes from the factory rather than from a list. ## Motocats [#motocats] | Contract | Address | What it does | | --------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | Motocats | [`0x0B024b3D4ad38BFf1Acd303cd308D2C49c936E0A`](https://etherscan.io/address/0x0B024b3D4ad38BFf1Acd303cd308D2C49c936E0A) | The Motocats NFT collection. | | Motocat staking | [`0x11890Ee9eE6099Ac33fd4dfb702f8E8f6d651f17`](https://etherscan.io/address/0x11890Ee9eE6099Ac33fd4dfb702f8E8f6d651f17) | Stake your cats for PvE drops and the Points boost. | | PvE vault | [`0x84303a885717C4dd9EffBdD2179BBE4b01756740`](https://etherscan.io/address/0x84303a885717C4dd9EffBdD2179BBE4b01756740) | Holds [PvE drops](/pve) for staked Motocats until they are claimed. | ## moto.fun [#motofun] | Contract | Address | What it does | | -------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | Launchpad | [`0xb65b67e986d7B097652920d72202F2c78319BC2C`](https://etherscan.io/address/0xb65b67e986d7B097652920d72202F2c78319BC2C) | Launches coins and runs every curve trade and graduation. | | Lens | [`0xb631697124c8e6Bb6C221Cc017e47D4168bd3411`](https://etherscan.io/address/0xb631697124c8e6Bb6C221Cc017e47D4168bd3411) | Read-only helper for quotes and coin data. | | Token template | [`0x0aA904177e972B5B671EB31A414A1c5e3C8d3Fd3`](https://etherscan.io/address/0x0aA904177e972B5B671EB31A414A1c5e3C8d3Fd3) | The code every moto.fun coin is a copy of. You never call it directly. | | Gas tank | [`0x4158630864850Ea7F0905803d607E33bAF1dA1A5`](https://etherscan.io/address/0x4158630864850Ea7F0905803d607E33bAF1dA1A5) | Keeps the graduation keeper topped up with gas. Nobody trades through it. | Each moto.fun coin has its own address, which you can find on its page on [moto.fun](https://moto.fun). # Earning (/user/earn) You get three seats at the table. Liquidity earns the biggest cut of every swap, MOTO staking pays you a share of every fee, and staked Motocats multiply your Points. Launch farming ran for 14 days at token launch and paid MOTO on top of that; it has ended, and there is no new farm. ## Provide liquidity [#provide-liquidity] Deposit both tokens of a pair at equal value. Liquidity providers on Motoswap earn the 0.30% trading fee on every swap in their pool, the largest slice of the `1%` fee, split pro-rata by your share. It accrues into the pool, so your position grows with every trade and pays out when you remove. No lock. Your position also earns [Points](./points.mdx) on the fees it generates. Want in with one token? The Zap on any pool page converts half and hands you the LP in one transaction. The first provider in a new pair sets the starting price from the ratio deposited. The app warns you when you are first; match the ratio to what the tokens are worth. Provide liquidity If the two tokens' prices move apart, withdrawing can be worth less than having held them in your wallet. That gap is real money, not a display artifact. ## Launch farming (ended) [#launch-farming-ended] Launch farming has ended. It ran for 14 days after MOTO launched, paying flat rewards to Uniswap LP and MOTO staked in the launch farm. Harvests paid `50%` liquid on the spot and streamed the other `50%` over `180 days`; the stream still sits on your profile, claimable as it unlocks. If you still have LP in a launch farm, withdraw it on the [Farm page](./launch-farming.mdx). Your LP is safe and stays yours. There is no new farm: liquidity you provide on Motoswap earns trading fees only, covered above under **Provide liquidity**. ## Stake MOTO [#stake-moto] MOTO staking MOTO is the ecosystem token: a fixed `10B` supply, bought back and burned by the swap fee. Staking it puts you on the other side of every trade: `0.20%` of every swap goes to stakers, paid in the assets the fees were taken in, not emissions. `$10M` of weekly volume means stakers split `$20,000` of cash. Slow week, smaller pot. There is no schedule underneath, only the fee flow. You pick a lock when you stake. The longer you lock, the more weight your stake carries, so the larger your share of the same pot. Claim rewards any time, even locked. Unstaking streams your MOTO back over `30 days`, claimable as it arrives. ## Stake Motocats [#stake-motocats] Motocats staking Staked Motocats multiply the [Points](./points.mdx) you earn on every swap. More cats, more boost. A cat sitting in your wallet does nothing; the boost turns on when you stake. One collection approval, then stake and unstake any number of cats in one transaction, whenever you want. No lockup, no fee, no cap. Staked cats also collect a share of every [PvE launch](/pve); that side of the story lives on the [Motocats page](./motocats.mdx). # The 1% (/user/fee) Motoswap charges `1%` per swap: `0.30%` stays in the pool for the LPs, and `0.70%` is collected in the quote asset of the trade. There are no deposit, withdrawal, or account fees. ## Where it goes [#where-it-goes] The whole 1%, left to right: LPs 0.30%, Rakeback 0.20%, MOTO stakers 0.20%, the treasury 0.20%, and buyback and burn 0.10%. 70% of it goes back to traders, liquidity providers, and stakers; the [Rakeback](./rakeback.mdx) and [Earning](./earn.mdx) pages cover how to collect each cut. Most swaps cost `1%`. A graduated [moto.fun](/fun) coin adds a `0.3%` [creator fee](/fun/creator-fees), paid to its creator in cash, so a swap between the coin and ETH or MOTO costs `1.3%`. The `1%` covers one pool. When the best route passes through a second pool, that pool keeps its own `0.30%` for its LPs, and the rest of the fee is still charged once. So a route through two pools costs about `1.3%`, a swap from any other token into a graduated coin about `1.6%`, and a swap from one graduated coin to another about `1.9%`, because both creators get their fee. While a coin is still on its bonding curve, trades cost `1.2%` with `0.5%` of that to the creator. ## What you pay elsewhere [#what-you-pay-elsewhere] The headline fee is never the whole bill. What a swap actually costs, all-in, and what comes back: | Venue | All-in, per swap | Made of | Back to you | | -------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- | | Photon on a pump.fun token | `2.25%` each way | 1% bot + 1.25% curve fee | Nothing | | pump.fun bonding curve | `1.25%` | 0.95% protocol + 0.30% creator | Nothing | | Phantom in-wallet | `0.85%` + pool fee | 0.85% app + route pool fee + network fee | Nothing | | Uniswap app | `0.3%` to `1%` | The pool fee, set per pair, + gas | Nothing | | **Motoswap** | `1%` (`1.3%` on a moto.fun coin) | Everything: LPs 0.30%, Rakeback 0.20%, stakers 0.20%, treasury 0.20%, buyback and burn 0.10%, plus the creator fee on moto.fun coins | **0.20% back + Points** | Uniswap is the cheapest venue for majors. The trenches run `1.25%` to `2.25%` per side, and a bot round trip on a fresh launch costs `4.5%` before tips. None of it comes back. Motoswap prices below the trenches and pays like a casino. Fees checked July 2026 against official fee pages and published schedules. Pump.fun coins graduate to PumpSwap, where fees tier down as market cap grows; new coins pay `1.25%`. Venues change fees; when one does, this table gets updated. ## The gas bill [#the-gas-bill] Fees are only half of what a swap costs on Ethereum. The other half is gas, and gas moves with the network. Motoswap deploys its pairs as beacon proxies instead of full contracts, so creating a pool deploys a small proxy rather than a whole pair contract. ## At your volume [#at-your-volume] Rakeback alone, before Points: | Monthly volume | Back to you | | -------------- | ----------- | | `$10k` | **$20** | | `$50k` | **$100** | | `$250k` | **$500** | | `$1M` | **$2,000** | | `$10M` | **$20,000** | # Launch farming (/user/launch-farming) Launch farming has ended. It was Motoswap's opening farm: one window of `14 days` of liquidity mining that ran after the MOTO token launch and closed on the day the DEX opened. You staked Uniswap V2 LP tokens, or MOTO on its own, into Motoswap's farm and earned MOTO for it while the window was open. There is no new farm. The trick was what you did not have to do. Your liquidity stayed on Uniswap, in the pools it was already in, earning the fees it already earned. Only the LP token moved: you staked it with Motoswap, and Motoswap paid you MOTO on top of everything your position was already doing. ## How it worked [#how-it-worked] 1. You provided liquidity on Uniswap V2, or already had. You could also skip that step and use MOTO on its own. 2. You staked the LP token, or your MOTO, into the Motoswap farm. 3. You earned MOTO while it was staked and the window was open. Harvests paid `50%` liquid immediately and streamed the other `50%` over `180 days`, so a stream kept paying out after the window closed. 4. You could unstake at any point in one call. No cooldown, no penalty, and your Uniswap position stayed untouched throughout. ## Moving your position to the DEX [#moving-your-position-to-the-dex] If you still have LP in a launch farm, withdraw it on the Farm page. Your LP is safe and stays yours. From there, the move wizard runs three transactions, each signed by you: withdraw from the launch farm, remove from Uniswap, and add the same tokens on Motoswap. Nothing custodial, nothing automatic; it is the same three moves you could make by hand, in order. Why move: liquidity providers on Motoswap earn the 0.30% trading fee on every swap in their pool. The launch farm stopped paying when its window closed. Every swap through a Motoswap pool also carries the fee that pays [Rakeback](./rakeback.mdx) and [Points](./points.mdx). You can stay on Uniswap. What launch farming paid you is yours, and there is no deadline to move. LP still staked in a launch farm earns nothing new; withdraw it in one call whenever you are ready. # moto.fun (/user/moto-fun) moto.fun is Motoswap's bonding-curve launch surface. Anyone launches a coin in seconds: no liquidity to raise, no pool to seed yourself. The coin goes straight onto a curve and trading starts immediately. Every buy and sell moves the price. There is no presale and no allocation list; the curve is the market from the first block. You reach moto.fun from its tab inside the app. It runs on Ethereum. When Motoswap reaches other networks, each will have its own curves, and a coin launched on one network will live on that network. | | | | ------------------------------ | ---------------------------------------------------------------------------- | | Cost to launch | Free. A signature, unless you want a dev buy | | Fee while bonding | `1.2%` per trade, `0.5%` of it to the creator | | Fee after graduation | The normal Motoswap `1%`, plus the `0.3%` creator fee | | Graduation | At 80% of supply sold, into an ETH pool and a MOTO pool, both with burned LP | | Points, Rakeback and referrals | The same as any swap, including the 10% referral share | The full section covers it properly: # MOTO (/user/moto) MOTO is the ecosystem token. It is an ERC-20 on Ethereum, a fixed supply of 10 billion, with no owner, no mint function, and no way to pause it. What launches is all there will ever be. The full split is on the [Tokenomics](./tokenomics.mdx) page. ## What MOTO is for [#what-moto-is-for] Holding MOTO does nothing by itself. It works when you put it to work: * Stake it, and you take the other side of every trade. Stakers split a share of the swap fee, paid in the assets the fees were collected in, not in fresh tokens. * Provide liquidity with it, and liquidity providers on Motoswap earn the 0.30% trading fee on every swap in their pool. * The protocol buys MOTO off the market with part of the swap fee and burns it. Trading shrinks the supply over time. Nothing grows it. Every moto.fun coin that graduates gets a MOTO pool as well as an ETH pool, so demand for new coins is demand for the MOTO underneath them. ## Where MOTO trades [#where-moto-trades] MOTO trades on Motoswap. It launched in September 2026 as a MOTO/WETH pool on Uniswap V2. [Launch farming](./launch-farming.mdx) paid Uniswap LPs in MOTO over one 14-day window and has ended. Every swap through a Motoswap pool carries the fee that pays traders and stakers back. # Motocats (/user/motocats) Motocats are the club membership of the Motoswap ecosystem: 20,000 cats, minted free to the community, built to be staked. A staked cat collects from every [PvE launch](/pve) and counts toward your [Points](./points.mdx). A cat sitting in your wallet does neither. ## Born staked [#born-staked] Motocats were distributed free and delivered already staked: every cat except the honoraries started life inside the staking contract, assigned to its holder and earning from block one. Every PvE drop splits across staked cats at a snapshot. Starting staked put the whole collection in position from the first block, so nobody missed the early drops because of a transaction they did not know they had to make. ## Staking rules [#staking-rules] The rules: * Stake and unstake whenever you want. No lockup, no minimum time, no cooldown. * No fees and no slashing, in either direction, ever. * Move one cat or hundreds in a transaction. No cap per wallet. * Staked cats sit in the staking contract, so a cat must be unstaked before it can be sold or transferred. Unstaking is instant. ## What a staked cat earns [#what-a-staked-cat-earns] Every PvE launch splits a slice across staked cats, cat for cat. Claims never expire Staked cats multiply the Points your swaps earn. More cats, more boost The mechanics of the drops, snapshots, and claims live in the [PvE mode](/pve) section. The short version: stake before a drop and you are in it; your shares wait for you forever; you claim on Motoswap's [staking page](https://motoswap.org/stake/motocats) from launch day, and the claim is a straight transfer of the launched tokens. ## The art [#the-art] Every cat's traits, rank, and rarity live on its detail view on the [stake page](https://motoswap.org/stake/motocats). None of it affects staking or earnings: a floor cat and the rarest cat in the collection collect the same share per cat. # Points (/user/points) Points track what you do on Motoswap and count toward the SZN 1 airdrop. Nothing to opt into. Five levers, and they stack: Every fee-paying swap, any size, no minimum The fees your position generates Launching a coin on moto.fun that people trade 10% of what anyone you refer earns, one level Multiply your swap Points. More cats, more boost Staked [Motocats](./earn.mdx#stake-motocats) multiply the Points from every swap you make; a cat in your wallet boosts nothing until you stake it. Staked cats also collect [PvE drops](/pve), separate from Points. Balances and the leaderboard update with the daily drop at 00:00 UTC. What you see moves once a day, not per trade, so a swap you made this afternoon shows up in tomorrow's number. Leaderboard ## Referrals [#referrals] Share your link and you earn `10%` of the Points anyone you refer earns, on Motoswap swaps and moto.fun trades, one level deep, for as long as they trade. Your cut is added on top. The person you refer keeps every Point they earn. Your friend confirms the link with one signature before their first trade. After that it cannot change. Share it before they start trading. Your link lives on [your profile](./reference.mdx#your-profile), ready to copy. ## The weekly bonus [#the-weekly-bonus] A weekly bonus lane runs alongside the regular streams. It rewards staking a cat and leaving it staked, week after week. The stake page marks which of your cats is carrying the current week. The bonus amounts and the exact qualifying rules are not published; the habit that earns them is. ## Leaderboard and airdrop [#leaderboard-and-airdrop] Points rank you on the leaderboard. SZN 1 Points count toward the airdrop, so this season's total is the record the airdrop draws from. The Points-to-airdrop rate, the airdrop size, and the date are not set. When they are, official channels announce them and this page gets updated. # Rakeback (/user/rakeback) Rakeback is a casino word, used on purpose. `0.20%` of every swap you make comes back to you, in the quote asset of the trade. [The fix](./why/the-fix.mdx) covers why no DEX has paid rakeback on every swap before. | Rakeback | How it works | | -------------- | ------------------------------------------------------ | | You get back | `0.20%` of every swap | | Paid in | The quote asset of the trade (ETH, USDC, USDT, MOTO) | | Accrues | Weekly epochs, closing Sunday `00:00 UTC` | | Opens to claim | About a day after the epoch closes | | Unlocks | `50%` with the epoch, `25%` each of the next two weeks | | Claim | Any time, per token, from the Rakeback panel | | Expires | `30 days` after it becomes claimable | ## The weekly schedule [#the-weekly-schedule] Each week is an epoch, closing Sunday `00:00 UTC` and opening to claim about a day later. Trade `$20k` in a week and that epoch holds `$40` for you. `$20` is claimable with the epoch, `$10` more opens the next week, and the last `$10` the week after. Whatever you see claimable at any moment is a mix of the current epoch and the two before it. ## Claiming [#claiming] Open the Rakeback panel from the header. Claims settle on chain. Hit claim and your wallet asks you to confirm one transaction, the same as any other trade. There is no extra signature to approve first. Rakeback panel Each part expires `30 days` after it opens. Unclaimed Rakeback is then swept to the treasury. Claim inside that window and nothing is left behind. # Reference (/user/reference) ## Motocats [#motocats] Motocats is Motoswap's NFT collection: 20,000 cats. A staked cat does two jobs: it collects a share of every [PvE launch](/pve), and it boosts the Points your swaps earn. A cat in your wallet does neither. The full story is on the [Motocats page](./motocats.mdx). Motocats were distributed free and born staked: every cat except the honoraries started life in the staking contract, already assigned to its holder. There is no mint site, no whitelist, and no claim page, and there never will be. Anyone linking you to a Motocat mint or claim site is running a scam. ## The MOTO token [#the-moto-token] MOTO is Motoswap's ERC-20. Four things run through it: | Utility | How | | ------------------------------------------------- | -------------------------------------------------------------------------------------- | | [Staking](./earn.mdx#stake-moto) | The `0.20%` staker cut of every swap, in real fee assets | | [Launch farming](./earn.mdx#launch-farming-ended) | Ended. Paid MOTO to Uniswap LPs for 14 days at launch | | Routing and moto.fun | A base asset for routes, and every moto.fun coin that graduates gets a TOKEN/MOTO pool | | Buyback and burn | `0.10%` of every swap buys MOTO and burns it | Every moto.fun graduation seeds a MOTO pool and burns its pool tokens, so the MOTO under each new market can never be withdrawn. Why that compounds is [Paid players come back](./why/the-flywheel.mdx). ## Your profile [#your-profile] Everything you hold and everything waiting for you lives on your profile: token holdings, LP positions, your MOTO stake and its fee share, unclaimed [PvE drops](/pve), your full activity history, and your referral link. Your profile also has two more things. The vesting card shows the streamed half of your past farm harvests unlocking over its `180 days`, claimable as it arrives. And every action surface can hand you a share card: a rendered image of the trade, harvest, or position, sized for posting, generated on demand from any position you hold. ## FAQ [#faq] ### Trading [#trading] The price moved past your slippage tolerance between quote and submit, or the deadline passed. It reverted instead of filling badly; you lose gas, not tokens. Raise slippage a little if the market is moving fast. Warnings start at `10%` impact and escalate to a block at `50%`. High impact on a normal-size trade usually means a thin pool. The ladder is on [Trading](./trade.mdx#price-impact). Only once. After a one-time setup, standard tokens trade on a signature; fee-on-transfer tokens keep the classic approve-then-swap path. A large order can split across two routes when the split pays meaningfully more, and a split runs as two transactions. The app shows both legs before you trade. ### Earning [#earning] Launch farming has ended. If you still have LP in a launch farm, withdraw it on the Farm page. Your LP is safe and stays yours. Half of each harvest you already claimed paid out at once; the other half streams over `180 days` and waits on your profile, where you claim it as it unlocks. ### Points and Rakeback [#points-and-rakeback] Rakeback vests per weekly epoch: half with the epoch, a quarter each of the next two weeks. A recent epoch shows only part of what it will eventually pay. `30 days` after it becomes claimable, which is when the epoch's slice is activated, not when the epoch ends. Claim from the Rakeback panel any time before then. Only staked cats count; a cat in your wallet boosts nothing. And Points balances move once a day at the drop, so a boost you added today shows in tomorrow's number. You earn `10%` of the Points from anyone you refer, added for you, not taken from them. Your friend confirms the link with one signature before their first trade. After that it cannot change. Details on [Points](./points.mdx#referrals). Points balances update once a day, with the drop at 00:00 UTC. Today's trading shows up in tomorrow's number. ### Tokens [#tokens] Import it: the review step shows its address and flags symbol collisions. Verify the address from an official source first. ## Stay safe [#stay-safe] Self-custody means no one can freeze or reverse a bad transaction for you. A few habits keep that from working against you. ### Motoswap will never message you first [#motoswap-will-never-message-you-first] Motoswap never DMs you first, never asks for your recovery phrase, and has no support staff who reach out over chat. Anyone who does any of these is trying to rob you. ### Verify the URL [#verify-the-url] Attackers clone the site at look-alike addresses and wait for you to connect. Reach Motoswap from a bookmark you saved yourself, not a link someone sent you. ### The import review is a real stop [#the-import-review-is-a-real-stop] When an import asks you to review a token, that is the safety check, not a formality. What to verify is on [Trading](./trade.mdx#importing-a-token). ### Approval hygiene [#approval-hygiene] An approval you leave open lets a malicious token contract drain that token. Approve what you need and revoke approvals for tokens you no longer trade. ### No Motocat mint or claim site [#no-motocat-mint-or-claim-site] There is no Motocat mint or claim site. Cats arrived staked. Any site offering a Motocat mint, whitelist, or claim is a scam. # Getting started (/user/start) Most DEXes are built like an on-chain NYSE, waiting for institutions that never came. The people actually trading are degens. Motoswap is built for them: a casino, on purpose, wired in at the contract level. The house pays you to play. 0.20% of every swap back to you, in the asset you traded against Every swap counts toward the SZN 1 airdrop Become the house: stakers split 0.20% of every swap, in the assets traded against Staked cats collect PvE drops and boost your Points Bonding curve, live in seconds, no pool to seed You were always the house's revenue. Six chapters on what changed Everything ships in order, and this table is the one place in these docs that knows what time it is: | | | | ---------------------------------- | ------------------- | | Motocats + staking, PvE claims | Live | | MOTO token | Live | | Launch farming | Ended (ran 14 days) | | The DEX: trading, Rakeback, Points | Live | | moto.fun | Live | The [launch farming](./launch-farming.mdx) page stays up as the record of that farm, and shows how to move a position that is still staked in it. None of this is a promo budget. It is the swap fee handed back to the people paying it: [the 1%](./fee.mdx) shows the whole split, and what the same swap costs you everywhere else. Under the hood: a Uniswap-v2-style DEX on Ethereum. Constant-product pools, self-custody throughout, any standard wallet. Why it had to be built this way is a story about a house, its players, and the fee that never came back. [Six chapters, starting here](./why/the-question.mdx). # Tokenomics (/user/tokenomics) MOTO is a fixed 10 billion tokens, forever. The token has no owner, no mint function, no burn function, and no pause. Burning means sending to a dead address that nobody can spend from. What exists at launch is all there will ever be. ## The split [#the-split] This is the genesis allocation. It sums to 100%. | Bucket | Share | MOTO | What it is | | --------------- | ----- | ------------- | ------------------------------------------------------------------------------- | | Initial LP Seed | 40% | 4,000,000,000 | The MOTO/WETH pool at launch. The LP moves to Motoswap on DEX day. | | Treasury Lock | 19% | 1,900,000,000 | Vesting treasury, ten equal unlocks, one every 180 days, over about five years. | | Treasury Unlock | 16% | 1,600,000,000 | Working capital. | | Community | 22% | 2,200,000,000 | Points, seasonal airdrops and community programs, allocated as each one opens. | | Launch farming | 3% | 300,000,000 | The opening farm, paid at a flat rate over its 14 days, now ended. | ## What the numbers mean [#what-the-numbers-mean] Most of the supply is liquidity: 40% seeds the market so trading works from the first block. Launch farming paid Uniswap LPs and MOTO holders for 14 days at token launch, on a public, flat schedule nobody could change after the fact. It has ended, and there is no second round: no new farm is running or promised, on Ethereum or anywhere else. The two treasury buckets are split on purpose. The locked 19% vests slowly over five years, so it cannot hit the market all at once. The 16% unlock is working capital for building. The token cannot be inflated. There is no mint, so no new MOTO is ever created. The only supply change possible is downward, through buyback and burn: a slice of the swap fee buys MOTO off the market and sends it to the dead address. Trading shrinks the supply over time. Nothing grows it. # Trading (/user/trade) Every swap on Motoswap earns you `0.20%` back as [Rakeback](./rakeback.mdx) plus [Points](./points.mdx). This page is everything between you and the confirm button. ## How a swap works [#how-a-swap-works] Quotes are live from the chain and refresh while you type. A review step shows the rate, price impact, fees, network cost, and what you earn back before you sign. Slippage defaults to `0.5%` and is adjustable behind the cog along with the transaction deadline; the app caps slippage at `50%`. Swap options If the price moves past your tolerance between quote and fill, the transaction reverts. You lose gas, not tokens. If that keeps happening on a busy pool, a slightly higher tolerance gives the price more room. Expert mode skips the review step. You arm it deliberately in settings with a typed confirmation; leave it off unless you know why you want it. Swap preview ## Approvals [#approvals] After a one-time approval per token, you trade it by signing a message instead of sending an approval transaction each time. Fee-on-transfer tokens keep the classic approve-then-swap path. ## Price impact [#price-impact] Below `10%` impact the app stays quiet. Past that, it escalates: | Impact | The app | | ------ | --------------------------------------------------------------------------------- | | `10%` | Warns you | | `15%` | Shows the impact in red | | `20%` | Adds a red warning about what you stand to lose. There is nothing to tick or type | | `50%` | Blocks the trade, unless expert mode is on | High impact on a normal-size trade usually means a thin pool. ## Routing [#routing] The app checks the direct pool and every hop through WETH, USDC and MOTO, plus the most liquid tokens on Motoswap, then quotes the path that pays the most. You never pick a route, and every route is re-priced live at submit: past your tolerance, it reverts instead of filling badly. A large trade can pay more split across two routes than through one pool. When a split wins by a meaningful margin, the app shows both legs and the improvement. A split runs as two transactions, so your wallet confirms each leg. ## Importing a token [#importing-a-token] A token not on the Motoswap token list stays out of your picker until you import it, and import is a review step, not one click. The review flags a symbol that collides with a listed token, which is exactly what an impersonator would do. Anyone can create a token with any name and symbol. Get the address from an official source and check it against the import step character for character before you trade. # Addresses and discovery (/dev/fun/addresses) > **Read addresses at runtime from `GET https://api.motoswap.org/config`.** That endpoint is the > canonical source for both the DEX and moto.fun. This page documents the shape you get back; it > does not publish address values, and hardcoding any address you find elsewhere will break your > integration silently. ## Mainnet (chain 1) [#mainnet-chain-1] moto.fun is deployed on Ethereum with the DEX. Read its addresses from `/config`. If a deploy has moto.fun off, its entries read as the zero address, `motofunEnabled` is `false`, `motofunUrl` is empty, and every `/motofun/*` route answers `404`. Deployed is not the same as open: `launchPhase` in `/config` reports the phase, and the app backend's `publicOpen` reports whether moto.fun is open to users. This table names the keys; `/config` holds the values. | Contract | `/config` key | Address | | :------------------- | :----------------------------- | :------------------------------------------------------- | | `LaunchpadCurve` | `addresses.launchpadCurve` | Deployed. Read the value from `/config` | | `LaunchpadLens` | `addresses.launchpadLens` | Deployed. Read the value from `/config` | | `CreatorFeeRegistry` | `addresses.creatorFeeRegistry` | Deployed. Read the value from `/config` | | `CreatorFeeVault` | `addresses.creatorFeeVault` | Deployed. Read the value from `/config` | | `PveVault` | `addresses.pveVault` | Deployed. Read the value from `/config` | | `MotoToken` (MOTO) | `addresses.motoToken` | Deployed. Read the value from `/config` | | WETH | `addresses.weth` | `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` (canonical) | `addresses.launchpadCurve` is a proxy, and it is the only curve address you ever call. The implementation behind it can be replaced through a timelock; see [Who can change the curve](/dev/fun/contracts#who-can-change-the-curve). Do not pin or call an implementation address. `LaunchpadToken` has no `/config` key. It is not a contract you call: read the implementation address from the moto.fun backend's own `/config` if you need it for CREATE2 derivation, or use `LaunchpadLens.predictToken` and skip the derivation entirely. ## The two config endpoints [#the-two-config-endpoints] They serve different things and both are worth reading once at startup. ### `GET https://api.motoswap.org/config` [#get-httpsapimotoswaporgconfig] The Motoswap API's config, cached 300s. The moto.fun keys in it: ```jsonc { "addresses": { "launchpadCurve": "0x…", // zero when moto.fun display is off "launchpadLens": "0x…" // gated independently of the curve }, "motofunEnabled": true, // boolean "motofunUrl": "https://…", // the moto.fun app origin, "" to fall back "launchPhase": "dex" // "prelaunch" | "vamp" (before the DEX) | "dex" } ``` `motofunEnabled` is exactly `launchpadCurve != 0x0`, so branching on the address alone is correct. The curve and the lens gate independently: a non-zero curve with a zero lens means the read surface is unavailable, and you should not quote. ### `GET /backend-fun//config` [#get-motofunurlbackend-funchain-slugconfig] The moto.fun service's own config, static per deploy, no DB read and no RPC: ```jsonc { "chainId": 1, "addresses": { "curve": "0x…", "lens": "0x…", "tokenImplementation": "0x…", "pveVault": "0x…", // zero when PvE is not live on this chain "weth": "0x…", "moto": "0x…", "creatorFeeRegistry": "0x…", // resolved off the curve. null until that first read lands "creatorFeeVault": "0x…" // same }, "eip712": { "name": "MotoswapLaunchpad", "version": "1" }, "motofunEnabled": true, // false = the service is up but no curve on this chain "publicOpen": true, // false = the app shows its launch-pending screen "launchOpensAt": null // ISO 8601 UTC string, or null when no opening time was set } ``` `publicOpen` gates a screen in the app and nothing else. Every route on the service answers normally while it is `false`, so do not read it as "the API is down" or "the curve is paused". `motofunEnabled` answers a different question: whether a curve exists on this chain at all. `pveVault` is served as the zero address when PvE is not live, never omitted. Branch on the value being `0x0`, not on the key being absent. The same rule holds for every address on both endpoints, with one exception: `creatorFeeRegistry` and `creatorFeeVault` on the app backend are read off the curve after boot, and serve `null` until that read has landed. `null` means "not known yet". It does not mean the zero address, so retry rather than treating the fee contracts as absent. The chain slug is per network and the same slug appears in share links as `?chain=`. Resolve `motofunUrl` from the Motoswap `/config` rather than assuming a hostname. ## Verify before you trade [#verify-before-you-trade] The app's own rule, worth copying: before any wallet prompt it checks that the served curve address matches the address baked into its build. A served config that disagrees with what you pinned means a redeploy happened, not that you should follow the new one blindly. If you cache addresses, cache them for minutes and re-read on any unexpected revert. If you pin them, pin them deliberately and fail closed when the served value differs. ## Deriving a coin's address [#deriving-a-coins-address] A coin's address is known before the coin exists, which is how a pending launch can be listed and linked. Two ways to get it: ```ts // Preferred: ask the lens. const predicted = await client.readContract({ address: cfg.addresses.launchpadLens, abi: parseAbi(["function predictToken(address creator, uint256 salt) view returns (address)"]), functionName: "predictToken", args: [creator, salt], }); ``` Deriving it offline is possible. Get these three things right: * The deployer is the **curve**, not the creator. * The init code is an **EIP-1167 minimal proxy** over `tokenImplementation`. * The **CREATE2 salt is `keccak256(abi.encode(creator, salt))`**, not the `salt` from the intent. That last one is deliberate: a salt replayed from another wallet lands at a different address, so a signed intent cannot be hijacked into the address someone else advertised. ## Where the curve's history starts [#where-the-curves-history-starts] To index the curve from chain you need the block its proxy was deployed in. No endpoint serves that block today. Take it from the explorer's contract-creation record for `addresses.launchpadCurve`. It is also the first block at which that address has code, and the block that holds the proxy's `Upgraded` and `Initialized` events. Start your `TokenCreated` backfill there: [listing moto.fun coins](/dev/integrations/moto-fun#discovery) has the details. ## Networks [#networks] moto.fun opens on Ethereum only. Other networks will come later, each with its own curve deployment and its own coin set. When they do, ask `/config` per network rather than assuming which are live; a network whose upstream is not configured answers the config path with the app's HTML rather than JSON, which is the signal that moto.fun is not live there. Do not enumerate networks from a hardcoded list. The set changes. ## Local development [#local-development] Local addresses are not listed here, and pinning them would be wrong anyway: a local stack mints fresh addresses every run. Start the stack and read its `/config`, which is exactly how the app and the indexer discover them. # REST API (/dev/fun/api) moto.fun data is served by two different services. Pick deliberately. | | Motoswap API | The moto.fun app backend | | :----------------- | :---------------------------------------- | :--------------------------------------------------------------------------------- | | Base | `https://api.motoswap.org` | `/backend-fun/` | | In `/openapi.json` | Yes | No | | ETags / `304` | Yes, every route | Yes, on JSON `200` responses | | Pagination | Cursor, `{ items, nextCursor, hasMore }` | Signed cursors on the board and the graduated feed. Hardcoded caps everywhere else | | Realtime | No | Server-Sent Events | | Candles | No | Yes | | Auth | None | None on reads | | Use it for | Discovery, listings, venue stats, revenue | Charts, holders, pending launches, streams | **Full endpoint reference:** [moto.fun API inventory](/dev/reference/fun-api-full-inventory). `api.motoswap.org` is the permanent public host and the documented surface, and its `/motofun/*` routes are in `/openapi.json`. Reach for the app backend only for what the Motoswap API does not carry. ## Motoswap API: the `/motofun` group [#motoswap-api-the-motofun-group] | Endpoint | Purpose | | :------------------------------------------- | :----------------------------------------------------------------------------------------------------------------- | | `GET /motofun/stats` | Venue counters: coins by lifecycle, TVL, 24h and all-time volume, fees split protocol vs creator, trades, launches | | `GET /motofun/coins?cursor=` | Newest-first coin list, 25 per page, with status, both pair addresses, volume, trade count, last price | | `GET /motofun/coins/{address}` | Point lookup. "Is this a curve coin, and has it graduated?" | | `GET /motofun/revenue/series?range=&bucket=` | Per-bucket venue revenue, Collector leg and creator leg separately | Plus five that carry moto.fun without being under the prefix: | Endpoint | moto.fun content | | :------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------- | | `GET /config` | `addresses.launchpadCurve`, `addresses.launchpadLens`, `motofunEnabled`, `motofunUrl` | | `GET /activity?venue=motofun` | The unified feed, filtered to `motofun_launch`, `motofun_trade`, `motofun_graduation` | | `GET /analytics/overview` | An optional top-level `motofun: { volumeUsd, feesUsd }`, absent when moto.fun is off | | `GET /creator/{address}` | Per-coin creator earnings, each tagged `venue: "motofun" \| "deployer"`. `"deployer"` marks coins from the retired launch product | | `GET /launchpad/token/{address}/logo` | A coin's logo as bytes, mirrored from moto.fun | ### Semantics that will bite you [#semantics-that-will-bite-you] * **A `404` usually means "moto.fun is off on this deploy"**, not "no data". Every `/motofun/*` route answers `404 {"error":"moto.fun is not enabled on this deploy"}` when the curve address is zero. Check `/config` first and you will never see that one. The point lookup has a second `404` of its own: with the venue on, `GET /motofun/coins/{address}` answers `404 {"error":"this token never launched on the moto.fun curve"}` for any address that is not a curve coin. That is the ordinary answer for an ordinary ERC-20, so tell the two apart by the body, or by reading `motofunEnabled` first. * **`status` is folded on read**, from the latest surviving lifecycle event. A coin with no lifecycle row is `"bonding"`. There is no stored status column, deliberately, so a reorg is a range delete with nothing to rebuild. * **USD fields are frozen write-time snapshots**, never re-priced, except `tvlUsd`, which is a live gauge. A trade recorded at one ETH price keeps that dollar value forever. * **The 24h window is anchored to the indexer head block timestamp**, not to wall clock. * **A malformed cursor returns an empty page with `200`**, not a `403`. Do not read an empty page as "end of list" without checking `hasMore`. * **`/motofun/*` ETags version on the curve's own head block**, so they stay `304` through DEX-only blocks. Poll them freely with `If-None-Match`. ### Wire types [#wire-types] Same conventions as the rest of the Motoswap API: addresses lowercase, wei and uint256 as decimal strings, USD as decimal-dollar strings, seconds as numbers. See the [API wire types cheat sheet](/dev/api#wire-types-cheat-sheet). Rate limit on `/motofun/*` is per-IP and generous; `/launchpad/*` is tighter because the logo route serves bytes. ## The app backend [#the-app-backend] Everything below is under `/backend-fun//`. No auth on reads, `{"error": "..."}` on failure. ETags are served, on JSON `200` responses only. The public host is `motofunUrl` from `GET https://api.motoswap.org/config`. Read it there rather than pinning one. Pre-release environments are access-controlled and answer `401` without credentials. Everything on chain is readable without any. | Endpoint | Purpose | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET /config` | Curve, lens, token implementation, PvE vault, WETH, MOTO, the EIP-712 domain | | `GET /health` | Indexer lag, frozen coins, keeper state, PvE failures. Operational, not a data source | | `GET /health/live` | Liveness only: `{ ok, commit }`. Poll this rather than `/health` | | `GET /version` | Which build is serving right now: `{ commit, builtAt, chainId }` | | `GET /status` | The public three-field health answer: `state`, a short `checks` list, and an operator `notice` (an object or `null`). Use this, not `/health`, to tell an outage from your own network | | `GET /tokens?limit=&cursor=&q=` | The board: coins plus pending intents plus the ETH/USD rate. Paginated, see below | | `GET /tokens/{address}` | One coin, with a live curve read (including its `paramsId`), recent trades, stats, and PvE totals | | `GET /tokens/{address}/quote?side=&amount=` | A lens quote served from the backend. `side` is `buy` or `sell`, `amount` is wei of ETH in or of coin in | | `GET /tokens/{address}/snapshot` | The coin page in one body: the detail, the holders, and optionally candles and a quote, in the same shapes as the separate routes | | `GET /graduated/feed?limit=&cursor=` | Graduated coins, newest graduation first, with both pair addresses. The feed for listing sites | | `GET /tokens/{address}/candles?resolution=&from=` | OHLCV buckets. See [realtime](/dev/fun/realtime) | | `GET /tokens/{address}/holders` | Top 10 net holders, the true holder count, and the PvE escrow balance | | `GET /tokens/{address}/image` | The creator's picture as bytes, re-verified against the on-chain hash | | `GET /tokens/{address}/card.png` | A 1200x630 share card | | `GET /share/{address}` | The crawler unfurl page. HTML, and `text/plain` on error | | `GET /trades/recent` | 20 newest trades and 6 newest launches from the last 24h | | `GET /stats` | Coin counts, volume, creator fees, PvE totals | | `GET /creators/top` | Top 25 creators by fees earned, folded per creator | | `GET /pve/value` | What the PvE escrow is currently worth on this chain | | `GET /wallet/{address}/positions` | Net positions with cost basis. Rate-limited | | `GET /wallet/{address}/trades-today` | The wallet's trades since 00:00 UTC today. Rate-limited | | `GET /wallet/{address}/fee-reroutes` | Coins whose creator fee failed over to the Collector | | `GET /fees/history/{wallet}` | Lifetime creator-fee claims. Rate-limited. Carries no live balances | | `GET /intents/precheck?creator=&symbol=` | Pre-signature advisory: throttled, reserved, slots free | | `POST /intents` | Submit a signed `LaunchIntent`. See below | | `GET /intents/{address}` | A pending launch's signed payload, verbatim | | `GET /stream` | Server-Sent Events. See [realtime](/dev/fun/realtime) | Not for integration: `GET /tickers/{symbol}/available` is deprecated and always answers `true`. The app also calls a handful of routes for its own sign-in and reporting flows (`/terms/*`, `/screening/{address}`, `/geo/gate`, `POST /reports`). They exist for the app, their shapes are not documented, and they can change without notice. There is also an operator-gated `/admin/*` group covering moderation, the audit log, metadata overrides and bans. It answers `401` without a credential, it is `no-store` throughout, and its shapes are deliberately not documented here. Do not build against it. ### Caching and ETags [#caching-and-etags] Every route carries a `Cache-Control` value chosen per route, and JSON `200` responses also carry a weak ETag. Send `If-None-Match` and you get a `304` with no body, keeping the same `Cache-Control` and the same `ETag`. | Tier | Routes | | :------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------- | | `no-store` | `/health`, `/health/live`, `/version`, `/intents/precheck`, `POST /intents`, `/stream`, every `/admin/*`, and all four wallet money routes | | `no-cache` (store, revalidate every time) | `/config`, `/intents/{address}`, `/tokens`, `/tokens/{address}`, its candles and holders, `/trades/recent`, `/stats` | | `public, max-age=10, stale-while-revalidate=20` | `/creators/top` | | `public, max-age=30, stale-while-revalidate=30` | `/pve/value` | | `public, max-age=300, stale-while-revalidate=600` | `/share/{address}` | | `public, max-age=300` | `/tickers/{symbol}/available` | | Set by the handler | `/tokens/{address}/image` (`max-age=86400` on the CDN redirect, `max-age=300` inline) and `/tokens/{address}/card.png` (`max-age=30`) | Three things follow from this that will catch you out. `no-cache` does not mean "do not cache", it means "store it and revalidate before every reuse", which is the tier most of this API wants. The wallet money routes are `no-store` rather than `no-cache` on purpose, so a balance cannot be read back off a shared cache. And there are no ETags on redirects, on the PNG routes, or on any non-`200`, so do not write a client that expects one everywhere. ### Rate limits [#rate-limits] Per-IP token buckets, each over a 10-minute window with continuous refill. The caps below are the defaults and every one is env-configurable, so treat them as the shipping values rather than as a contract. | Bucket | Env var | Default | Applies to | | :-------------- | :------------------------------------ | :------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Intent writes | `INTENTS_PER_IP_PER_10MIN` | 5 | `POST /intents` | | Board | `BOARD_READS_PER_IP_PER_10MIN` | 6000 | `GET /tokens` | | Coin detail | `TOKEN_DETAIL_READS_PER_IP_PER_10MIN` | 30000 | `GET /tokens/{address}`, and one token per `snapshot` | | Quotes | `QUOTE_READS_PER_IP_PER_10MIN` | 28800 | `GET /tokens/{address}/quote`. A second, smaller budget covers quotes that miss the cache and reach the chain, and answers `429 {"error":"too many new quotes"}` | | Expensive reads | `READS_PER_IP_PER_10MIN` | 3000 | `/pve/value`, `/status`, `/graduated/feed`, `/wallet/{address}/trades-today`, `/wallet/{address}/positions`, `/fees/history/{wallet}` | | Creators | `CREATORS_TOP_READS_PER_IP_PER_10MIN` | 3000 | `GET /creators/top` | Every other public read is unlimited, `/health` and `/health/live` included. A refusal is `429 { "error": "rate limited" }`. `/tokens`, `/pve/value` and `/creators/top` add `Retry-After` in seconds plus `Cache-Control: no-store` on the refusal. The other throttled routes answer `429` with no `Retry-After` yet, so back off on your own clock there rather than assuming the header. Because the buckets refill continuously, `Retry-After` is the wait for one token and is usually about a second, not the rest of the window. The stream is capped separately: 2000 connections globally and 40 per IP by default, both env-configurable. Over the per-IP cap you get `429`, over the global cap `503`, each with `Retry-After`. ### Pagination, and caps where there is none [#pagination-and-caps-where-there-is-none] Two lists page with a cursor: the board and the graduated feed. ``` GET /tokens the whole board in one body: up to 500 coins, 100 pending GET /tokens?limit=50 one page. limit is 1 to 100, default 50 once you page GET /tokens?limit=50&cursor=... the next page, from the previous body's nextCursor GET /tokens?q=cat a case-insensitive search, at most 64 characters, pages the same way ``` Every board body carries `nextCursor`: a string when the page was full, `null` when you have reached the end. Pages run newest coin first. The graduated feed works the same way and returns `{ coins, nextCursor }`, with a default `limit` of 100. The cursor is opaque and signed. Do not build one, edit one, or carry one from one search to another: a cursor that fails its signature, or that was issued for a different `q`, answers `400 {"error":"bad cursor"}`. A `q` longer than 64 characters answers `400 {"error":"bad q"}`. The two lists treat `limit` differently: on `/tokens` anything outside 1 to 100 answers `400 {"error":"bad limit"}`, while on `/graduated/feed` only a non-number or a value below 1 is refused, and a value above 200 is quietly clamped to 200. A cursor can also stop verifying after a backend restart, so treat `bad cursor` as "start the scan again", not as a fatal error. Sending no parameters at all keeps the original behaviour, one body with the whole board, and that request is the one served from the 5-second cache. Prefer it for a poll and the paged form for a backfill. Everything else has a server-side cap and no paging: 50 recent trades on a coin, 10 holders, 20 trades and 6 creations on `/trades/recent`, 25 creators, 200 fee claims. `/wallet/{address}/positions` is the one uncapped list, dust-filtered. If you need more than a cap gives you, index the events yourself. See [scanners](/dev/integrations/scanners#motofun-curve-events). ### Response shapes are open, not closed [#response-shapes-are-open-not-closed] `GET /tokens`, `GET /tokens/{address}` and `GET /intents/{address}` select whole rows, so a schema migration adds fields to the response automatically. `GET /config` and `GET /version` have grown the same way: `/config` now also serves `addresses.creatorFeeRegistry`, `addresses.creatorFeeVault`, `publicOpen` and `launchOpensAt`, and `/version` also serves `dirty` and `branch`. Treat the documented field lists as **at least these fields** and ignore what you do not recognise. Do not write a strict decoder that rejects unknown keys. ### Nullability is not uniform [#nullability-is-not-uniform] `ethUsd` is nullable on `/stats` and `/pve/value` (a null means no real price was available, never a fabricated one) and non-nullable on `/tokens`, `/tokens/{address}` and `/creators/top` (those fall back to a configured rate). This is intentional. Do not assume one rule across the service. `metadata` is `null` rather than wrong when the stored blob's hash disagrees with the on-chain commitment. `curve` is `null` when the chain read failed past its stale window, with `curveLive: false` marking a stale-but-served state. A throttled RPC renders as `curveLive: false`, never as a `404`. ## Submitting a launch intent [#submitting-a-launch-intent] `POST /intents` is open, gated by an EIP-712 signature over the payload and an IP rate limit of 5 per 10 minutes by default. The backend never holds launch authority: the chain re-verifies the same signature at materialization, so a compromised backend cannot mint a coin. ``` LaunchIntent(address creator, address submitter, string name, string symbol, bytes32 metadataHash, uint256 salt, uint256 nonce, uint256 deadline, address feeRecipient) ``` Domain: `{ name: "MotoswapLaunchpad", version: "1", chainId, verifyingContract: }`, all four from the served `/config` plus the chain you are on. Field order is load-bearing. `metadataHash` is `keccak256` of the exact serialized metadata JSON. `nonce` must equal the on-chain `launchNonce(creator)` or you get a `409` naming the value to use. `deadline` is at most 7 days out. `submitter` of `0x0` means anyone may submit the first buy; set it to yourself if you are taking a dev buy, or the signature is a bearer token in the mempool. Responses: `201 { predictedAddress, metadataHash }`, `409` on a nonce or salt collision, `429` on either throttle, `503` when the chain cannot be reached or moto.fun is not live on this chain. ## Where to go next [#where-to-go-next] * [Full API inventory](/dev/reference/fun-api-full-inventory) for exact request and response shapes. * [Realtime](/dev/fun/realtime) for the stream and the candle rules. * [AI agents](/dev/integrations/ai-agents#motofun) for the read-only agent loop. # Contracts (/dev/fun/contracts) Five contracts on the trading path. Two of them (`CreatorFeeRegistry`, `CreatorFeeVault`) are the same deployments the DEX uses. A sixth, `GasTank`, funds the keeper and is never called by a user. Full per-contract surface with every function, event, error, constant and access rule, read from source at a named commit: [moto.fun contracts inventory](/dev/reference/fun-contracts-full-inventory). | Contract | What it does | Upgradeable | | :------------------- | :--------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ | | `LaunchpadCurve` | The singleton: launch, buy, sell, graduate, PvE escrow, fee routing, the MOTO price checkpoint | Yes. A UUPS proxy. You call the proxy address, which is the one `/config` serves. See [Who can change the curve](#who-can-change-the-curve) | | `LaunchpadToken` | The ERC-20 template. One EIP-1167 clone per coin | No. No owner, no proxy, one-shot `initialize` | | `LaunchpadLens` | Read-only companion. Exists because the curve sits against the EIP-170 size limit | No | | `CreatorFeeRegistry` | Which coins have a creator fee and whose it is | No. `Ownable2Step` | | `CreatorFeeVault` | Per-coin, per-quote-asset claim escrow | No. `Ownable2Step` | | `GasTank` | Holds ETH for the keeper and refills it on demand. Ops only | No. `Ownable2Step`, renounce disabled | Three more pieces are deployed alongside the curve and are never called directly: `GraduationSeeder`, `MotoFloorMath` and `LaunchpadTokenDeployer`. They are linked libraries the curve runs by `delegatecall`, split out only to keep the curve under the contract size limit. Their code executes as the curve, so every event still comes from the curve's address and there is nothing extra to index. The errors they raise surface from the curve call and are declared on the curve's ABI as well, so one ABI decodes everything. The curve also depends on `PveVault`, a canonical Motoswap contract, for PvE escrow. Its surface and the cat-stake mirror that will feed it on future L2 deployments are in the inventory too. ## Who can change the curve [#who-can-change-the-curve] Two separate roles, and they are separate on purpose. * **The owner** holds the operational setters: fee rates inside their caps, the graduation bounty, the Collector, the PvE vault and keeper, the pause switches, and the launch parameters for future coins. It is `Ownable2Step`. * **The upgrade authority** is the only account that can replace the implementation, and it is a timelock, not the owner. Read it with `upgradeAuthority()`. The owner cannot reassign it: `setUpgradeAuthority` is gated on the current authority, so a change of authority is itself a delayed action. The timelock is a standard OpenZeppelin `TimelockController`, so you can check it yourself: `getMinDelay()` on that address returns the delay in seconds. `renounceUpgradeability()` is one-way and freezes the implementation for good while every operational setter keeps working. What this means for an integration: the address you call never changes, because it is the proxy. An upgrade can change behaviour behind it, so watch `Upgraded(address indexed implementation)` on the curve address and `UpgradeAuthoritySet(address indexed previous, address indexed next)` if you want to know when that happens. Nothing the owner can do stops a sell. The pause switches cover launches and buys only. ## Curve state and constants [#curve-state-and-constants] ```solidity enum Status { None, Trading, Frozen, Graduated } struct Curve { uint112 realEth; uint112 tokenReserve; Status status; bool buysPaused; uint16 paramsId; // which launch parameter set prices this coin, fixed at launch } mapping(address => Curve) public curves; // keyed by coin address, returns all five members mapping(address => uint256) public launchNonce; // per creator, for intents mapping(address => address) public creatorOf; // the signer, not the fee recipient mapping(address => uint256) public pveAccrued; // escrow awaiting the graduation credit mapping(address => uint256) public frozenAt; ``` `curves(token)` returns five values. An ABI fragment written with four outputs predates `paramsId` and should be updated. | Constant | Value | Meaning | | :------------------------------------ | :-------------------- | :----------------------------------------------------------------------------- | | `SUPPLY` | `1_000_000_000 ether` | Fixed supply per coin. The one curve number that is the same for every coin | | `FLOOR` | `200_000_000 ether` | **Parameter set `0`.** The graduation floor for a coin whose `paramsId` is `0` | | `VIRTUAL_TOKENS` | `66_666_667 ether` | **Parameter set `0`.** The virtual token reserve for `paramsId` `0` | | `VIRTUAL_ETH` | `4 ether / 3` | **Parameter set `0`.** The virtual ETH reserve for `paramsId` `0` | | `MAX_PROTOCOL_FEE_BPS` | `100` | Cap on `protocolFeeBps` | | `MAX_CREATOR_FEE_BPS` | `50` | Cap on `creatorFeeBps` | | `MAX_BOUNTY` | `0.08 ether` | Cap on `graduationBounty` | | `SELL_REOPEN_DELAY` | `15 minutes` | How long a `Frozen` coin waits before `sell` reopens | | `MIN_TWAP_WINDOW` / `MAX_TWAP_WINDOW` | *not published* | The MOTO reference window | Live, owner-tunable, read them rather than pinning them: `protocolFeeBps` (ships at `70`), `creatorFeeBps` (ships at `50`, which is also its cap), `graduationBounty` (ships at `0.015 ether`), `maxTwapDeviationBps` (the MOTO buyback tolerance band, whose value and bounds are deliberately not published here), `collector`, `pveVault`. ## Each coin has its own curve parameters [#each-coin-has-its-own-curve-parameters] `FLOOR`, `VIRTUAL_TOKENS` and `VIRTUAL_ETH` are parameter set `0`: what a coin with `paramsId == 0` resolves to. That is the set a fresh deployment launches coins under, because a launch is stamped with `nextParamsId` and that reads `0` until the owner first appends a set. So these are real, in-force numbers for every coin stamped `0`. What they are not is a property of the contract: they belong to one parameter set. The owner can append a new parameter set, and every coin launched after that is stamped with the new id and priced by it for life. A coin already on the curve never moves: its id is written once at launch and the parameter table is append-only, so no later change can reach it. The read is one call: ```solidity function paramsOf(address token) view returns (uint256 virtualEth, uint256 floorSupply, uint256 virtualTokens); ``` Use `paramsOf(token)` everywhere you would have used `VIRTUAL_ETH`, `FLOOR` or `VIRTUAL_TOKENS`. Different coins on the same curve contract can carry different values, so never cache one coin's answer for another. Related reads: `nextParamsId()` is the id the next launch will be stamped with, and `launchParams(uint16 id)` returns one table entry. `launchParams(0)` returns zeros, not the set `0` constants, because id `0` is not a table entry. `paramsOf` handles that case for you, which is the reason to prefer it. The owner moves the dial with `setLaunchParams(uint128 virtualEth, uint128 floorSupply)`, which emits `LaunchParamsSet(uint16 indexed id, uint128 virtualEth, uint128 floorSupply, uint256 virtualTokens, uint256 impliedRaise)`. The virtual token reserve is derived by the contract, never supplied. The implied raise must sit inside an owner-set band (`raiseBand()`, changed by `setRaiseBand`, which emits `RaiseBandSet`). The band's bounds are not published here. What the dial changes in practice is how much ETH a coin raises before it graduates. Do not assume one figure for every coin. ## The pricing math [#the-pricing-math] Constant product against virtual reserves on both sides. There is no pair and no `getAmountsOut`; this is the whole model: ```solidity (uint256 vEth, uint256 floorSupply, uint256 vTok) = paramsOf(token); // buy: fee off the input, then the swap uint256 net = gross - (gross * (protocolFeeBps + creatorFeeBps)) / 10_000; uint256 ethVirt = vEth + c.realEth; uint256 tokVirt = vTok + c.tokenReserve; tokensOut = (tokVirt * net) / (ethVirt + net); // sell: the swap, then the fee off the output uint256 grossOut = (ethVirt * tokensIn) / (tokVirt + tokensIn); ethOut = grossOut - (grossOut * (protocolFeeBps + creatorFeeBps)) / 10_000; // spot, 1e18 fixed point, the same number Trade.priceX18 carries price = ((vEth + c.realEth) * 1e18) / (vTok + c.tokenReserve); ``` Only the **net** ETH enters `realEth`. The fee is wrapped to WETH and routed out in the same call. A buy that would take the reserve below `floorSupply` is filled up to the floor, charged only for what it bought, refunded the rest, and freezes the coin. Three inputs to a quote are live state: `protocolFeeBps`, `creatorFeeBps`, and the coin's own curve parameters. `LaunchpadLens.quoteBuy`, `quoteSell` and `currentPrice` read all three at call time, per coin. A local copy with the rates or the set `0` constants baked in is right only for coins stamped `0`. For any other coin it is wrong, and by a lot: the error scales with the ratio between the two virtual ETH values. ## Write surface [#write-surface] | Function | Notes | | :--------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `launch(string name, string symbol, bytes32 metadataHash, uint256 salt, uint256 pveEth, address feeRecipient) payable returns (address)` | Direct launch with an optional atomic dev buy. `pveEth` is the slice of `msg.value` donated to the PvE escrow. No `minTokensOut` | | `buyWithIntent(LaunchIntent intent, bytes signature, uint256 minTokensOut) payable returns (address)` | The gasless-launch path. Anyone submits an open intent, but only at zero value, which materializes without buying. A funded submission must name its `submitter`, else `ValueNeedsSubmitter`. When `intent.submitter` is set, only that address may submit | | `buy(address token, uint256 minTokensOut) payable returns (uint256)` | Ordinary buy | | `sell(address token, uint256 tokensIn, uint256 minEthOut) returns (uint256)` | Accepts a `Frozen` coin once `SELL_REOPEN_DELAY` has passed, and such a sell returns it to `Trading` | | `buyPve(address token, uint256 minTokensOut) payable returns (uint256)` | Buy at the going rate and donate every coin to the PvE escrow | | `graduate(address token)` | Permissionless, signerless, bounty-paid, and not pausable by anyone including the owner. Requires `Status.Frozen` | | `pokePriceCheckpoint()` | Permissionless. Advances the MOTO/WETH reference. Buys self-call it opportunistically | | `claimBounty(address to) returns (uint256)` | Withdraw a graduation bounty whose push failed | | `recordPveLate(address token)` | Owner or `pveKeeper` only. Retries a PvE credit that failed at graduation | `buy`, `sell`, `buyPve` and `launch` take no deadline, by design: there is no LP-side staleness, and `minTokensOut` / `minEthOut` are the whole price guard. The only deadline on the user path is `intent.deadline` inside `buyWithIntent`. Do not look for a deadline argument to pass. The buy slippage check is price-equivalent rather than a raw amount, so a floor-crossing partial fill that refunds the remainder still satisfies a `minTokensOut` set for the full amount. On a full fill it reduces to `tokensOut >= minTokensOut`. ## Read surface: `LaunchpadLens` [#read-surface-launchpadlens] Every function is `view` and nothing in the contract can move a wei. | Function | Returns | | :-------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `quoteBuy(address token, uint256 ethIn)` | `(uint256 tokensOut, uint256 grossUsed, uint256 refund)` | | `quoteLaunch(uint256 ethIn, uint256 pveEth)` | `(uint256 devTokens, uint256 pveTokens, uint256 grossUsed, uint256 refund)` | | `quoteSell(address token, uint256 tokensIn)` | `uint256 ethOut`, net of fees | | `currentPrice(address token)` | ETH per whole coin, 1e18 fixed point. **Does not check status.** For a `Graduated` coin it returns the coin's graduation price, frozen: its price when the reserve reached the floor. The live price is the pair's. For an address the curve never launched it returns a non-zero number too, so read `curves(token).status` first | | `bondingProgress(address token)` | `0` to `1e18` | | `predictToken(address creator, uint256 salt)` | The CREATE2 address `launch` will deploy | | `graduationReadiness(address token)` | `(Readiness regime, uint64 readyAt, uint256 minMotoOut, uint256 quotedOut, uint256 deviationBps, uint256 bountyNow, uint256 sellsOpenAt)` | `Readiness` is `{ Ready, TooFresh, Stale, Missing, OutOfBand, Upstream, NotFrozen }`. It never reverts: every cross-domain read is guarded and every failure classified, so a keeper can branch on the regime instead of on a revert string. `quoteLaunch` is not optional for a create form. `quoteBuy` answers a different question and overstates the creator's take by the PvE ratio on a split launch. `LaunchpadLens.motoPoolDeviationBps` is kept only for a dual-ABI migration window and is marked stale in the source. Do not build on it, and do not read its `usable` flag as "graduation will succeed". `graduationReadiness` is the replacement. ## How a coin trades before graduation [#how-a-coin-trades-before-graduation] Against the curve's own ETH, and nowhere else. * There is no pair and no LP while a coin is bonding. Every buy sends ETH to the curve and every sell is paid out of the ETH the curve holds for that coin (`realEth`). The curve holds the whole unsold supply. * The price is a function of that coin's `realEth`, its `tokenReserve`, and its own curve parameters. Nothing outside the curve contract can move it. * A pool cannot be opened early on the venues the curve covers. At launch the curve derives the coin's future pair addresses on the Motoswap, Uniswap V2 and Sushi factories, against WETH, MOTO and each entry of the owner's `blockedQuoteAssets` list, and writes them into the coin. Until the coin is bonded, a transfer to any of those addresses reverts `BlockedUntilBonded`. An external venue that is not configured on a given deployment is skipped. * That is a list of addresses, not a general rule. See [The token template](#the-token-template) for exactly what it does not cover. A router or aggregator should therefore treat a bonding coin as having one venue, the curve, and quote it through `LaunchpadLens`. Do not route it through a pair, and do not synthesize a pair address for it. ## What graduation does [#what-graduation-does] `graduate(token)` is one transaction, callable by anyone, and it either opens both pools or does nothing at all. 1. The coin must be `Frozen`, which happens when a buy takes `tokenReserve` down to the coin's `floorSupply`. Otherwise `NotFrozen`. 2. The pot is the coin's `realEth`. The caller's bounty comes off first: `graduationBounty`, capped at one fiftieth of the pot. The rest is split in half. One half is the WETH side of TOKEN/WETH. The other half buys MOTO through the `FeeRouter` and becomes the MOTO side of TOKEN/MOTO. 3. Both pools get their token side at the curve's closing spot price, `(virtualEth + pot) / (floorSupply + virtualTokens)`. That is what "seeded at the final curve price" means: the first pool price equals the last curve price. 4. The MOTO buy happens **before** the coin is bonded, so the pre-bond blocklist still covers the TOKEN/MOTO address right up to the moment the curve seeds it. Then `setBonded()` clears the blocklist for good. 5. Each pair is looked up with `getPair` and created only if it does not exist. If a pair already exists with reserves, the curve matches that pair's live ratio rather than minting blind. 6. **Both LP positions are minted to `0x000000000000000000000000000000000000dEaD`.** Nothing is kept by the curve, the caller or the protocol, and there is no lock contract or unlock date. 7. Coins left over after both pools are seeded are sent to the same burn address and reported as `dustBurned`. Leftover MOTO or WETH goes to the Collector. 8. The PvE escrow, if the coin has one, is credited to Motocat stakers in the same transaction. If the vault cannot take the credit, graduation still completes, `PveRecordFailed` is emitted, and the credit is retried later through `recordPveLate`. A holder's exit never depends on the vault. **Two pools or nothing.** If the TOKEN/MOTO leg comes out too small to mint, the whole transaction reverts `MotoLegTooSmall()`. There is no one-pool graduation and `Graduated` is never emitted with a zero `pairMoto`. The other reverts on this path are just as clean: `MotoOutBelowFloor(got, need)` when the MOTO buy fills below its price floor, `MotoPoolUnreadable()` when the MOTO/WETH pool or a fee read cannot be used, and `MotoRefMissing`, `MotoRefTooFresh(readyAt)` or `MotoRefStale` when the price reference is not usable yet. In every case nothing is spent, nothing is owed, the coin stays `Frozen`, and the next attempt can succeed. Holders are not locked in while that happens: `sell` reopens `SELL_REOPEN_DELAY` after the freeze. A keeper should call `LaunchpadLens.graduationReadiness(token)` first. It classifies all of the above without reverting. ## Events [#events] The full catalog with indexed fields is in the [inventory](/dev/reference/fun-contracts-full-inventory). The five an indexer needs: ```solidity event TokenCreated(address indexed token, address indexed creator, string name, string symbol, bytes32 metadataHash, address indexed quoteToken); event Trade( address indexed trader, address indexed token, bool isBuy, uint256 ethAmount, // curve-leg gross ETH: fee-inclusive on buys, pre-fee on sells uint256 tokenAmount, uint256 priceX18, // spot after the trade, 1e18 uint256 protocolFee, // the literal WETH transfer to the Collector uint256 creatorFee // the literal WETH deposit into the vault ); event FloorReached(address indexed token, uint256 realEth); event Graduated( address indexed token, address pairWeth, address pairMoto, uint256 wethLp, uint256 motoLp, uint256 tokensLpWeth, uint256 tokensLpMoto, uint256 dustBurned, address indexed keeper ); event PveFill(address indexed token, address indexed contributor, uint256 tokens); ``` Note what is **not** indexed. `Trade.isBuy` is a data field, so you cannot topic-filter buys from sells. `Graduated` indexes `token` and `keeper` but not the pairs, so pair discovery means decoding the data. Topic hashes, so you can filter without compiling anything. Each is `keccak256` of the canonical signature shown: | Event | Canonical signature | `topic0` | | :------------- | :----------------------------------------------------------------------------------- | :------------------------------------------------------------------- | | `TokenCreated` | `TokenCreated(address,address,string,string,bytes32,address)` | `0x6223a619ba904a9914dac4461d25f8f27e372c0a030791c3a5c012768765ef45` | | `Trade` | `Trade(address,address,bool,uint256,uint256,uint256,uint256,uint256)` | `0x2c76e7a47fd53e2854856ac3f0a5f3ee40d15cfaa82266357ea9779c486ab9c3` | | `FloorReached` | `FloorReached(address,uint256)` | `0xc1d2a70654f792a9ba0353d25aaa2722a5f588f85d4560d21324f4e2d3d730e6` | | `Graduated` | `Graduated(address,address,address,uint256,uint256,uint256,uint256,uint256,address)` | `0x79d3d3525cbe6d1919438aa800976255676f9242b78cb45587085d4a63a9ba31` | `TokenCreated` has four topics: the hash, `token`, `creator`, `quoteToken`. `name`, `symbol` and `metadataHash` are in the data. `Graduated` has three topics: the hash, `token`, `keeper`. Its data is seven 32-byte words in declaration order, so `pairWeth` is word 0 and `pairMoto` is word 1. ### Which asset a coin is quoted in [#which-asset-a-coin-is-quoted-in] On the curve, always ETH. Buys are paid in ETH, sells pay out ETH, `Trade.ethAmount` and both fee fields are wei, and `Trade.priceX18` is ETH per whole coin. There is no per-coin quote asset to look up while a coin is bonding, and `TokenCreated` names it: `quoteToken` is the curve's WETH address on every launch. After graduation a coin has two pools, TOKEN/WETH and TOKEN/MOTO, and both addresses are in `Graduated`. Also worth catching: `FeeRecipientSet` (fires only when the recipient differs from the creator), `CreatorFeeRoutedToCollector` (the creator leg failed over to the protocol), `PveRecorded` / `PveRecordFailed`, `BountyOwed` / `BountyClaimed`, and `LaunchParamsSet` (new coins will be priced by a new parameter set from this block on). ## The token template [#the-token-template] Fixed 1e27 supply, 18 decimals, no owner, no mint, no tax, hand-written ERC-20. Two behaviors are active only while unbonded and are cleared permanently by `setBonded()` at graduation: * **A pre-bond transfer blocklist.** At launch the curve derives the coin's pair address on three venues against WETH, MOTO and any blocked quote assets, and writes them into the clone. Transfers to those addresses revert `BlockedUntilBonded`, so nobody can seed a pool before graduation does. * **A narrow curve allowance exemption**, only when `msg.sender == curve && to == curve`, which is the gasless sell pull. The blocklist is a list, not a rule: an unlisted quote asset, a V3 or V4 pool, or an unknown V2 fork sits outside it. Treat pre-bond pool liquidity as possible but unsupported, not as impossible. ## Errors [#errors] Decodable custom errors throughout. The full table is in the [inventory](/dev/reference/fun-contracts-full-inventory). Two to special-case: `claimBounty` reverts `PveNothingToRecord()` when there is no bounty owed. The name does not match the condition, so do not treat that selector as PvE-specific. `MotoRefTooFresh(uint64 readyAt)` carries the timestamp the reference matures at. A keeper should sleep until `readyAt` rather than retrying blind. A launch can also be refused by the creator-fee registration. Each of the three refusals below is a revert, so a coin never launches without its fee registered. The one benign case is not a refusal: if the registry rejects the call because the coin is already registered to this same creator, the launch simply proceeds. | Error | Meaning | | :------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `CreatorAlreadyClaimed()` | The registry already holds a different creator for this coin address. Coin addresses are predictable before launch, so this is treated as a hijack attempt | | `RegistryUnreadable()` | The registry could neither register the coin nor be read back | | `RegistryRefused()` | The registry refused and the coin's slot is still empty, which happens only when the curve is not a registrar on the registry. Nothing to fix on the caller's side: retry once that is restored | `CreatorFeeUnregistered(address indexed token, address indexed creator)` is still declared on the ABI, and nothing on the current implementation emits it. Keep it in your ABI only if you decode logs from before that change. ## Fees, in one place [#fees-in-one-place] | Where | Rate | Paid in | Goes to | | :------------------------ | :---------------------------------------------------------------------------------------------------------------------- | :-------------------------- | :----------------------------------------- | | Curve trade, protocol leg | `protocolFeeBps`, currently `70` (0.70%), capped at `100` | WETH | The Collector, the same one DEX fees reach | | Curve trade, creator leg | `creatorFeeBps`, currently `50` (0.50%), capped at `50` | WETH | `CreatorFeeVault`, credited to the coin | | After graduation | The normal Motoswap 1% swap fee, plus the DEX creator fee, currently 0.30% and read live from `FeeRouter.creatorFeeBps` | The quote asset of the swap | As on any Motoswap pool with a creator fee | Both curve legs come off the ETH side, on buys and sells alike: off the input on a buy, off the output on a sell. All of these rates are live contract state, so read them rather than pinning them. `Trade.protocolFee` and `Trade.creatorFee` are the literal amounts moved, which means a rate change needs no handling in a pipeline that sums them. If the registry reports no active creator for a coin (its fee is disabled), the whole fee goes to the Collector and the trader pays exactly the same rate. If the registry or vault cannot be read at all, the same thing happens and `CreatorFeeRoutedToCollector(token, amount)` is emitted. That event marks an outage, not a configuration. ## Creator fees [#creator-fees] `CreatorFeeRegistry` and `CreatorFeeVault` are shared with the DEX, so a moto.fun coin claims through exactly the same path as any other token with a creator fee. Two reads that must not be conflated: * `activeCreator(token)` is the hot-path read and returns zero for unregistered **or disabled**. * `creatorOf(token)` returns the creator regardless of the disabled flag, and it is what gates `claim`. Disabling a coin stops future accrual without freezing money already earned. `claim(address token, address[] quoteAssets, bool unwrapWeth)` is caller-gated to `registry.creatorOf(token)`. Zero balances are skipped rather than reverting. `claimMany(address[] tokens, address[] quoteAssets, bool unwrapWeth)` does the same across several coins in one transaction, and reverts the whole batch if any listed coin is not the caller's. A creator can move their own fee stream with `CreatorFeeRegistry.setCreator(token, newCreator)`, gated on `creatorOf`. Three consequences worth building around: * **Claim before you rotate.** The vault accrues per coin, not per creator, and `claim` pays whoever `creatorOf(token)` is at the moment of the claim. Everything unclaimed moves to the new creator with the rotation. If the old address is meant to keep what it earned, it has to claim first. * **It works on a disabled coin too.** `setCreator` checks only that the caller is the current creator. It is not admin-only, and a disabled fee does not freeze the recipient. * **One rotation emits two events.** The self path fires `CreatorSet` and `CreatorSetBySelf` together; the owner path (`adminSetCreator`) fires `CreatorSet` alone. Treat `CreatorSet` as the record of the change and `CreatorSetBySelf` as the label on it. Counting both double-counts every self-redirect. A coin address can be registered once, ever. There is no unregister and no re-register. After graduation the creator fee is an extra charge on the quote leg of a swap, on top of the protocol fee, not a share taken out of it. A swap between two coins that both have an active creator pays it twice, once to each creator. ### Who registered a coin [#who-registered-a-coin] The registry records it on chain, in an event whose three fields are all indexed: ```solidity event TokenRegistered(address indexed token, address indexed creator, address indexed registrar); ``` The registry's address is `addresses.creatorFeeRegistry` in `/config`, and the curve will also tell you on chain: `LaunchpadCurve.registry()` returns the registry and `vault()` the fee vault. `registrar` is the address that made the registration. For a moto.fun coin it is the curve, because the curve registers every coin inside the launch. Filter `TokenRegistered` on the registry by `token` to read it for one coin, or by `registrar` equal to the curve address to list every coin the curve registered. `creator` here is the fee recipient the launch named, which is not always the signer: compare it with `LaunchpadCurve.creatorOf(token)` when you need the signer. The app backend's `GET /tokens/{address}` serves this too: `registeredBy` (the registering address, or `null` for rows indexed before the field existed) and `selfRegistered` (`true` only when the registrar is the coin itself). The event above stays the canonical source. A coin can register its own address only while the registry's open registration is on (`OpenRegistrationSet`), so a `registrar` equal to the coin marks that path. ## Where to go next [#where-to-go-next] * [Full contracts inventory](/dev/reference/fun-contracts-full-inventory) for every signature. * [Addresses](/dev/fun/addresses) for how to resolve the curve and lens at runtime. * [Trading bots](/dev/integrations/trading-bots#motofun-curve-trades) for the buy and sell loop. * [Scanners](/dev/integrations/scanners#motofun-curve-events) for the indexing rules. # moto.fun overview (/dev/fun) moto.fun is Motoswap's bonding-curve launch surface. Every coin on every curve lives in **one contract**: there is no per-coin curve, no per-coin pool, and no factory to enumerate. A coin is an EIP-1167 clone of a fixed ERC-20 template, and the curve holds its entire supply until it graduates. Contracts on this page: `LaunchpadCurve`, `LaunchpadToken`, `LaunchpadLens`, `CreatorFeeRegistry`, `CreatorFeeVault`. Every function, event, error and constant: [moto.fun contracts inventory](/dev/reference/fun-contracts-full-inventory). ## The mental model in six facts [#the-mental-model-in-six-facts] 1. **One curve contract holds every coin.** State is `curves(address token)`, keyed by the coin. Discovery is `TokenCreated` logs on that one address, not a `PairCreated` sweep. 2. **A coin is a clone at a CREATE2 address derived from `(creator, salt)`.** The address is known before the coin exists, which is why a coin can be listed as pending with no code at it yet. 3. **Trading is ETH in, ETH out, against virtual reserves.** No LP, no pair, no route, no `getAmountsOut`. The virtual reserves are per coin, fixed at launch, and read with `paramsOf(token)`. Quote through `LaunchpadLens`, which does that for you. 4. **Graduation is the state transition that matters.** `Status` goes `Trading → Frozen → Graduated`, and only at `Graduated` do real pools exist. It opens TOKEN/WETH and TOKEN/MOTO together at the final curve price and burns both LP positions, or it reverts and changes nothing. 5. **Curve trades are DEX trades for fees, Points and Rakeback.** The 0.70% protocol fee goes to the same Collector, in WETH, at the same rate as `FeeRouter`. 6. **Nothing about a coin is mutable.** Fixed supply, no owner, no tax, no mint, metadata committed by hash at launch, curve parameters stamped at launch. The curve contract itself sits behind a proxy with a timelocked upgrade path, which is a separate question from the coin. ## The pieces [#the-pieces] | Piece | What it is | Where | | :--------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------ | | `LaunchpadCurve` | The singleton. Launch, buy, sell, graduate, PvE escrow, fee routing. A UUPS proxy: upgrades go through a timelock, day-to-day setters through the owner | [Contracts](/dev/fun/contracts) | | `LaunchpadToken` | The ERC-20 template. One clone per coin, 1e27 fixed supply | [Contracts](/dev/fun/contracts) | | `LaunchpadLens` | Read-only companion. Quotes, price, progress, graduation readiness | [Contracts](/dev/fun/contracts) | | `CreatorFeeRegistry` / `CreatorFeeVault` | Shared with the DEX. Who the creator is, and the claim escrow | [Contracts](/dev/fun/contracts) | | Motoswap API `/motofun/*` | The documented public read surface. ETag-cached, in `/openapi.json` | [API](/dev/fun/api) | | The moto.fun app backend | The app's own service. Candles, holders, intents, SSE | [API](/dev/fun/api) | ## Which API to build against [#which-api-to-build-against] There are two, and they are not the same service. `https://api.motoswap.org/motofun/*` is the surface to integrate against. It is in `/openapi.json`, ETag-cached, rate-limited generously, and versioned on the curve's own head block. Use it for discovery, listings, venue stats and revenue. The moto.fun app's own backend serves what the app needs and the Motoswap API does not carry: candles, holder tables, pending launch intents, per-wallet positions, a graduated-coins feed, and a Server-Sent Events stream. It serves ETags. Only the board and the graduated feed are paginated. Reach it at the moto.fun origin, which `GET https://api.motoswap.org/config` gives you as `motofunUrl`, under `/backend-fun//`. Both are documented on the [API page](/dev/fun/api). ## Ordering, in one sequence [#ordering-in-one-sequence] ``` sign LaunchIntent ──► POST /intents (coin is pending, address known, no code) │ first buy ────────────────┴──► buyWithIntent(intent, sig, minOut) or launch(...) payable │ ├─► TokenCreated coin exists └─► Trade every buy and sell │ floor supply reached ─┴──► FloorReached Status = Frozen, trading stops │ graduate(token) (permissionless, bounty-paid) │ └─► Graduated two pools or a revert, LP burned to 0xdead ``` A creator taking a dev buy skips the intent path entirely and calls `launch` directly, which deploys and buys in one transaction. ## What is deliberately absent [#what-is-deliberately-absent] No ticker exclusivity or reservation. No deadline parameter on curve trades. No max buy, no per-wallet cap, no cooldown, no launch block delay. No LP lock, because LP is burned rather than locked. No sweep or withdraw function on the curve. Building around any of these is building around something that does not exist. ## Where to go next [#where-to-go-next] * [Contracts](/dev/fun/contracts) for the call surface and the event catalog. * [Addresses](/dev/fun/addresses) for discovery, and why nothing here is hardcoded. * [API](/dev/fun/api) for both HTTP surfaces. * [Realtime](/dev/fun/realtime) for the SSE stream and the candle rules. * [Aggregators](/dev/integrations/aggregators#motofun-coins) for listing a curve coin. # Realtime and candles (/dev/fun/realtime) There is no WebSocket. Realtime is Server-Sent Events on the moto.fun app backend, with polling as the supported fallback, and the Motoswap API's ETags as the cheap way to poll. ## The stream [#the-stream] ``` GET /backend-fun//stream ``` A firehose. There is no subscription protocol, no query parameter, and no way to filter to one coin or one event type: you receive every event for every coin on that chain and filter locally. Each message is a named SSE event whose `data` is the whole payload: ``` event: trade data: {"type":"trade","token":"0x…"} ``` Every event carries exactly `{ type, token }`. No amounts, no prices, no block numbers, no trader. The stream tells you *what changed*, and you re-fetch the REST queries it touched. Any pipeline that tries to reconstruct state from the stream alone will be wrong. | `type` | Fires when | | :---------------------------------------- | :-------------------------------------------------------------------- | | `created` | `TokenCreated` was indexed | | `trade` | Any `Trade` was indexed | | `frozen` | `FloorReached` was indexed | | `unfrozen` | A sell past the reopen delay walked a `Frozen` coin back to `Trading` | | `graduated` | `Graduated` was indexed | | `moto_leg_completed`, `moto_leg_deferred` | Retired. Nothing on the current curve emits them | `token` is always lowercased at publish time. ### Connection rules [#connection-rules] * A `ping` event is written on connect and every 25 seconds. Treat a much longer gap as a dead connection. * 2000 concurrent streams globally and 40 per IP by default, both configurable per deployment. Over the global cap you get `503`, over the per-IP cap `429`, each with a `Retry-After` header. * **There is no `id:` field, no `Last-Event-ID` handling, and no replay buffer.** A reconnect resumes blind with a gap you cannot recover from the stream. On reconnect, re-fetch what you care about, then resume listening. Standard `EventSource` auto-reconnect works, it just does not backfill. * The bus is in-process. One process, one stream. ### Polling instead [#polling-instead] Nothing requires SSE. The indexer polls every 5 seconds and `/tokens` is cached for 5 seconds, so polling that service faster than about 5 seconds buys nothing. On the Motoswap API, poll `/motofun/*` with `If-None-Match`: those ETags version on the curve's own head block, so a quiet curve returns `304` through every DEX block. ## Candles [#candles] ``` GET /backend-fun//tokens/{address}/candles?resolution=&from= ``` | Parameter | Type | Default | Bounds | | :----------- | :-------------- | :------------ | :------------------------------------------------------------- | | `resolution` | integer seconds | `300` | Clamped to `[60, 86400]`. A continuous range, not an enum | | `from` | unix seconds | `now - 86400` | Raised to the window floor, then aligned down to a bucket edge | The response is a **bare JSON array**, not an envelope: ```json [ { "bucket": 1755993600, "low": 1300000000, "high": 1400000000, "trades": 6, "volume": "410000000000000000", "open": "1310000000", "close": "1395000000" } ] ``` ### The rules, exactly [#the-rules-exactly] 1. **Buckets are UTC epoch-aligned**: `floor(timestamp / resolution) * resolution`, no offset. 2. **The window is 2000 buckets, anchored to the coin's last trade, not to wall clock.** A dormant coin therefore serves the tail of its own trading life at every resolution instead of an empty set. A `from` earlier than that floor is raised to it; a `from` later than it is honoured as given. 3. **One seed row is prepended**: the newest bucket strictly before the window, so a client's fill-forward has a bar to bridge from. A response can hold 2001 rows. 4. **Empty buckets produce no row at all.** There is no server-side fill-forward, no synthetic OHLC, and no zero-volume bar. The array is sparse. 5. **`low` and `high` are JSON numbers. `open` and `close` are decimal strings.** The asymmetry is real, and `open` and `close` are optional: treat them as possibly absent. 6. **`volume` is an exact decimal wei string** and sums buys and sells together. There is no side filter. `trades` is a count. 7. **A coin with no trades returns `[]` with status `200`.** Never a `404`, never an error, and there is no `nextTime` or `noData` envelope. This is not a TradingView UDF datafeed. The API is sparse by design and the app fills forward on the client. If you are drawing a chart you must synthesize the flat bars yourself, or your gaps will render as holes rather than as quiet markets. ### What the app does, if you want to match it [#what-the-app-does-if-you-want-to-match-it] The app's chart carries 1m, 5m, 10m, 30m, 1h and 6h, mounts at 1 minute, and applies these rules on top of the sparse response: * **Fill forward to the current bucket.** Every empty bucket becomes a flat, zero-volume candle whose open, high, low and close all equal the previous close. Every real bar opens at the previous close. Capped at 10,000 filled buckets. * **Prices convert from the `1e18` fixed-point values directly.** A naive wei-to-float helper with a 1e-6 floor renders a live curve price as zero; curve prices routinely sit near 1e-9 ETH. * **After graduation the series joins the curve history to the pool history** at the graduation bucket, so the chart does not restart. The bucket at the seam keeps its own open rather than inheriting the previous close. ### Graduated coins move venue [#graduated-coins-move-venue] Once a coin has graduated, its candles come from the DEX side, not the curve. The curve series is frozen history. Use `/motofun/coins/{address}` to learn the status and the pair addresses, then read the pool chart from the Motoswap API like any other pool. ## Where to go next [#where-to-go-next] * [API](/dev/fun/api) for both HTTP surfaces. * [Scanners](/dev/integrations/scanners#motofun-curve-events) to index the logs yourself instead. * [Full API inventory](/dev/reference/fun-api-full-inventory) for exact shapes. # Aggregators (/dev/integrations/aggregators) Motoswap is Uniswap V2 in its math and its interfaces. It is not integrable with a stock V2 adapter, because the swap leg is gated. This page is what an adapter needs: why the stock path reverts, the `FeeRouter` interface, how to quote it, the fee model in basis points, and how to find pairs. Every contract claim here was checked against the Solidity source, and signatures are copied from it. ## Deployment status: read it from `/config` [#deployment-status-read-it-from-config] Status is per contract, and `GET https://api.motoswap.org/config` is where you read it. The per-contract table with addresses is on [addresses](/dev/addresses). The DEX contracts and the moto.fun curve are deployed, and `launchPhase` reads `"dex"`. It read `"vamp"` before the DEX opened, when the MOTO token and the launch farm were deployed and the DEX contracts were not. Read the pair, router and `FeeRouter` addresses from `/config`. Do not integrate against a Motoswap pair, router or `FeeRouter` address from any source but `/config`, and treat a zero address there as unset for the current phase rather than as a target. `GET /dex/pools` and `GET /swap/candidates` can return Uniswap V2 pairs alongside Motoswap pools: those trade through the Uniswap V2 router. Confirm any pool you route through with `pair.factory() == cfg.addresses.factory`. ```ts const cfg = await fetch("https://api.motoswap.org/config").then((r) => r.json()); const dexLive = cfg.launchPhase === "dex" && cfg.addresses.feeRouter !== "0x0000000000000000000000000000000000000000"; ``` * **The check.** `GET /config` returns `launchPhase` equal to `"dex"` AND `addresses.feeRouter` is not the zero address. That is `dexLive` above. Run it on every start, and stop if either side fails. * **How often to poll.** `/config` answers with `Cache-Control: public, max-age=300, s-maxage=300, stale-while-revalidate=900` and no `ETag`, so there is no `304` to ask for. The edge holds an answer for 300 seconds and may serve a stale one for up to 900 seconds more while it refreshes. Polling more than once every 5 minutes returns the same bytes. * **Read, do not hardcode.** The `FeeRouter` is an upgradeable proxy (UUPS, owner-gated), and the address `/config` serves is the proxy. Read `router()`, `protocolFeeBps()` and `creatorFeeBps()` live on every quote, as the [quote recipe](#quote) does. Do not copy the quote ranks either: read `quoteRank(token)` from `registry()`, or let `feeOnOutput(path)` apply them. `upgradesRenounced()` tells you whether upgrades are still possible. * **Split fills.** `/config.splitSwapEnabled` is a Motoswap app setting, not an on-chain switch. See [the note under the interface](#the-feerouter-interface). * **No public test deployment.** There is no public test deployment of the DEX contracts. Build against the interfaces and the quote recipe on this page, then verify against mainnet. ## Why a stock V2 adapter reverts [#why-a-stock-v2-adapter-reverts] A V2 adapter transfers tokens to the pair and calls `pair.swap` from its own executor, or calls the V2 router. Both revert on Motoswap. | Surface | Access | | :----------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | | `MotoSwapPair.swap` | Requires `factory.swapAllowed(msg.sender)`. Reverts `MotoSwap: SWAPPER_NOT_ALLOWED` otherwise | | Every swap entrypoint on `MotoSwapRouter02` (the core router) | Same requirement, `onlyPerimeter`. Reverts `MotoSwapRouter: SWAPPER_NOT_ALLOWED` | | Flash swaps | Closed for every caller. `pair.swap` requires empty `data` and reverts `MotoSwap: FLASH_SWAPS_CLOSED` | | `FeeRouter.swapExact*` | Public | | Core router liquidity functions, `getAmountsOut`, `getAmountsIn`, `quote` | Public | | Pair reads: `getReserves`, `token0`, `token1`, `factory`, `kLast`, cumulative prices | Public | The perimeter holds exactly two addresses: the core router and the `FeeRouter`. The deployment seeds those two, and the deployment check asserts that both are admitted and that the protocol's own zap is not. The core router is in the set only because the `FeeRouter` reaches pairs through it. So the integration path for every aggregator is the same: your route's Motoswap leg is a `FeeRouter` call, and it pays the full fee. Price the fee into the venue's effective rate. ## The fee model in basis points [#the-fee-model-in-basis-points] Three fees exist. One sits in the pair math. Two are skimmed by the `FeeRouter` from the quote leg of the trade. | Fee | Live read | Value before configuration | Value at launch | Hard cap | | :----------------------------------- | :--------------------------- | :----------------------------------------- | :-------------- | :------- | | Pair fee, per hop | `factory.swapFeeBps()` | `initialize` sets 100 | 30 | 200 | | Protocol fee, once per trade | `feeRouter.protocolFeeBps()` | No default. It is an `initialize` argument | 70 | 100 | | Creator fee, per registered endpoint | `feeRouter.creatorFeeBps()` | 0 until configured | 30 | 50 | Read all three from the chain, and keep reading them: each one has an owner-gated setter, and the pair reads `swapFeeBps` live on every swap. Hardcoding `997/1000` is a latent mispricing. Where each fee goes: * **Pair fee.** Stays in the pool. The launch configuration sets `feeTo` to the zero address, and with `feeTo` unset `_mintFee` does nothing, so the whole pair fee accrues to LPs. * **Protocol fee.** Transferred to the `Collector` in the quote asset. It can be set to 0. The `FeeRouter` still emits `Swap`, with `feeAmount == 0`. * **Creator fee.** Charged only when a path endpoint has an active creator in the `CreatorFeeRegistry` (`activeCreator(token) != address(0)`), which moto.fun registers for its coins at launch. It is taken from the same quote leg and the same base as the protocol fee, and sent to the `CreatorFeeVault`. It is not part of `Swap.feeAmount`. It has its own `CreatorFee` event. How they combine, at the launch values, for an exact-input swap: | Route | Pair fee | Quote-leg skim | Multiplier, before price impact | All-in | | :--------------------------------------------------------------------- | :--------- | :-------------------------------------------- | :------------------------------------------- | :----- | | One hop, quote asset and an unregistered token | 30 | 70 | `(1 - 0.0070) * (1 - 0.0030)` | 0.998% | | One hop, quote asset and a registered token | 30 | 70 + 30 | `(1 - 0.0100) * (1 - 0.0030)` | 1.297% | | Two hops, a quote asset at one end, an unregistered token at the other | 30 per hop | 70, once | `(1 - 0.0070) * (1 - 0.0030)^2` | 1.295% | | Two hops, a quote asset at one end, a registered token at the other | 30 per hop | 70 + 30, once | `(1 - 0.0100) * (1 - 0.0030)^2` | 1.593% | | Two hops, token to quote asset to token, one token registered | 30 per hop | 70 + 30, once, on the interior quote leg | `(1 - 0.0030) * (1 - 0.0100) * (1 - 0.0030)` | 1.593% | | Two hops, token to quote asset to token, both tokens registered | 30 per hop | 70 + 30 + 30, once, on the interior quote leg | `(1 - 0.0030) * (1 - 0.0130) * (1 - 0.0030)` | 1.891% | All-in is one minus the multiplier. On an interior-quote route the skim comes off the first hop's output, which is why it sits between the two pair fees. The protocol fee is charged once per trade, never per hop. The creator fee is charged once per endpoint with an active creator, so a route between two registered tokens pays it twice. Each skim is computed on the same base and rounded down on its own: ``` protocolFee = base * protocolFeeBps / 10_000 creatorCut = base * creatorFeeBps / 10_000 // per registered endpoint remainder = base - protocolFee - creatorCut * registeredEndpoints ``` ## Which leg pays [#which-leg-pays] The quote assets are the tokens in the `QuoteAssetRegistry` (`feeRouter.registry()`, then `quoteAssets()`, `isQuote(token)`, `quoteRank(token)`). The launch configuration registers WETH, USDT, USDC and MOTO with ranks 4, 3, 2 and 1. The set and the ranks are owner-settable, so read them. | Path endpoints | Base the fees come off | | :---------------------------------- | :---------------------------------------------------------------------------------------------------------------------------- | | Only the output is a quote asset | Gross output. Pools see the full input | | Only the input is a quote asset | Input. Pools see the remainder | | Both are quote assets | The side with the higher `quoteRank`. A tie goes to the output | | Neither is a quote asset | The output of the hops up to the first interior quote asset in the path. The remainder continues through the rest of the path | | No quote asset anywhere in the path | Reverts `NoQuoteSide()` | `feeOnOutput(address[] path)` is a public view that returns `true` when the fees come off the output leg. It looks at `path[0]` and the last element only. For a path with no quote asset at either end it returns `true`, but the swap does not take the output branch: it takes the interior-quote branch above. Check `isQuote` on both endpoints first, then call `feeOnOutput`. The ETH entrypoints require a quote asset at one end, which WETH always is while it is registered. ## Quote [#quote] `FeeRouter` has no quote view. It is exact-input only, so there is no `getAmountsIn` equivalent to build either. The recipe: 1. Read `protocolFeeBps`, `creatorFeeBps`, `creatorRegistry`, `registry` and `router` from the `FeeRouter`. 2. Count the distinct path endpoints with `activeCreator(token) != 0`. 3. Pick the fee leg from the table above. 4. Call the core router's `getAmountsOut`, which is public and reads the live `swapFeeBps`, and apply the skims on the correct side of it. Run every read at one block number. ```ts import { parseAbi, zeroAddress } from "viem"; const feeRouterAbi = parseAbi([ "function router() view returns (address)", "function registry() view returns (address)", "function creatorRegistry() view returns (address)", "function protocolFeeBps() view returns (uint256)", "function creatorFeeBps() view returns (uint256)", "function feeOnOutput(address[] path) view returns (bool)", ]); const routerAbi = parseAbi([ "function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[])", ]); const registryAbi = parseAbi(["function isQuote(address token) view returns (bool)"]); const creatorAbi = parseAbi(["function activeCreator(address token) view returns (address)"]); // Net amount the recipient gets for an exact-input swap through FeeRouter. export async function quoteExactIn(client, feeRouter, amountIn, path, blockNumber) { const at = { blockNumber }; const fr = (functionName, args) => client.readContract({ address: feeRouter, abi: feeRouterAbi, functionName, args, ...at }); const [router, registry, creatorRegistry, protocolFeeBps, creatorFeeBps] = await Promise.all([ fr("router"), fr("registry"), fr("creatorRegistry"), fr("protocolFeeBps"), fr("creatorFeeBps"), ]); const amountsOut = async (amount, p) => (await client.readContract({ address: router, abi: routerAbi, functionName: "getAmountsOut", args: [amount, p], ...at, })).at(-1); const isQuote = (token) => client.readContract({ address: registry, abi: registryAbi, functionName: "isQuote", args: [token], ...at }); // Creator fee: once per distinct endpoint that has an active creator. const tokenIn = path[0]; const tokenOut = path.at(-1); let creators = 0n; if (creatorRegistry !== zeroAddress && creatorFeeBps > 0n) { const endpoints = tokenIn.toLowerCase() === tokenOut.toLowerCase() ? [tokenIn] : [tokenIn, tokenOut]; for (const token of endpoints) { const creator = await client.readContract({ address: creatorRegistry, abi: creatorAbi, functionName: "activeCreator", args: [token], ...at, }); if (creator !== zeroAddress) creators += 1n; } } // Both fees are computed on the same base, each rounded down on its own. const afterFees = (base) => base - (base * protocolFeeBps) / 10_000n - creators * ((base * creatorFeeBps) / 10_000n); const [inQuote, outQuote] = await Promise.all([isQuote(tokenIn), isQuote(tokenOut)]); if (!inQuote && !outQuote) { // Neither endpoint is a quote asset: the fee comes off the first interior quote leg. let j = 1; while (j < path.length - 1 && !(await isQuote(path[j]))) j++; if (j >= path.length - 1) throw new Error("NoQuoteSide: no quote asset anywhere in the path"); const gross = await amountsOut(amountIn, path.slice(0, j + 1)); return amountsOut(afterFees(gross), path.slice(j)); } if (await fr("feeOnOutput", [path])) { // Pools see the full input. Fees come off the gross output. return afterFees(await amountsOut(amountIn, path)); } // Fees come off the input. Pools see the remainder. return amountsOut(afterFees(amountIn), path); } ``` Call it with a viem public client and `cfg.addresses.feeRouter` once `dexLive` is true. The function was tested with the `FeeRouter` reads stubbed and `getAmountsOut` served by Uniswap V2 reserves on Ethereum, against a separate calculation of each branch. If you would rather price from reserves you index yourself, the pair math is V2 with the fee as a parameter (`MotoSwapLibrary.getAmountOut`): ```ts function getAmountOut(amountIn, reserveIn, reserveOut, feeBps) { const amountInWithFee = amountIn * (10_000n - feeBps); return (amountInWithFee * reserveOut) / (reserveIn * 10_000n + amountInWithFee); } ``` With `feeBps = 30n` this returns the same integer as Uniswap V2's `getAmountOut` for every input. The common mistake: quoting the raw `getAmountsOut` output and using it as `amountOutMin` on a fee-on-output route. The skim happens after the pools fill, the delivery comes up short of the minimum, and the swap reverts every time. ## The FeeRouter interface [#the-feerouter-interface] Exact input only. There is no `swapTokensForExactTokens` and no other exact-output entrypoint on the `FeeRouter`. ```solidity // Plain ERC-20 paths. The caller approves FeeRouter for path[0]. function swapExactTokensForTokens( uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline ) external returns (uint256[] memory amounts); function swapExactETHForTokens( uint256 amountOutMin, address[] calldata path, address to, uint256 deadline ) external payable returns (uint256[] memory amounts); function swapExactTokensForETH( uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline ) external returns (uint256[] memory amounts); // Same swaps, input pulled with a Permit2 SignatureTransfer instead of an approval. // PermitTransferFrom is ((address token, uint256 amount) permitted, uint256 nonce, uint256 deadline). function swapExactTokensForTokensPermit2( IPermit2.PermitTransferFrom calldata permit, bytes calldata signature, uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline ) external returns (uint256[] memory amounts); function swapExactTokensForETHPermit2( IPermit2.PermitTransferFrom calldata permit, bytes calldata signature, uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline ) external returns (uint256[] memory amounts); // Fee-on-transfer tokens. Each returns one number, not an array. function swapExactTokensForTokensSupportingFeeOnTransferTokens( uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline ) external returns (uint256 delivered); function swapExactETHForTokensSupportingFeeOnTransferTokens( uint256 amountOutMin, address[] calldata path, address to, uint256 deadline ) external payable returns (uint256 delivered); function swapExactTokensForETHSupportingFeeOnTransferTokens( uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline ) external returns (uint256 net); // Split fills. struct SwapLeg { uint256 amountIn; address[] path; } function swapExactTokensForTokensSplit( SwapLeg[] calldata legs, uint256 minTotalOut, address to, uint256 deadline ) external returns (uint256[] memory legOuts, uint256 totalOut); function swapExactETHForTokensSplit( SwapLeg[] calldata legs, uint256 minTotalOut, address to, uint256 deadline ) external payable returns (uint256[] memory legOuts, uint256 totalOut); function swapExactTokensForETHSplit( SwapLeg[] calldata legs, uint256 minTotalOut, address to, uint256 deadline ) external returns (uint256[] memory legOuts, uint256 totalOut); ``` The full ABI is in the [contracts inventory](/dev/reference/contracts-full-inventory). `/config.splitSwapEnabled` does not gate the three `Split` entrypoints. It is an API configuration flag that tells the Motoswap app whether to submit a split fill as one `swapExact*Split` transaction or as one ordinary swap per leg. The `FeeRouter` has no on-chain switch for them: in the contract source they are plain `external nonReentrant` functions, so they work whatever the flag says. ### Semantics an adapter depends on [#semantics-an-adapter-depends-on] | Topic | Behavior | | :------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Approval | Token-in entrypoints pull with `transferFrom(msg.sender, feeRouter, amountIn)`. Approve the `FeeRouter`, not the core router and not the pair | | Permit2 | Only the two entrypoints above have a Permit2 twin. `permit.permitted.token` must equal `path[0]`, else `PermitTokenMismatch()`. If Permit2 is not configured they revert `Permit2NotSet()` | | `amountOutMin` | Enforced on the net amount `to` receives, after the protocol fee and any creator fee | | Which revert you get | Fee-on-output routes revert `InsufficientOutput()`. Fee-on-input routes enforce the floor inside the core router and revert the string `MotoSwapRouter: INSUFFICIENT_OUTPUT_AMOUNT` | | `deadline` | Passed through to the core router, which reverts `MotoSwapRouter: EXPIRED` when `deadline < block.timestamp`. The `FeeRouter` has no separate check | | `to` | Receives the net output. On fee-on-input routes the last pair pays `to` directly, so `to` cannot be either token of that pair (`MotoSwap: INVALID_TO`). On ETH-out routes `to` receives native ETH and must accept it | | ETH paths | `swapExactETHForTokens` requires `path[0] == WETH`, the ETH-out entrypoints require the last element to be WETH, else `InvalidPath()`. WETH is `feeRouter.WETH()` | | Returned `amounts` | `amounts[0]` is the caller's gross input and the last element is the net delivery, on every branch. Interior elements do not chain across the fee hop on interior-quote routes. `swapExactTokensForETH` fills only the first and last elements and leaves the interior at zero | | Reentrancy | Every swap entrypoint is `nonReentrant`. The `FeeRouter` holds no balance between calls | | Split fills | 1 to 4 legs (`MAX_SPLIT_LEGS`). Every leg shares `path[0]` and the final token, else `LegMismatch()`. `minTotalOut` floors the sum, there are no per-leg floors. `msg.value` must equal the sum of leg inputs on the ETH-in form, else `ValueMismatch()` | ### Fee-on-transfer tokens [#fee-on-transfer-tokens] The plain entrypoints compute with nominal amounts and revert on a token that taxes its own transfers. The three `SupportingFeeOnTransferTokens` entrypoints measure balance deltas instead: * `amountIn` is what leaves the caller. The swap runs on what arrived at the `FeeRouter`, and that received amount is the `amountIn` in the `Swap` event. * `amountOutMin` is enforced on what `to` actually receives. * The fee model is unchanged: same protocol fee, same creator fee, same leg. * They support the interior-quote route, so a taxing token routes wherever a plain one does. * They have no Permit2 twin. The caller needs an ERC-20 approval to the `FeeRouter`. `getAmountsOut` does not know about a token's tax, so your quote for a taxing token needs the tax applied on your side. ## Events [#events] ```solidity // FeeRouter. One per swap, and one per leg on a split fill. event Swap( address indexed user, // msg.sender of the FeeRouter call. The payer, not `to` address indexed tokenIn, // path[0]. WETH on the ETH-in entrypoints address indexed tokenOut, // last path element. WETH on the ETH-out entrypoints uint256 amountIn, // gross input (msg.value on ETH-in) uint256 amountOut, // net delivered to `to`, after all fees address feeToken, // the quote asset the protocol fee was taken in uint256 feeAmount // the protocol fee only. Creator fees are not included ); // FeeRouter. One per endpoint that has an active creator. event CreatorFee(address indexed token, address indexed creator, address quoteAsset, uint256 amount); ``` `user` is the address that called the `FeeRouter` (`msg.sender`), not the recipient. If your settlement contract makes the call, your settlement contract is the `user` on every swap it routes. Each hop also emits the standard pair `Swap` and `Sync`. The pair-level `sender` is the core router. The pair-level `to` is the next pair, the `FeeRouter` on fee-on-output routes, or the recipient. Reconcile fills against `FeeRouter.Swap.amountOut`, not against the pair legs. ## Pair discovery [#pair-discovery] | Need | How | | :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ | | One pair | `factory.getPair(tokenA, tokenB)`, either order | | All pairs | `factory.allPairsLength()` and `factory.allPairs(i)` | | New pairs | `event PairCreated(address indexed token0, address indexed token1, address pair, uint256)` on the factory. The last field is the pair count | | Indexed list with TVL | `GET /dex/pools`, cursor-paginated. See the [API reference](/dev/reference/api-full-inventory) | Every pair is an ERC-1967 beacon proxy deployed with CREATE2 from the factory. The salt is `keccak256(abi.encodePacked(token0, token1))` with the tokens sorted, as in V2. The init code hash is not the V2 constant: it depends on the deployment's beacon address. Read it from `factory.pairCodeHash()` (`INIT_CODE_PAIR_HASH()` returns the same value) and never hardcode it. ```ts import { encodePacked, getCreate2Address, keccak256, parseAbi } from "viem"; const factory = cfg.addresses.factory; const pairCodeHash = await client.readContract({ address: factory, abi: parseAbi(["function pairCodeHash() view returns (bytes32)"]), functionName: "pairCodeHash", }); const [token0, token1] = tokenA.toLowerCase() < tokenB.toLowerCase() ? [tokenA, tokenB] : [tokenB, tokenA]; const pair = getCreate2Address({ from: factory, salt: keccak256(encodePacked(["address", "address"], [token0, token1])), bytecodeHash: pairCodeHash, }); ``` `eth_getCode` on a pair returns the 82-byte proxy, so bytecode matching against a V2 pair template fails. Identify pairs by the factory event or by `pair.factory()`. `createPair(tokenA, tokenB)` is permissionless, with these checks: | Revert | Condition | | :------------------------------ | :-------------------------------------------------------------------------------------------------------- | | `MotoSwap: IDENTICAL_ADDRESSES` | `tokenA == tokenB` | | `MotoSwap: ZERO_ADDRESS` | Either token is the zero address | | `MotoSwap: TOKEN_NOT_DEPLOYED` | Either token has no code at the time of the call | | `MotoSwap: NO_QUOTE` | The factory has a quote registry set and neither token is a quote asset. The launch configuration sets it | | `MotoSwap: PAIR_EXISTS` | The pair already exists | With the registry set, every pair has a quote asset on one side. That is why a route between two long-tail tokens always has an interior quote asset to take the fee from. ## Indexing the launch farming pairs: the feeTo log order [#indexing-the-launch-farming-pairs-the-feeto-log-order] The pools the farm accepts are Uniswap V2 pairs, and their `Mint` event has no recipient field. The recipient is the `to` of the last LP `Transfer` from the zero address that the same pair emitted before the `Mint`, in the same transaction. An indexer that takes the first one credits the deposit to the factory's `feeTo`, because `_mintFee` runs before `_mint(to, liquidity)`. The Motoswap pair orders `mint()` the same way, so use the same rule there. The rule, the reason, a worked mainnet transaction and a runnable sample are in [the `feeTo` log order](/dev/guides/watch-events#the-feeto-log-order-on-uniswap-v2-pairs) on the events guide. A runnable decode is on [scanners and indexers](/dev/integrations/scanners#decode-pair-logs). ## moto.fun coins [#motofun-coins] A coin launched on [moto.fun](/dev/fun) is not routable while it is bonding. It has no pair, no reserves, and no path: it trades only against its own curve, in ETH. There is nothing for a routing engine to quote and nothing to settle against. The moment that changes is graduation. A graduated coin has a TOKEN/WETH pool and a TOKEN/MOTO pool like any other Motoswap token, with the LP burned, and from then on it routes, quotes and settles exactly as this page describes. For a router, that reduces to one predicate: ```ts // Is this a curve coin, and can I route it yet? const res = await fetch(`https://api.motoswap.org/motofun/coins/${token}`); if (res.status === 404) { // Either moto.fun is off on this deploy ("moto.fun is not enabled on this deploy"), or the // token never launched on the curve ("this token never launched on the moto.fun curve"). // The error body says which. Both mean: treat it as an ordinary ERC-20 and route normally. } else { const coin = await res.json(); if (coin.status !== "graduated") { // Not routable. No pair exists. Do not synthesize one. } // coin.pairWeth and coin.pairMoto are the pools to index once it has graduated. } ``` This endpoint answers `404` for every token while `motofunEnabled` is `false`, so branch on that flag before you call it. A graduated coin normally carries the creator fee on top of the protocol fee, because moto.fun registers its coins in the `CreatorFeeRegistry` at launch. That is the registered-token row in the fee table above. Do not assume it: `activeCreator(token)` is the check, and the quote function on this page already makes it. Discovery, curve pricing, graduation detection and listing fields are on [listing moto.fun coins](/dev/integrations/moto-fun). ## Where to go next [#where-to-go-next] * [Listing moto.fun coins](/dev/integrations/moto-fun) for the bonding-curve venue. * [Trading bots](/dev/integrations/trading-bots) for the single-wallet loop with confirmation handling. * [Scanners and indexers](/dev/integrations/scanners) for the event catalog and the decode recipes. * [Addresses](/dev/addresses) for the `/config` key names. * [Contracts inventory](/dev/reference/contracts-full-inventory) for full ABIs, events and errors. # AI agents (/dev/integrations/ai-agents) The whole read surface of Motoswap is a public REST API with no authentication and aggressive ETag caching. An agent can discover the deployment, read every market and user metric, and stay current by polling, without touching an RPC. Writes are ordinary EVM transactions; if your agent trades, layer [the bot loop](/dev/integrations/trading-bots) on top of this page. ## Machine-readable entry points [#machine-readable-entry-points] | Resource | Where | | :------------------------------------------------------------- | :-------------------------------------------------------------- | | Every documented endpoint, with parameters and response shapes | [API reference](/dev/reference/api-full-inventory) | | Conventions: base URL, caching, pagination, errors | [API overview](/dev/api) | | These docs as plain text for context windows | [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt) | The API reference is the route list to build against. ## Orient in one call [#orient-in-one-call] `GET /config` describes the deployment an agent is talking to: ```jsonc { "chainId": 1, "addresses": { "...": "0x..." }, // zero address = unset for this phase "launchPhase": "dex", // "prelaunch" | "vamp" (before the DEX) | "dex" "splitSwapEnabled": false, "motofunEnabled": true, "bridgeMotoEnabled": false, "motofunUrl": "https://...", // the moto.fun site, or "" when off "pveComboEnabled": false, "misconfigured": [] } ``` There is no RPC URL in it; an agent that goes on chain brings its own. Two rules fall out of it. Interpret **zero addresses as absent features**, and branch on **`launchPhase`** before assuming the AMM surface exists. The response also carries the Uniswap V2 addresses that the launch farm (ended) and `UniswapZap` use (`uniswapV2Router`, `uniswapV2Factory`, `motoUniswapPair`). Status is per contract, and the table is on [addresses](/dev/addresses). An endpoint that depends on a contract with no data behind it yet returns empty or zero values rather than errors, so check the shape you get back rather than assuming a failure. ## The read surface in one table [#the-read-surface-in-one-table] | Question | Endpoint | | :-------------------------------------------- | :--------------------------------------------------------------------- | | What tokens exist, with symbol and decimals? | `GET /tokens` (directory; `verified` flags the curated set) | | What is this token worth, and lately? | `GET /tokens/{address}/chart?range=24h`, `GET /tokens/{address}/stats` | | What pools exist, by TVL? | `GET /dex/pools` (cursor-paginated) | | One pool's state, candles, trades? | `GET /dex/pools/{pair}`, `.../chart`, `.../swaps` | | Protocol-wide volume and revenue? | `GET /analytics/overview`, `GET /stats/series` | | A wallet's Points, rank, Rakeback, portfolio? | `GET /points/{address}`, `/rakeback/{address}`, `/portfolio/{address}` | | Farm pools and emissions? | `GET /farms` (complete bounded set, unpaginated) | | The indexer's current tip? | `GET /chain/head` | Full inventory with response shapes: [API reference](/dev/reference/api-full-inventory). ## Poll politely, get speed for free [#poll-politely-get-speed-for-free] Every user-agnostic GET carries an ETag keyed to the data's actual freshness (the indexer head for chain-driven data, the wallet's own activity for per-user data). The contract: ```ts const res = await fetch(url, { headers: etag ? { "If-None-Match": etag } : {} }); if (res.status === 304) return cached; // nothing changed, ~free etag = res.headers.get("etag") ?? undefined; // rotate and store ``` A `304` means **nothing changed**, not "try later". Per-user endpoints only change when that wallet transacts, so a polling agent that honors ETags gets near-real-time data at a request cost close to zero. Details and per-tier TTLs: [caching guide](/dev/guides/caching-and-etags). Cursor-paginated lists return `{ items, nextCursor, hasMore }`. Cursors are signed and bound to their filters: reuse one with different query params and you get `403`, not silently wrong pages. Send the same filters each page. Details: [pagination guide](/dev/guides/cursor-pagination). ## moto.fun [#motofun] The bonding-curve venue is on the same API, under `/motofun/*`, with the same ETag contract and no keys. When `motofunEnabled` is `false`, every `/motofun/*` route answers `404`, so branch on it first. Four endpoints cover it: | Question | Endpoint | | :------------------------------------------------ | :------------------------------------------- | | What is the venue doing overall? | `GET /motofun/stats` | | What coins exist, newest first? | `GET /motofun/coins?cursor=` (25 per page) | | Is this token a curve coin, and has it graduated? | `GET /motofun/coins/{address}` | | What has the venue earned, per bucket? | `GET /motofun/revenue/series?range=&bucket=` | Plus `GET /activity?venue=motofun` for the unified feed and `GET /creator/{address}`, whose per-coin rows carry `venue: "motofun" | "deployer"` (`"deployer"` marks coins from the retired launch product; new coins launch on moto.fun). Three rules an agent has to encode: * **`404` has two meanings.** On `/motofun/stats`, `/motofun/coins` and `/motofun/revenue/series` it only ever means moto.fun is off on this deploy. On the point lookup `GET /motofun/coins/{address}` it can also mean the venue is on and that token never launched on the curve. Tell them apart by the error body (`"moto.fun is not enabled on this deploy"` versus `"this token never launched on the moto.fun curve"`), or read `/config.motofunEnabled` first and branch. * **A coin has two phases.** While `status` is `"bonding"` or `"frozen"` there is no pair and no pool: `pairWeth` and `pairMoto` are `null`, and nothing in the DEX endpoints knows about it. Once it is `"graduated"` it is an ordinary token in ordinary pools. * **USD values are frozen at write time** and never re-priced, except `tvlUsd`, which is a live gauge. Do not reconcile a historical dollar figure against today's ETH price and report a discrepancy; there is not one. These ETags version on the curve's own head block rather than the chain head, so a quiet venue returns `304` through every DEX block. Polling `/motofun/*` on a short interval is close to free. The app's own backend carries what this API does not: candles, holder tables, pending launches, and an SSE stream. It serves ETags on JSON `200`s, so send `If-None-Match` there too. It pages only the board (`GET /tokens`) and the graduated feed (`GET /graduated/feed`), both with a signed cursor, and some of its routes are rate-limited per IP, so poll it gently. [The API page](/dev/fun/api) covers both. ## What an agent should not do [#what-an-agent-should-not-do] * **Do not scrape numbers from the app.** Everything rendered there comes from these endpoints; take them at the source. * **Do not infer Points math.** Scoring internals are deliberately unpublished; `/points/{address}` and the leaderboard are the public truth. What is public is documented in [points and referrals](/dev/guides/points-and-referrals). * **Do not cache addresses across days.** Deployments change; `/config` is cheap and always right. ## Where to go next [#where-to-go-next] * [Trading bots](/dev/integrations/trading-bots) to add execution on top of reads. * [API reference](/dev/reference/api-full-inventory) for every endpoint's exact shape. * [Caching and ETags](/dev/guides/caching-and-etags) for the freshness model. # Pick your path (/dev/integrations) Motoswap looks like Uniswap V2 from a distance and behaves like it in the math. It does not behave like it at the entry points, and every integration that assumes a vanilla V2 fork breaks in the same three places. Pick your path below; each page front-loads the differences that matter for that job. | You are building | Path | What you'll use | Page | | :-------------------------------------------- | :---------------------------------------------- | :--------------------------------------------------------------- | :--------------------------------------------------- | | An aggregator or routing engine | Quote and execute through `FeeRouter` | Chain reads: the core router's `getAmountsOut` plus the fee legs | [Aggregators](/dev/integrations/aggregators) | | A trading or Telegram bot | One swap loop: discover, quote, submit, confirm | REST + chain writes | [Trading bots](/dev/integrations/trading-bots) | | A scanner, indexer, or analytics pipeline | Event streams and enumeration | RPC logs + the event catalog | [Scanners and indexers](/dev/integrations/scanners) | | An AI agent or data consumer | REST only, no keys, ETag-cached | The public API | [AI agents](/dev/integrations/ai-agents) | | A listing site, chart site, or launch tracker | Discover coins, price a curve, catch graduation | Curve reads + REST | [Listing moto.fun coins](/dev/integrations/moto-fun) | ## The three facts every integrator needs [#the-three-facts-every-integrator-needs] These are repeated on every path page because getting any one wrong ships a bug. `MotoSwapPair.swap` requires `factory.swapAllowed(msg.sender)`, and so does every swap function on `MotoSwapRouter02`, the core router. Only the core router and the `FeeRouter` are admitted. **`FeeRouter` is the only public way to swap**, it is exact-input only, and it charges the full fee. There are no flash swaps. Liquidity functions and the read functions on the core router stay permissionless. Pairs are ERC-1967 beacon proxies. The CREATE2 init code hash depends on the deployment, so derive pair addresses with the hash from `factory.pairCodeHash()`, or skip derivation and read `factory.getPair`. A pasted Uniswap-style `hex"96e8ac..."` constant will compute addresses that do not exist. `FeeRouter` skims `protocolFeeBps`, plus `creatorFeeBps` for a token with an active creator, from whichever end of the path is the more senior quote asset (WETH > USDT > USDC > MOTO at launch settings), or from the first interior quote leg when neither endpoint is quote. Quote your trade on the wrong side and your `amountOutMin` is unfillable. The contract tells you the side: `feeRouter.feeOnOutput(path)` is a public view. There is no quote view; the recipe is on the [aggregators](/dev/integrations/aggregators#quote) page. ## One deployment, three phases [#one-deployment-three-phases] The deployment has moved through three launch phases, and the contract set changed with them: `prelaunch` and `vamp` (the MOTO token and the launch farm, no DEX) came before `dex`, which is the live phase. `GET /config` is the single source of truth: * `launchPhase` is `"prelaunch"`, `"vamp"`, or `"dex"`. It reads `"dex"`: the Motoswap DEX contracts and the moto.fun curve are deployed. In `"vamp"` the MOTO token and the launch farm were deployed and the DEX contracts were not. Per-contract status is on [addresses](/dev/addresses). * A **zero address means that contract is unset for the current phase.** Never hardcode addresses; they also change on redeploy. * The DEX surface documented across these pages (factory, `FeeRouter`, pools, Rakeback) is the `dex` phase. Branch on the phase before you route: during `vamp` (launch farming), trading ran on canonical Uniswap V2 (`uniswapV2Router` / `uniswapV2Factory` in the same response) and the launch farm accepted Uniswap LP. ```ts const cfg = await fetch("https://api.motoswap.org/config") .then((r) => r.json()); if (cfg.launchPhase !== "dex") { // The Motoswap AMM is not the trading venue yet. cfg.addresses.feeRouter // is the zero address in this state. Do not take an address from anywhere else. } ``` # Listing moto.fun coins (/dev/integrations/moto-fun) This page is for anyone building a listing, a chart site, or a scanner over moto.fun: how to find coins, how to price one that has no pool yet, how to notice the moment it gets pools, and what to put in each field of a listing card. A curve coin is not a pool. Half of what a V2 indexer does has no equivalent here until graduation, and the half that does looks different. Read [the moto.fun overview](/dev/fun) first if you have not. Contracts: `LaunchpadCurve`, `LaunchpadToken`, `LaunchpadLens`. Full surfaces: [contracts inventory](/dev/reference/fun-contracts-full-inventory). Endpoints: [API inventory](/dev/reference/fun-api-full-inventory). ## The two-phase lifecycle [#the-two-phase-lifecycle] | | Bonding | Graduated | | :----------- | :------------------------------------- | :---------------------------------------- | | Venue | One curve contract | Two Motoswap V2 pools | | Trade event | `LaunchpadCurve.Trade` | `FeeRouter.Swap` + pair `Swap` | | Price source | Virtual reserves on the curve | Pair reserves | | Liquidity | One-sided: ETH held by the curve | Two-sided, LP burned | | Fee | 1.20% (0.70% protocol + 0.50% creator) | Pool fee + 0.70% protocol + 0.30% creator | | Quote asset | ETH | WETH and MOTO, two pools | Both phases belong to the same coin and the same address. A listing that treats graduation as a new asset restarts the chart and loses the history; the app joins them and so should you. ## Discovery [#discovery] ```ts // Cursor-paginated, newest first, ETag-cached, 25 per page. let cursor: string | undefined; do { const url = new URL("https://api.motoswap.org/motofun/coins"); if (cursor) url.searchParams.set("cursor", cursor); const page = await fetch(url).then((r) => r.json()); for (const coin of page.items) index(coin); cursor = page.hasMore ? (page.nextCursor ?? undefined) : undefined; } while (cursor); ``` `items[]` carries `address`, `name`, `symbol`, `creator`, `timestamp`, `status`, `pairWeth`, `pairMoto`, `volume24hUsd`, `volumeAllUsd`, `trades`, `lastPriceX18`. Check `hasMore`, not an empty `items`: a malformed cursor returns an empty page with `200`. ```ts // One address emits every launch. There is no factory to enumerate. const logs = await client.getLogs({ address: cfg.addresses.launchpadCurve, event: parseAbiItem( "event TokenCreated(address indexed token, address indexed creator, string name, string symbol, bytes32 metadataHash, address indexed quoteToken)", ), fromBlock, toBlock, }); ``` Treat the coin set as append-only. `creator` is the launch signer, which is the coin's identity for attribution. It is not necessarily the fee recipient, which is announced separately by `FeeRecipientSet` when it differs. `topic0` for this event is `0x6223a619ba904a9914dac4461d25f8f27e372c0a030791c3a5c012768765ef45`, and `quoteToken`, the fourth topic, is the curve's WETH address. See [which asset a coin is quoted in](/dev/fun/contracts#which-asset-a-coin-is-quoted-in). **Where to start the backfill.** From the block the curve proxy was deployed in. No endpoint serves that block today. Two ways to get it. The explorer's contract-creation record for the curve address in `/config` gives it directly. Or find it from chain: it is the first block at which `eth_getCode(curve, block)` is non-empty, so binary-search between block `0` and the head, which takes about 25 calls. That search needs a provider that serves historical state, which many free endpoints do not, and they answer with an error rather than an empty result, so you will know. **Check your provider before you trust an empty result.** Public RPC endpoints cap `eth_getLogs` ranges, and some answer a query they cannot serve with an empty success rather than an error. Two habits cover it. Page your backfill in fixed block ranges instead of asking for everything at once. And before you start, query the curve's deploy block on its own with no topic filter: a proxy emits `Upgraded` and `Initialized` there, so a provider that returns nothing for that block is dropping results, and its empty pages prove nothing. A gasless launch is a signed intent: the address is fixed and the app lists it, but there is no contract at it and no `TokenCreated` log until the first buy. If you list pending coins, get them from `GET /backend-fun//intents/{address}` on the app backend and mark them clearly. If you only list real tokens, filter on `TokenCreated` and you will never see one. ## One script, end to end [#one-script-end-to-end] The snippets on this page assume a viem `client` and a `cfg` read from `/config`. This one assumes nothing: it lists every launch, prints one coin's first trades, and either points you at the lens or, if the coin has graduated, finds both pools and prices it from the WETH pool. It needs `viem` and three environment variables, and it runs as pasted. ```ts // Lists every moto.fun launch, then follows one coin to its pools. // Run: RPC_URL=... CURVE=0x... START_BLOCK=... bun track.ts // CURVE is addresses.launchpadCurve from GET /config. START_BLOCK is the block the curve was deployed in. import { createPublicClient, http, parseAbi, parseAbiItem, formatUnits, type Address } from "viem"; const client = createPublicClient({ transport: http(process.env.RPC_URL) }); const curve = process.env.CURVE as Address; const startBlock = BigInt(process.env.START_BLOCK ?? "0"); const PAGE = 2_000n; // blocks per eth_getLogs call. Lower it if your provider refuses the range. const tokenCreated = parseAbiItem( "event TokenCreated(address indexed token, address indexed creator, string name, string symbol, bytes32 metadataHash, address indexed quoteToken)", ); const trade = parseAbiItem( "event Trade(address indexed trader, address indexed token, bool isBuy, uint256 ethAmount, uint256 tokenAmount, uint256 priceX18, uint256 protocolFee, uint256 creatorFee)", ); const graduated = parseAbiItem( "event Graduated(address indexed token, address pairWeth, address pairMoto, uint256 wethLp, uint256 motoLp, uint256 tokensLpWeth, uint256 tokensLpMoto, uint256 dustBurned, address indexed keeper)", ); const curveAbi = parseAbi([ "function curves(address) view returns (uint112 realEth, uint112 tokenReserve, uint8 status, bool buysPaused, uint16 paramsId)", ]); const pairAbi = parseAbi([ "function token0() view returns (address)", "function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)", ]); // Before trusting an empty page: the deploy block always holds the proxy's own events. const head = await client.getBlockNumber(); const sanity = await client.getLogs({ address: curve, fromBlock: startBlock, toBlock: startBlock }); if (sanity.length === 0) throw new Error("no logs in the curve's deploy block: wrong START_BLOCK, or this provider drops results"); async function paged(fetchPage: (from: bigint, to: bigint) => Promise): Promise { const out: T[] = []; for (let from = startBlock; from <= head; from += PAGE) { const to = from + PAGE - 1n > head ? head : from + PAGE - 1n; out.push(...(await fetchPage(from, to))); } return out; } // 1. Every launch. viem decodes the indexed fields from the topics and the rest from the data. const launches = await paged((fromBlock, toBlock) => client.getLogs({ address: curve, event: tokenCreated, fromBlock, toBlock }), ); for (const l of launches) console.log("launch", l.args.token, l.args.symbol, "by", l.args.creator, "at block", l.blockNumber); if (launches.length === 0) throw new Error("no launches on this curve yet"); // 2. One coin's trades. `token` is the SECOND indexed field of Trade, so filter on args.token. const token = launches[0].args.token!; const trades = await paged((fromBlock, toBlock) => client.getLogs({ address: curve, event: trade, args: { token }, fromBlock, toBlock }), ); for (const t of trades.slice(0, 3)) { const a = t.args; console.log(a.isBuy ? "buy " : "sell", formatUnits(a.ethAmount!, 18), "ETH for", formatUnits(a.tokenAmount!, 18), "coins,", "price", formatUnits(a.priceX18!, 18), "ETH per coin, fees", a.protocolFee, "+", a.creatorFee, "wei"); } // 3. Status decides where the price comes from. 1 = Trading, 2 = Frozen, 3 = Graduated. const [, , status] = await client.readContract({ address: curve, abi: curveAbi, functionName: "curves", args: [token] }); if (status !== 3) { console.log("still on the curve, status", status, "- price it through LaunchpadLens.currentPrice"); } else { const [g] = await paged((fromBlock, toBlock) => client.getLogs({ address: curve, event: graduated, args: { token }, fromBlock, toBlock }), ); const { pairWeth, pairMoto } = g.args; console.log("graduated. TOKEN/WETH pool", pairWeth, "TOKEN/MOTO pool", pairMoto); // Price from the WETH pool. V2 orders reserves by token address, so find which side is the coin. const [token0, [r0, r1]] = await Promise.all([ client.readContract({ address: pairWeth!, abi: pairAbi, functionName: "token0" }), client.readContract({ address: pairWeth!, abi: pairAbi, functionName: "getReserves" }), ]); const coinIs0 = token0.toLowerCase() === token.toLowerCase(); const [coinReserve, wethReserve] = coinIs0 ? [r0, r1] : [r1, r0]; const priceX18 = (wethReserve * 10n ** 18n) / coinReserve; // ETH per whole coin, 1e18 fixed point console.log("pool price", formatUnits(priceX18, 18), "ETH per coin"); } ``` It pages `eth_getLogs` because an unpaged backfill is the most common way to get a silently empty result, and it refuses to start if the deploy block comes back empty, for the reason given under [Discovery](#discovery). ## Pricing a bonding curve [#pricing-a-bonding-curve] There is no pair, no reserves pair, and no `getAmountsOut`. **Ask the lens.** It reads the coin's own curve parameters and the live fee rates at call time, which is why it is the recommended method: ```ts import { parseAbi } from "viem"; const priceX18 = await client.readContract({ address: cfg.addresses.launchpadLens, abi: parseAbi(["function currentPrice(address token) view returns (uint256)"]), functionName: "currentPrice", args: [token], }); ``` `currentPrice` is the live price only while a coin is on the curve. **It does not check status.** For a graduated coin it returns the graduation price, frozen: the price the coin had when its reserve reached the floor, which is also the price its pools opened at. That number is correct for what it is and goes stale against the market from the first pool trade on, with no revert and no zero to warn you. The app backend serves the same value as `curve.priceX18` on `GET /tokens/{address}`, for graduated coins too, with `status` beside it. For an address the curve never launched, the call returns a non-zero number as well. So read `curves(token).status` first (`0` None, `1` Trading, `2` Frozen, `3` Graduated), and take a graduated coin's live price from its pools, as shown [below](#pricing-after-graduation). If you need the arithmetic itself, for a simulation or to price from an event stream without an RPC round trip per coin, this is the whole model. The curve is constant product against virtual reserves on both sides. The virtual reserves are **per coin**: read them with `paramsOf(token)`, never from the `VIRTUAL_ETH` and `VIRTUAL_TOKENS` constants, which describe only coins stamped with parameter set `0`. That is what a fresh deployment stamps, and it stops being the whole story the first time the owner appends a set. ```ts import { parseAbi } from "viem"; const curveAbi = parseAbi([ "function curves(address) view returns (uint112 realEth, uint112 tokenReserve, uint8 status, bool buysPaused, uint16 paramsId)", "function paramsOf(address token) view returns (uint256 virtualEth, uint256 floorSupply, uint256 virtualTokens)", ]); const [[realEth, tokenReserve], [virtualEth, , virtualTokens]] = await Promise.all([ client.readContract({ address: cfg.addresses.launchpadCurve, abi: curveAbi, functionName: "curves", args: [token] }), client.readContract({ address: cfg.addresses.launchpadCurve, abi: curveAbi, functionName: "paramsOf", args: [token] }), ]); // ETH per whole coin, 1e18 fixed point. Identical to Lens.currentPrice and to Trade.priceX18. const priceX18 = ((virtualEth + realEth) * 10n ** 18n) / (virtualTokens + tokenReserve); ``` `curves` returns five values. `paramsOf` can be cached per coin for as long as you like, because a coin's parameters are fixed at launch. It cannot be shared between coins. A coin launched under parameter set `0` opens at `priceX18 = 1249999999`, which is about `1.25e-9` ETH per coin. Under any parameter set the opening price is `virtualEth * 1e18 / (virtualTokens + 1e27)`, so it moves in proportion to that set's virtual ETH. A display pipeline that converts wei to a float with a `1e-6` floor renders every live curve as zero. Convert from the 1e18 fixed-point value directly and format with enough significant figures, or your whole board reads `$0.00`. For quotes rather than spot, use the lens, which reads the live fee rates: ``` quoteBuy(address token, uint256 ethIn) -> (tokensOut, grossUsed, refund) quoteSell(address token, uint256 tokensIn) -> ethOut // net of fees bondingProgress(address token) -> uint256 // 0 to 1e18 ``` ## Pricing after graduation [#pricing-after-graduation] A graduated coin is an ordinary Motoswap V2 token with two pools. Take both addresses from `Graduated` (or from `GET /motofun/coins/{address}`) and price from reserves: ``` priceX18 = wethReserve * 1e18 / coinReserve // on pairWeth, ETH per whole coin ``` Use **`pairWeth` as the reference price.** It is quoted in the same asset as the curve was, so the series continues without a conversion, and it opens at exactly the last curve price. `pairMoto` prices the coin in MOTO and needs a MOTO/WETH rate to compare. Both the coin and WETH have 18 decimals, so there is no decimals adjustment. V2 orders `reserve0` and `reserve1` by token address, so read `token0()` to learn which side is the coin: the script above does exactly this. ## Filling a listing card [#filling-a-listing-card] | Field | Bonding | Graduated | | :--------------- | :------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- | | Base token | The coin. 18 decimals, always | Same | | Quote token | ETH | WETH, and MOTO on the second pool | | Total supply | `1e27`, always, fixed | Same. No mint, no burn except graduation dust | | Price | `priceX18` above, or `Trade.priceX18` | Pair reserves | | Market cap / FDV | `priceX18 * 1e9 / 1e18` ETH (price per whole coin times 1 billion coins). They are equal: supply is fully issued at launch | Same formula | | Liquidity | `realEth` from `curves(token)`. **One-sided** | Both pools' reserves | | Volume | Sum `Trade.ethAmount`, or read `volume24hUsd` | `FeeRouter.Swap` | | Holders | `GET /backend-fun//tokens/{address}/holders` | Same, plus transfers | | Pair address | **None. Do not synthesize one** | `pairWeth`, `pairMoto` from `Graduated` | | Logo | `GET /launchpad/token/{address}/logo` | Same | Two of those need care. **Liquidity while bonding is one-sided and is not withdrawable by anyone.** `realEth` is the ETH the curve holds against the coins it has not sold. There is no LP position, no LP holder, and no function that lets anybody remove it, including the coin's creator and the protocol owner. If your listing has a "LP burned" signal or any other liquidity-safety check, a bonding coin satisfies it structurally rather than by having a burned LP token. **Market cap equals FDV** on every coin, in both phases. The whole supply is minted at launch, so there is no unlock schedule, no vesting, no team allocation and no future emission to discount. A listing that shows an FDV different from market cap is showing a bug. ## Detecting graduation [#detecting-graduation] Watch one event: ```solidity event Graduated( address indexed token, address pairWeth, // NOT indexed address pairMoto, // NOT indexed uint256 wethLp, uint256 motoLp, uint256 tokensLpWeth, uint256 tokensLpMoto, uint256 dustBurned, address indexed keeper ); ``` The pair addresses are in the data, not the topics, so a topic-only filter finds the graduation but not the pools. Decode the payload. Polling instead: `GET /motofun/coins/{address}` returns `status` and both pair addresses, ETag-cached and versioned on the curve's own head block, so a poll costs a `304` until something actually happens. `FloorReached` fires earlier and is worth catching too: it marks the coin frozen and trading paused, usually under a minute before `Graduated`. A listing that shows a frozen coin as tradeable will collect failed transactions. If nobody graduates a frozen coin, selling reopens 15 minutes after the freeze and a sell walks it back to `Trading`. `Frozen` is not a terminal state and not a one-way ratchet. Model the back edge or your state machine will get stuck. At graduation the pools are seeded at the final curve price and **both LP positions are burned to `0x…dEaD`**, in the same transaction. There is no lock contract to inspect and no unlock date: the LP supply is at the burn address, permanently. To verify it, look at the graduation transaction: the pair's LP `Transfer` to `0x…dEaD` is the whole of that mint. Right after graduation `totalSupply()` is that amount plus the 1000 wei V2 minimum liquidity a pair mints to the zero address on its first mint. Do not assert that equality later: other liquidity providers add to the supply and not to the burn, a protocol fee mint can add to it, and if the pair existed before graduation the curve's mint was not the first one. ## Tracking one coin from launch to pools [#tracking-one-coin-from-launch-to-pools] Everything below is on the curve address, filtered by the coin in `topics[1]`, except where noted. | Step | Event | What you learn | What to do | | :--- | :--------------------------------------------------- | :--------------------------------------------------------------------- | :----------------------------------------------------------------- | | 1 | `TokenCreated` | `token`, `creator`, name, symbol, `metadataHash`, `quoteToken` | Start tracking. Read `paramsOf(token)` once and keep it | | 1a | `FeeRecipientSet` (only if redirected) | Where the creator fee goes | Absent means the creator is the recipient | | 2 | `Trade` (many) | Side, amounts, the spot price after the trade, both fee amounts | `token` is `topics[2]` here, not `topics[1]`: `trader` comes first | | 3 | `FloorReached` | The coin is `Frozen`. Trading is paused | Stop quoting buys. Expect `Graduated` shortly | | 3a | `Trade` with `isBuy == false` after the reopen delay | The coin went back to `Trading` | Resume. This back edge is real, model it | | 4 | `Graduated` | `pairWeth` and `pairMoto` in the data, plus what was seeded and burned | Switch the coin to its two pools. The curve series is now history | Two ordering facts. In the transaction that freezes a coin, `FloorReached` is emitted **before** that buy's own `Trade`, so a pipeline folding status in log order sees the freeze first. And `Frozen` is not instantaneous: a keeper normally graduates the coin within about a minute, which is several blocks in which the coin is frozen and visible as such. ### Decoding the logs by hand [#decoding-the-logs-by-hand] A library does this for you (the script above uses viem's). If you are decoding raw logs, an indexed `address` is a 32-byte topic with the address in its last 20 bytes, and every data field below is one 32-byte word unless marked. `TokenCreated` has two strings, so its data uses the ABI head and tail layout: | Where | Field | | :--------------------- | :------------------------------------------------------------------------------- | | `topics[1]` | `token` | | `topics[2]` | `creator` | | `topics[3]` | `quoteToken` | | data word 0 | byte offset of `name` within the data (`0x60` for this event) | | data word 1 | byte offset of `symbol` | | data word 2 | `metadataHash` | | at the `name` offset | one word holding the byte length, then the UTF-8 bytes padded to a word boundary | | at the `symbol` offset | the same shape | `Trade`: | Where | Field | | :---------- | :--------------------------------------------------------------------- | | `topics[1]` | `trader` | | `topics[2]` | `token` | | data word 0 | `isBuy`, a full word holding `0` or `1` | | data word 1 | `ethAmount` in wei. Fee-inclusive on a buy, pre-fee on a sell | | data word 2 | `tokenAmount`, 18 decimals | | data word 3 | `priceX18`. ETH per whole coin is this divided by `1e18`, nothing else | | data word 4 | `protocolFee` in wei | | data word 5 | `creatorFee` in wei | `Graduated`: `topics[1]` is `token`, `topics[2]` is `keeper`, and the data words in order are `pairWeth`, `pairMoto`, `wethLp`, `motoLp`, `tokensLpWeth`, `tokensLpMoto`, `dustBurned`. Despite the names, `wethLp` and `motoLp` are the **amounts of WETH and MOTO seeded** into the two pools, not LP token amounts, and `tokensLpWeth` and `tokensLpMoto` are the coin amounts seeded beside them. After step 4 the curve emits no more trades for that coin. Trades are pair `Swap` events on the two pool addresses you decoded, like any other Motoswap pool. The one thing the curve can still emit for a graduated coin is its PvE credit: `PveRecorded` normally lands inside the graduation transaction, and if that credit failed (`PveRecordFailed`) it lands later, whenever it is retried. If you would rather poll than index: `GET /motofun/coins/{address}` on the Motoswap API returns `status`, `pairWeth` and `pairMoto`, and it costs a `304` until something changes. ## Trades, volume, and holders [#trades-volume-and-holders] One `Trade` event per curve trade. There is no pair `Swap` and no `FeeRouter.Swap` until graduation. ```solidity event Trade( address indexed trader, address indexed token, bool isBuy, uint256 ethAmount, uint256 tokenAmount, uint256 priceX18, uint256 protocolFee, uint256 creatorFee ); ``` * **`isBuy` is not indexed.** You cannot topic-filter buys from sells. Decode and branch. * **`ethAmount` is fee-inclusive on buys and pre-fee on sells.** Decide which notion of volume you are reporting before you sum it, and be consistent. The official venue stats sum both sides. * `protocolFee` and `creatorFee` are literal transfer amounts, not rates, so a fee change needs no handling on your side. * `priceX18` is the spot **after** the trade. Holders come from `GET /backend-fun//tokens/{address}/holders`, which gives the top 10 by net position, the true total count, and the PvE escrow balance separately. The escrow is netted out of the holder list so it does not read as a whale. If you need the full holder set, derive it from `Transfer` logs like any ERC-20. ## Metadata and logos [#metadata-and-logos] Name and symbol are on the token contract and immutable. The description, image and links are committed on chain as a `metadataHash` at launch, and both services re-verify against that hash before serving, returning `null` rather than unverified content. The logo endpoint to use is on the Motoswap API: ``` GET https://api.motoswap.org/launchpad/token/{address}/logo ``` ETag-cached, PNG or WebP, at most 256 KB and 512x512. `404 { "error": "no logo" }` when the coin has none. Do not scrape the data URL out of the app backend's JSON. ## Venue-level numbers [#venue-level-numbers] ``` GET /motofun/stats # coins by status, TVL, volume, fees, trades, launches GET /motofun/revenue/series?range=30d&bucket=day # revenue, Collector leg vs creator leg GET /analytics/overview # optional top-level "motofun" component ``` Every USD field except `tvlUsd` is a **frozen write-time snapshot** and is never re-priced. `tvlUsd` is a live gauge over non-graduated curves only; a graduated coin's liquidity is in pools and is counted by the DEX side. The 24h window is anchored to the indexer head block timestamp, not to wall clock. ## Sniping, and why the constraints are not there [#sniping-and-why-the-constraints-are-not-there] There is no block delay, no cooldown, no max buy, no per-wallet cap, and no anti-bot window on the curve. The first buyer can take everything above the coin's graduation floor in one transaction. The only bound on a single buy is the partial-fill clamp at the graduation floor, which refunds the excess rather than reverting. What does exist: * A launch intent's `submitter` field binds who may submit the first buy. When a creator takes a dev buy they must set it to themselves, or the signature is a bearer token in the mempool * The CREATE2 salt is `keccak256(abi.encode(creator, salt))`, so a replayed salt from another wallet lands at a different address * A pre-bond transfer blocklist stops the coin being seeded into a pool before graduation, on the venues it enumerates. It is a list, not a rule: an unlisted quote asset, a V3 or V4 pool, or an unknown V2 fork sits outside it ## Where to go next [#where-to-go-next] * [Contracts](/dev/fun/contracts) for the full call surface. * [Realtime and candles](/dev/fun/realtime) for the stream and the exact chart rules. * [Scanners](/dev/integrations/scanners#motofun-curve-events) for the indexing discipline. * [Trading bots](/dev/integrations/trading-bots#motofun-curve-trades) to trade rather than list. # Scanners and indexers (/dev/integrations/scanners) Everything the official indexer knows, it learned from logs; there is no private state. This page is the map: which events exist, which fields mean what, how to decode them, and the attribution traps our own indexer had to solve that yours will hit too. ## What you can index [#what-you-can-index] Status is per contract. `GET /config` is the source, and the per-contract table with addresses is on [addresses](/dev/addresses). In short: the Motoswap factory, pairs, `FeeRouter`, `Collector`, `RakebackV2` and the moto.fun curve are deployed, next to the MOTO token, MOTO staking, Motocat staking and the launch farm (`/config` keys `factory`, `feeRouter`, `collector`, `rakeback`, `launchpadCurve`, `motoToken`, `motoStaking`, `motocatStaking`, `vampChef`). Read every address from `/config`, and treat a zero key there as unset for the current phase. The launch farm's pools are Uniswap V2 pairs (plus one single-sided MOTO pool) from launch farming (ended), not Motoswap pairs; `GET /farms` lists them as `lpToken`, and `/config.addresses.motoUniswapPair` is the MOTO/WETH one. The pair event set below is the same on both. A Motoswap pair emits the Uniswap V2 events with identical signatures and topics, so a decoder you build against Uniswap V2 pairs works unchanged on Motoswap pairs. The trade-level `FeeRouter.Swap` event exists only on Motoswap. ## Enumerate from genesis [#enumerate-from-genesis] `PairCreated` on the factory is the discovery event, exactly as in V2: ```solidity event PairCreated(address indexed token0, address indexed token1, address pair, uint256); ``` | Field | Type | Description | | :------- | :---------------- | :----------------------------------------- | | `token0` | `address` indexed | Sorted lower token | | `token1` | `address` indexed | Sorted higher token | | `pair` | `address` | The pool. An 82-byte ERC-1967 beacon proxy | | *(4th)* | `uint256` | Running pair count | Watch the factory address from `/config`, backfill with `getLogs`, and treat the pair set as append-only. `eth_getCode` on a Motoswap pair returns 82 bytes, not V2 pair bytecode, and bytecode verification against a V2 template will fail. Identity comes from the factory event or from `pair.factory()`, not the code. Offline address derivation needs `factory.pairCodeHash()`, never a hardcoded hash. The recipe is on [aggregators](/dev/integrations/aggregators#pair-discovery). ## The core event set [#the-core-event-set] ### Pair: `Swap`, `Sync`, `Mint`, `Burn` [#pair-swap-sync-mint-burn] Signatures and indexed flags, from the pair source. They match Uniswap V2 exactly, topics included: ```solidity event Swap( address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to ); event Mint(address indexed sender, uint256 amount0, uint256 amount1); event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); event Sync(uint112 reserve0, uint112 reserve1); ``` | Event | `topic0` | | :----- | :------------------------------------------------------------------- | | `Swap` | `0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822` | | `Mint` | `0x4c209b5fc8ad50758f13e2e1088ba56a560dff690a1c6fef26394f4c03821c4f` | | `Burn` | `0xdccd412f0b1252819cb1fd330b93224ca42612892bb3f4f789976e6d81936496` | | `Sync` | `0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1` | The semantics an indexer needs on top: | Event | The trap | The rule | | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------- | | `Swap` | `sender` is the router. `to` is often a contract too: the next pair on a multi-hop, the `FeeRouter` when the fee comes off the output, the router on ETH exits | On Motoswap, join with `FeeRouter.Swap` in the same transaction for the trader. On Uniswap V2 pairs, use the transaction sender | | `Sync` | None. Emitted on every reserve change, before the `Swap`, `Mint` or `Burn` it belongs to | The canonical reserve feed. Price from here | | `Mint` | **No recipient field.** `sender` is the router | Pair it with the last LP `Transfer` from the zero address that the same pair emitted before it in the same transaction. See the next section | | `Burn` | `to` is the router on ETH exits, because the router unwraps and forwards | Use the transaction sender, or the LP `Transfer(user, pair, ...)` that precedes it, for the withdrawing account | ### The feeTo transfer comes before the depositor's [#the-feeto-transfer-comes-before-the-depositors] The recipient of a `Mint` is the `to` of the **last** LP `Transfer` from the zero address that the same pair emitted before it in the same transaction, never the first. In `mint()` the pair runs `_mintFee` before it mints the depositor's LP tokens, so when the factory's `feeTo` is set, a `Transfer(0x0, feeTo, ...)` comes first. The Uniswap V2 factory has `feeTo` set, and the Motoswap pair runs the same order. The full rule, the first-mint case, a worked mainnet transaction and a runnable sample are in [the `feeTo` log order](/dev/guides/watch-events#the-feeto-log-order-on-uniswap-v2-pairs) on the events guide. ## Decode pair logs [#decode-pair-logs] This runs as written once you set `RPC_URL` to your own Ethereum mainnet RPC endpoint. It reads the Motoswap MOTO/WETH pair, found with `factory.getPair`. Point `pair` at any other Motoswap pair (from `factory.getPair` or `GET /dex/pools`) and nothing else changes. ```ts import { createPublicClient, http, parseAbi, parseEventLogs, zeroAddress } from "viem"; const API = "https://api.motoswap.org"; const cfg = await fetch(`${API}/config`).then((r) => r.json()); // Your own Ethereum mainnet RPC endpoint. const RPC_URL = process.env.RPC_URL; if (!RPC_URL) throw new Error("Set RPC_URL to your own Ethereum mainnet RPC endpoint."); const client = createPublicClient({ transport: http(RPC_URL) }); // The Motoswap MOTO/WETH pair, read from the factory in /config. const factoryAbi = parseAbi(["function getPair(address tokenA, address tokenB) view returns (address)"]); const pair = await client.readContract({ address: cfg.addresses.factory, abi: factoryAbi, functionName: "getPair", args: [cfg.addresses.motoToken, cfg.addresses.weth], }); if (pair === zeroAddress) throw new Error("No Motoswap MOTO/WETH pair at this factory."); const pairAbi = parseAbi([ "event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to)", "event Mint(address indexed sender, uint256 amount0, uint256 amount1)", "event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to)", "event Sync(uint112 reserve0, uint112 reserve1)", "event Transfer(address indexed from, address indexed to, uint256 value)", ]); const head = await client.getBlockNumber(); const raw = await client.getLogs({ address: pair, fromBlock: head - 300n, toBlock: head }); const logs = parseEventLogs({ abi: pairAbi, logs: raw }); for (const log of logs) { if (log.eventName === "Swap") { const { amount0In, amount1In, amount0Out, amount1Out, to } = log.args; console.log("Swap", { amount0In, amount1In, amount0Out, amount1Out, to }); } if (log.eventName === "Sync") { console.log("Sync", log.args.reserve0, log.args.reserve1); } if (log.eventName === "Mint") { // The LP recipient is the last zero-address Transfer the pair emitted // before this Mint in the same transaction. const recipient = logs .filter( (l) => l.eventName === "Transfer" && l.transactionHash === log.transactionHash && l.logIndex < log.logIndex && l.args.from === zeroAddress, ) .at(-1)?.args.to; console.log("Mint", log.args.amount0, log.args.amount1, "LP to", recipient); } if (log.eventName === "Burn") { console.log("Burn", log.args.amount0, log.args.amount1, "to", log.args.to); } } ``` Every amount comes back as a `bigint`. Keep it that way: a reserve does not fit in a JS `Number`. ## Reserves and price [#reserves-and-price] `Sync` carries both reserves as `uint112`. Spot price is their ratio, scaled for decimals, in integer math: ```ts const pairReadAbi = parseAbi([ "function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)", "function token0() view returns (address)", "function token1() view returns (address)", ]); const erc20Abi = parseAbi(["function decimals() view returns (uint8)"]); const [[reserve0, reserve1], token0, token1] = await Promise.all([ client.readContract({ address: pair, abi: pairReadAbi, functionName: "getReserves" }), client.readContract({ address: pair, abi: pairReadAbi, functionName: "token0" }), client.readContract({ address: pair, abi: pairReadAbi, functionName: "token1" }), ]); const [decimals0, decimals1] = await Promise.all( [token0, token1].map((address) => client.readContract({ address, abi: erc20Abi, functionName: "decimals" }), ), ); // Price of one whole token0 in token1, as an 18-decimal fixed-point bigint. const price0In1 = (reserve1 * 10n ** BigInt(decimals0) * 10n ** 18n) / (reserve0 * 10n ** BigInt(decimals1)); ``` Spot price ignores the fee and the trade size. For an executable price use the amount-out formula with the live pair fee, and on Motoswap the `FeeRouter` skims on top of it: [the quote recipe](/dev/integrations/aggregators#quote). The pair also keeps the V2 cumulative price accumulators (`price0CumulativeLast`, `price1CumulativeLast`) for TWAPs. ## `FeeRouter.Swap`: the trade-level event [#feerouterswap-the-trade-level-event] Motoswap only: a trade that never touches the `FeeRouter` does not emit it. A multi-hop trade emits N pair `Swap`s but one of these, and it is the only place the net delivery and the exact protocol fee appear: ```solidity event Swap( address indexed user, address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut, address feeToken, uint256 feeAmount ); ``` | Field | Type | Description | | :---------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `user` | `address` indexed | The caller of the `FeeRouter` (`msg.sender`). The payer, **not** the `to` recipient. They differ when a contract swaps on someone's behalf: an aggregator's settlement contract is the `user` on every swap it routes | | `tokenIn` | `address` indexed | Path start. WETH on ETH-in swaps | | `tokenOut` | `address` indexed | Path end. WETH on ETH-out swaps | | `amountIn` | `uint256` | Gross input. On the fee-on-transfer entrypoints, the amount that reached the `FeeRouter` | | `amountOut` | `uint256` | **Net delivery after all fees** | | `feeToken` | `address` | The quote asset the protocol fee was taken in | | `feeAmount` | `uint256` | The protocol fee transferred to the `Collector`. Creator fees are not included | A split fill emits one `Swap` per leg, not one for the whole order, because legs can pay their fee in different quote assets. Sum the legs of a transaction if you want order-level volume. A creator fee, when one applies, has its own event on the `FeeRouter`: `CreatorFee(address indexed token, address indexed creator, address quoteAsset, uint256 amount)`. Volume, fee revenue, and per-trade accounting all reconcile from these events. Do not sum pair legs to get trade volume; on multi-hop and interior-quote routes the legs do not sum to the delivery. Only the core router and the `FeeRouter` can reach `pair.swap`, and only the `FeeRouter` can call the core router's swap functions. So every pair `Swap` on Motoswap sits in a transaction with a `FeeRouter.Swap`. When the protocol fee is set to 0 the event is still emitted, with `feeAmount` 0. Trade-level metrics come from the `FeeRouter` event; pool-level flow comes from pair events. Keep the two pipelines separate and they never double-count. ## Module catalog [#module-catalog] Beyond the DEX core, each module has a small, stable event set. The names below are the events an indexer consumes, checked against the contract source; admin and config events are left out. Full signatures with every field: [addresses, config and events](/dev/reference/addresses-config-events). | Contract | Status on Ethereum | Events an indexer consumes | | :--------------------------------------------------------------- | :---------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `VampChef` (launch farming, ended, `/config.addresses.vampChef`) | Deployed, farming ended | `PoolAdded`, `PoolSet`, `Deposit`, `Withdraw`, `EmergencyWithdraw`, `RewardPaid`, `RewardForfeited`, `EmissionStarted`, `RewardShortfall` (not live yet, arrives with a contract upgrade). Withdrawals are one step here: there is no `WithdrawQueued` or `WithdrawClaimed` | | `MotoStaking` | Deployed | `Staked`, `Imported`, `Appended`, `UnstakeRequested`, `Unstaked`, plus `RewardNotified` per revshare inflow and `Claimed` / `ClaimedAsMoto` per payout. `ClaimedAsMoto` needs a `FeeRouter` wired into the deployed implementation, which arrives with a contract upgrade | | `MotocatStaking` | Deployed | `Staked`, `Unstaked`, `Seeded` (distribution-day rows; wallets did not act), `SeedingClosed` | | `MasterChef` (deployed; no sustain farm is running) | Deployed | `PoolAdded`, `PoolSet`, `Deposit`, `WithdrawQueued`, `WithdrawClaimed`, `EmergencyWithdraw`, `RewardPaid`, `RewardShortfall`, `RewardForfeited`, `EmissionStarted` | | `Collector` | Deployed | `Distributed`, `BucketPaid`, `BucketHeld`, `HeldReleased`: the split of the protocol fee across its buckets, per distribution | | `RakebackV2` | Deployed | `RootPosted`, `SlicePosted`, `Activated`, `Expired`, `RootCancelled`: the weekly epoch roots. `Claimed` and `ClaimedAsMoto`: the Rakeback ledger paid against them. `Claimed(epoch, token, index, account, amount)` indexes epoch and token only. The claimant `account` is NOT indexed, so you cannot topic-filter by claimant; filter by epoch or token and match `account` in your handler (`ClaimedAsMoto` does index the user) | | `PveVault` | Deployed | `PveReceived`, `PveCredited`, `PveHeld`, `PveClaimed`, `Forwarded`: the PvE drop ledger for Motocat stakers | "Deployed" means the `/config` key is set, there is code at the address, and every event named in that row is in the deployed implementation, except where a row marks one "not live yet". Read the address from `/config` in every case, and never subscribe to a zero address. A `Claimed` row alone does not tell you what a wallet was owed, because a leaf is all-or-nothing against a published root. Consume the root lifecycle (`RootPosted` / `SlicePosted` / `Activated`) to know what was payable per `(epoch, token)`, and `Expired` to know what lapsed unclaimed. Unclaimed Rakeback expires after 30 days and is swept to the treasury. ## moto.fun curve events [#motofun-curve-events] [moto.fun](/dev/fun) coins emit nothing on the factory or the pairs until they graduate. Their whole pre-graduation life is logs from **one address**, the `LaunchpadCurve` singleton, which is also the discovery source: there is no factory to enumerate here. | Event | Fields | What an indexer does with it | | :-------------------------------- | :-------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------- | | `TokenCreated` | `token` indexed, `creator` indexed, `name`, `symbol`, `metadataHash`, `quoteToken` indexed | Discovery. Insert the coin. `creator` is the launch signer, the attribution identity, and not necessarily the fee recipient | | `Trade` | `trader` indexed, `token` indexed, `isBuy`, `ethAmount`, `tokenAmount`, `priceX18`, `protocolFee`, `creatorFee` | The trade-level truth. One per curve trade, no pair leg | | `FloorReached` | `token` indexed, `realEth` | The coin froze. Trading is paused, `tokenReserve` is exactly the floor | | `Graduated` | `token` indexed, `keeper` indexed, plus **unindexed** `pairWeth`, `pairMoto`, four amounts and `dustBurned` | Switch the coin to pool indexing. Decode the payload to get the pairs | | `PveFill` | `token` indexed, `contributor` indexed, `tokens` | An escrow contribution for Motocat stakers | | `PveRecorded` / `PveRecordFailed` | `token` indexed, `tokens` | The graduation-time credit landed, or is still pending a retry | | `FeeRecipientSet` | `token` indexed, `recipient` indexed | Fires **only when the recipient differs from the creator** | | `CreatorFeeRoutedToCollector` | `token` indexed, `amount` | The creator leg failed over to the protocol on that trade | The full catalog, including the admin and bounty events, is in the [moto.fun event reference](/dev/reference/fun-addresses-config-events#5-events---integrator-reference). **Curve parameters are per coin.** The virtual reserves and the floor are fixed for a coin at launch, so read them once with `paramsOf(token)` on the curve and keep them. Do not price every coin from one global set. `LaunchParamsSet` marks the block from which new coins use a new parameter set. Coins that already exist keep theirs. **Key the `TokenCreated` decoder on `topic0`.** It is `0x6223a619ba904a9914dac4461d25f8f27e372c0a030791c3a5c012768765ef45`, the signature that carries `quoteToken`. See [which asset a coin is quoted in](/dev/fun/contracts#which-asset-a-coin-is-quoted-in), and [tracking one coin from launch to pools](/dev/integrations/moto-fun#tracking-one-coin-from-launch-to-pools) for the step-by-step event walk. **`Trade.isBuy` is not indexed.** You cannot topic-filter buys from sells. Decode and branch. **`Graduated` does not index the pair addresses.** A topic filter finds the graduation and misses the pools. The pairs are in the data. **`ethAmount` is fee-inclusive on buys and pre-fee on sells.** Pick one notion of volume and apply it consistently, or your buy and sell totals are measured differently. ### Lifecycle, with the back edge [#lifecycle-with-the-back-edge] `None → Trading → Frozen → Graduated` is the happy path, and `Frozen` is **not** terminal. If nobody graduates a frozen coin, selling reopens 15 minutes after the freeze and a sell returns it to `Trading`, with no event of its own beyond the `Trade`. Model that edge, or a stuck-frozen row will outlive the coin's actual state. Two ordering facts. In the transaction that freezes a coin, `FloorReached` is emitted **before** that buy's own `Trade`, so a pipeline that folds status in log order sees the freeze first. And `Frozen` is not instantaneous: a keeper normally graduates the coin within about a minute, which is several blocks in which the coin is frozen and visible as such. Both official indexers fold status from the latest surviving lifecycle event rather than storing a status column, so a rollback is a range delete with nothing to rebuild. That is worth copying. ### After graduation [#after-graduation] The coin becomes an ordinary Motoswap token: pair `Swap`, `Sync`, `Mint`, `Burn` and one `FeeRouter.Swap` per trade, exactly as above. Keep both pipelines pointed at the same coin id so the history joins rather than restarting. `Graduated` carries the two pair addresses in its data, and those are what you read the post-curve chart from. A graduated coin's long-range price history comes from `GET /dex/pools/{pair}/chart?range=30d` (hourly buckets) and `?range=1y` (daily), on the Motoswap API. The moto.fun backend's own candles cover the curve phase; the pool chart covers everything after it. See [pool chart ranges](/dev/api#pool-chart-ranges) for the bucket widths and the `minute` units, which are minutes rather than seconds. ## Reorg discipline [#reorg-discipline] Index by `(blockNumber, txHash, logIndex)`, keep raw events append-only, and derive aggregates from the raw table so a rollback is a range delete plus re-derive. That is how the official indexer survives reorgs, and the shape we would recommend to anyone: an accumulator you cannot rebuild from raw rows is a liability on any chain with reorgs. If you would rather not run your own pipeline, the REST API serves the indexed result of everything above with ETag caching. [Start there](/dev/integrations/ai-agents) and move to raw logs when you need something it does not serve. Response shapes are in the [API reference](/dev/reference/api-full-inventory). ## Where to go next [#where-to-go-next] * [Full event signatures](/dev/reference/addresses-config-events) with every field and topic. * [Watch events guide](/dev/guides/watch-events) for a running viem subscription setup. * [Aggregators](/dev/integrations/aggregators) if your scanner also prices routes. # Trading bots (/dev/integrations/trading-bots) One loop, five steps, against the `dex` phase surface. This page assumes a bot that holds keys and trades one wallet at a time; if you are routing other people's flow at scale, read [Aggregators](/dev/integrations/aggregators) instead, and if you only read data, [AI agents](/dev/integrations/ai-agents) is shorter. ## Deployment status [#deployment-status] Status is per contract, and the table is on [addresses](/dev/addresses). The DEX contracts and the moto.fun curve are deployed, and `GET /config` reports `launchPhase: "dex"`. The loop below needs that phase. Before the DEX opened the phase was `"vamp"`, every DEX address in `/config` was the zero address, and MOTO traded on its Uniswap V2 pair (`addresses.motoUniswapPair`) through the Uniswap V2 router. Do not take a `FeeRouter` address from anywhere but `/config`. ## The swap perimeter, for bots [#the-swap-perimeter-for-bots] Motoswap pairs use V2 math, but the swap leg is gated. This decides how a bot is built: * **`pair.swap` reverts for your contract.** The pair requires `factory.swapAllowed(msg.sender)`, and only the core router and the `FeeRouter` are admitted. A bot contract that transfers tokens to the pair and calls `swap` directly gets `MotoSwap: SWAPPER_NOT_ALLOWED`. * **The core router reverts too.** Every swap function on `MotoSwapRouter02` carries the same check on its caller, so an EOA or a bot contract calling it gets `MotoSwapRouter: SWAPPER_NOT_ALLOWED`. Its read functions (`getAmountsOut`, `getAmountsIn`) and its liquidity functions are public. * **Every swap goes through the `FeeRouter`**, from an EOA or from your own contract, and pays the full fee. There is no cheaper path to the pools. * **There are no flash swaps.** `pair.swap` reverts `MotoSwap: FLASH_SWAPS_CLOSED` on any non-empty `data`, for every caller. * **The `FeeRouter` is exact-input only.** There is no exact-output swap. Arbitrage and backrun contracts written for V2 forks need their swap call changed to a `FeeRouter` call and their profit math changed to include the fee. The full table is on [aggregators](/dev/integrations/aggregators#why-a-stock-v2-adapter-reverts). ## What you handle [#what-you-handle] Motoswap gives you quoting primitives, one public swap contract, and a REST API for everything indexed. Your bot owns the rest: 1. **Keys and nonces.** Every write is a normal EVM transaction from your wallet. Concurrent trades from one wallet need your own nonce management. 2. **An RPC you trust.** Quotes read reserves; a lagging RPC quotes a stale pool. `/config` does not give you one. Bring your own. 3. **Slippage policy.** `amountOutMin` is your only price protection. It is one line of BigInt math; the tolerance you pick is your call. 4. **Confirmation.** A submitted hash is not a fill. Read the receipt and the `FeeRouter.Swap` event before telling a user anything. ## The loop [#the-loop] ### Discover the deployment [#discover-the-deployment] ```ts import { erc20Abi, parseAbi, parseEventLogs, zeroAddress } from "viem"; // `client` is a viem public client on cfg.chainId, `wallet` a viem wallet // client holding the bot's key, `botWallet` its address. const cfg = await fetch("https://api.motoswap.org/config") .then((r) => r.json()); if (cfg.launchPhase !== "dex") throw new Error("AMM not live in this phase"); const { feeRouter, weth, usdc, usdt, motoToken } = cfg.addresses; ``` Cache it, refresh every few minutes (`Cache-Control` is 300s). A zero address means unset for the current phase; never hardcode. The throw above is the correct behavior when the phase is anything but `dex`. ### Build the candidate paths [#build-the-candidate-paths] With the factory's quote registry set, every Motoswap pair has a quote asset on one side, so the useful paths are the direct pair and one hop through each quote asset: ```ts const same = (a, b) => a.toLowerCase() === b.toLowerCase(); const paths = [ [tokenIn, tokenOut], ...[weth, usdc, usdt, motoToken] .filter((mid) => !same(mid, zeroAddress) && !same(mid, tokenIn) && !same(mid, tokenOut)) .map((mid) => [tokenIn, mid, tokenOut]), ]; ``` `GET /swap/candidates?tokenIn=&tokenOut=` returns the indexed pairs that connect two tokens if you would rather not probe the chain. Its response shape is in the [API reference](/dev/reference/api-full-inventory). ### Quote with the fee on the right side [#quote-with-the-fee-on-the-right-side] The `FeeRouter` has no quote function. The pair fee is inside the core router's `getAmountsOut`. The protocol fee (`feeRouter.protocolFeeBps()`) and any creator fee (`feeRouter.creatorFeeBps()`, on tokens with an active creator) come off the quote leg of the path, and which end that is varies by trade. Read all three from the chain. [Which leg pays](/dev/integrations/aggregators#which-leg-pays) has the rule, and [the quote recipe](/dev/integrations/aggregators#quote) has `quoteExactIn`, a function that applies it. Copy it into your bot. ```ts // quoteExactIn is the function from the aggregators page. const blockNumber = await client.getBlockNumber(); let best = { path: null, expectedOut: 0n }; for (const path of paths) { try { const expectedOut = await quoteExactIn(client, feeRouter, amountIn, path, blockNumber); if (expectedOut > best.expectedOut) best = { path, expectedOut }; } catch { // getAmountsOut reverts when a pair on the path does not exist. Skip it. } } if (!best.path) throw new Error("no route"); const toleranceBps = 100n; // 1% const minOut = (best.expectedOut * (10_000n - toleranceBps)) / 10_000n; ``` `amountOutMin` is checked against the net amount your wallet receives, after every fee. A `minOut` built from the raw `getAmountsOut` number on a fee-on-output route is unfillable. Quotes age. Re-quote if more than a few seconds pass between quote and submit; a Telegram confirmation step is more than a few seconds. ### Approve once, then swap [#approve-once-then-swap] ERC-20 input: approve **`FeeRouter`** (not the raw router, not the pair). ETH input: skip approval, use the payable entrypoint. ```ts await wallet.writeContract({ address: tokenIn, abi: erc20Abi, functionName: "approve", args: [feeRouter, amountIn], // or maxUint256 once, your policy }); const hash = await wallet.writeContract({ address: feeRouter, abi: parseAbi([ "function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline) returns (uint256[])", ]), functionName: "swapExactTokensForTokens", args: [amountIn, minOut, best.path, botWallet, BigInt(Math.floor(Date.now() / 1000) + 300)], }); ``` The deadline is a real protection for a bot: a transaction stuck in the mempool past it reverts `MotoSwapRouter: EXPIRED` instead of filling at a five-minute-old price. ETH in uses `swapExactETHForTokens` with `path[0]` set to WETH and the amount as `value`. A token that taxes its own transfers needs the `SupportingFeeOnTransferTokens` entrypoint of the same shape. ### Confirm from the event, not the hash [#confirm-from-the-event-not-the-hash] ```ts const receipt = await client.waitForTransactionReceipt({ hash }); if (receipt.status !== "success") throw new Error("swap reverted"); const swapLog = parseEventLogs({ abi: parseAbi([ "event Swap(address indexed user, address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut, address feeToken, uint256 feeAmount)", ]), logs: receipt.logs, })[0]; // swapLog.args.amountOut is the NET delivery, after all fees. // That is the number you report to the user. // swapLog.args.user is the caller of the FeeRouter (your bot wallet), not `to`. ``` The `FeeRouter` event and the pair event are both named `Swap`. Their signatures differ, so the ABI above only matches the `FeeRouter` one. ## Failure modes worth coding for [#failure-modes-worth-coding-for] | Symptom | Cause | Fix | | :----------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------- | | `InsufficientOutput()` or `MotoSwapRouter: INSUFFICIENT_OUTPUT_AMOUNT` | Quoted the wrong fee side, or the pool moved. The first comes from fee-on-output routes, the second from fee-on-input routes | Net the quote per the fee-side rule; re-quote closer to submit | | `MotoSwap: SWAPPER_NOT_ALLOWED` or `MotoSwapRouter: SWAPPER_NOT_ALLOWED` | You called the pair or the core router | Route through `FeeRouter`, always | | `MotoSwap: FLASH_SWAPS_CLOSED` | A contract passed callback data to `pair.swap` | There are no flash swaps | | `MotoSwapRouter: EXPIRED` | The deadline passed before inclusion | Re-quote and resubmit | | `NoQuoteSide` | Long-tail to long-tail path with no quote leg anywhere | Route through WETH or another quote asset | | Approval spent but swap fails | Approved the core router or the pair | Approvals go to `FeeRouter` | | Everything reverts on a fresh deployment | Phase is not `dex`, addresses are zero | Check `launchPhase` before trading | ## moto.fun curve trades [#motofun-curve-trades] A bonding [moto.fun](/dev/fun) coin is a different loop: no route, no approval, no deadline, and no pair. You call the curve directly with ETH. ```ts // Buy. ETH in, coins out. minTokensOut is your only protection. const [tokensOut] = await client.readContract({ address: cfg.addresses.launchpadLens, abi: parseAbi([ "function quoteBuy(address token, uint256 ethIn) view returns (uint256 tokensOut, uint256 grossUsed, uint256 refund)", ]), functionName: "quoteBuy", args: [token, ethIn], }); const hash = await wallet.writeContract({ address: cfg.addresses.launchpadCurve, abi: parseAbi(["function buy(address token, uint256 minTokensOut) payable returns (uint256)"]), functionName: "buy", args: [token, (tokensOut * 99n) / 100n], // 1% tolerance value: ethIn, }); ``` ```ts // Sell. No approval: the coin exempts the curve's own pull while it is unbonded. const ethOut = await client.readContract({ address: cfg.addresses.launchpadLens, abi: parseAbi(["function quoteSell(address token, uint256 tokensIn) view returns (uint256)"]), functionName: "quoteSell", args: [token, tokensIn], }); await wallet.writeContract({ address: cfg.addresses.launchpadCurve, abi: parseAbi([ "function sell(address token, uint256 tokensIn, uint256 minEthOut) returns (uint256)", ]), functionName: "sell", args: [token, tokensIn, (ethOut * 99n) / 100n], }); ``` Confirm from `LaunchpadCurve.Trade` in the receipt, not from the hash. `tokenAmount` is what the wallet received on a buy; `ethAmount` is fee-inclusive on buys and pre-fee on sells. ### What is different from a DEX swap [#what-is-different-from-a-dex-swap] | | Curve | DEX | | :---------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | | Entry point | `LaunchpadCurve.buy` / `.sell` | `FeeRouter.swapExact*` | | Approval | None. The coin exempts the curve's own pull on a sell while it is unbonded | `FeeRouter` | | Deadline | **None. There is no parameter** | Required | | Slippage | `minTokensOut` / `minEthOut` | `amountOutMin` | | Fee | `protocolFeeBps` plus `creatorFeeBps` on the curve, both sides, off the ETH leg. The contract initializes them to a 1.20% total. Both are owner-settable, so read them live | Pair fee plus protocol fee plus any creator fee, each read live | | Quote | `LaunchpadLens.quoteBuy` / `quoteSell` | Core router `getAmountsOut` plus the fee legs | Curve trades take no deadline, deliberately: there is no LP-side staleness to protect against, and the minimum-out is the whole price guard. A bot that expects one will not compile against the ABI, and a bot that leans on one for mempool protection needs to lean on `minTokensOut` instead. ### Failure modes specific to the curve [#failure-modes-specific-to-the-curve] | Revert | Cause | Fix | | :---------------- | :------------------------------------------------- | :-------------------------------------------------------------------- | | `NotTrading()` | The coin is frozen, graduated, or does not exist | Check `curves(token).status` first. `3` means trade the pools instead | | `Slippage()` | The minimum was not met, or the quote went stale | Re-quote closer to submit | | `BuysArePaused()` | A global or per-coin buy pause | Nothing to retry. Sells are unaffected | | `ZeroAmount()` | A buy with no value, or a sell of zero | Check inputs | | `NotFrozen()` | You called `graduate` on a coin that is not frozen | Poll `LaunchpadLens.graduationReadiness` | A buy that crosses the graduation line **fills partially and refunds the rest in the same transaction**. That is a success, not a failure. Read the `Trade` event rather than assuming you got what you paid for. ### Sniping constraints, or the lack of them [#sniping-constraints-or-the-lack-of-them] There is no launch block delay, no cooldown, no max buy, and no per-wallet cap. The one thing that matters for a sniper is how a gasless launch materializes. It is a signed intent submitted with `buyWithIntent`, and the intent's `submitter` field decides who can do what: * **Open intent (`submitter` is the zero address).** Anyone can submit it, but only with zero value. That creates the coin and buys nothing. Sending ETH with an open intent reverts `ValueNeedsSubmitter()`. * **Bound intent (`submitter` is set).** Only that address can submit it, with or without value. Anyone else reverts `NotTheSubmitter()`. So a funded first buy through `buyWithIntent` always comes from the named submitter. You cannot attach your own ETH to someone else's intent. Once the coin exists, `buy` is open to everyone. Rebuild the intent tuple verbatim from `GET /intents/{address}` on the app backend; any field you retype wrong fails signature recovery. ### Graduating for the bounty [#graduating-for-the-bounty] `graduate(token)` is permissionless and pays a bounty out of the curve's ETH. Poll `LaunchpadLens.graduationReadiness(token)`, which never reverts and classifies every failure: ``` (Readiness regime, uint64 readyAt, uint256 minMotoOut, uint256 quotedOut, uint256 deviationBps, uint256 bountyNow, uint256 sellsOpenAt) ``` `Ready` means send it. `TooFresh` carries `readyAt`: sleep until then rather than retrying blind. `OutOfBand` means the MOTO price reference is outside its band and the transaction would revert. If your send loses the race, it reverts and the coin is graduated anyway, which costs you gas and nothing else. If the bounty push to your address fails, it is held and you take it with `claimBounty(to)`. ## Reading the account back [#reading-the-account-back] Balances are chain reads. Everything derived is one REST call each, ETag-cached so polling is nearly free: `/points/{address}` (Points and rank), `/rakeback/{address}` (the Rakeback state for that address), `/token/{address}/price` for price display. Send `If-None-Match`; expect `304`. Conventions: [caching guide](/dev/guides/caching-and-etags). ## Where to go next [#where-to-go-next] * [Aggregators](/dev/integrations/aggregators) for the full `FeeRouter` interface, the fee model in basis points and the quote recipe. * [Scanners and indexers](/dev/integrations/scanners) for decoding pair logs and BigInt price math. * [Claim Rakeback](/dev/guides/claim-rakeback) for how an address reads and claims what it is owed. * [API inventory](/dev/reference/api-full-inventory) for every read endpoint. # Addresses, config and events (/dev/reference/addresses-config-events) ## 1. Deployed Contract Addresses [#1-deployed-contract-addresses] ### Ethereum (chain 1) [#ethereum-chain-1] **Public API base URL:** `https://api.motoswap.org`. Deployment status is per contract, never per network. MOTO, the launch farm (`VampChef`), `UniswapZap`, `MotoStaking`, `RewardVestingEscrow`, `TreasuryVesting`, the Motocat collection and `MotocatStaking` went out first. The DEX contracts (factory, router, `FeeRouter`, `Collector`, `QuoteAssetRegistry`, `RakebackV2`, `MasterChef`, zap, the creator fee pair, `PveVault`, `BuybackBurner`, the moto.fun contracts) went out with the DEX. Contracts in the source tree that are not deployed are called out where they appear below. Fetch addresses from **`GET /config`** rather than hard-coding them. The status table, with every address the site pins and every key that reads `0x0`, lives on [Contract Addresses](/dev/addresses), so one page owns it and it cannot drift. This page documents the `/config` keys themselves: `factory`, `router`, `permit2`, `motoStaking`, `motoToken`, `masterChef`, `tokenLauncher`, `feeRouter`, `collector`, `quoteRegistry`, `rakeback`, `zap`, `vaultCompounder`, `usdc`, `usdt`, `weth`, `creatorFeeRegistry`, `creatorFeeVault`, `motocatNft`, `motocatStaking`, `pveVault`, `pveVaultV2`, `buybackBurner`, `buybackDistributor`, `rewardVestingEscrow`, `treasuryVesting`, `launchpadCurve`, `launchpadLens`, `motoUniswapPair`, `vampChef`, `uniswapZap`, `uniswapV2Router`, `uniswapV2Factory`, plus the vestigial `vaults[0..3]` array (`MotoStaking` holds every lock option in one contract). **Not exposed by `/config`.** `Snapshotter` is the only contract in the source tree with no key on the endpoint. There is no getter for it; if it ships, its address will be published separately. An address that reads back as `0x0` from `/config` means the contract is unset for that chain in the current `launchPhase`, or its feature is off. It does not mean the key is missing. Never send a transaction to it. `tokenLauncher` belongs to a retired contract and stays `0x0`. `rewardVestingEscrow` also has an on-chain getter, `vestingEscrow()` on the chef, which is worth preferring since it survives a redeploy that `/config` has not caught up with. Read `/config.addresses.masterChef` rather than assuming what is behind it. While `launchPhase` was `vamp` it held the same address as `vampChef`, and the contract behind that key is a `VampChef` (one-step withdraw, its own event set, listed below). The seeded MOTO/WETH pool for the current phase is served as `motoUniswapPair` (during launch farming it was a Uniswap V2 pair). Derive any other pair from `factory.getPair(tokenA, tokenB)` rather than hard-coding it. `/config` also carries two moto.fun keys, `launchpadCurve` and `launchpadLens`. Both read back as `0x0` unless that surface is enabled for the chain, so gating on the address alone is correct. The top-level `motofunEnabled` / `motofunUrl` fields flag the same surface. Contracts published under Motoswap-adjacent repos are sibling experiments, not the Motoswap DEX. If an address is not in `/config` and not discoverable from a `/config` contract's getters, do not integrate against it. ### Local development [#local-development] Local addresses are not listed here. The local stack described below is Motoswap's own development environment and is not public. As an outside integrator, fork Ethereum mainnet with your own node tooling and use the chain 1 addresses from `GET https://api.motoswap.org/config`. ### Discovering addresses at runtime [#discovering-addresses-at-runtime] The recommended integrator path: call **`GET /config`** on the backend (`https://api.motoswap.org/config`). It returns the current chain's contract addresses, so you never hard-code them. The answer below is a complete chain `1` capture taken on 2026-09-28 after the DEX opened, with `launchPhase` at `dex`. Any `0x0000000000000000000000000000000000000000` is a contract that is not deployed. Call the endpoint for the values you use: ```json { "chainId": 1, "addresses": { "factory": "0x81c9cbc47d700da1777abd831d8da3f526dfae24", "router": "0x6f0b6a0d84c3cff6f273d5294cda147885225d5e", "permit2": "0x000000000022d473030f116ddee9f6b43ac78ba3", "motoStaking": "0xce88f2c6b49efbb92555ee5475311f0e250c2528", "motoToken": "0xbd965230588eaa536de6aa45e8ebbc01638535e0", "masterChef": "0x939f348b6658ce4db7cde088341b00de341fe085", "tokenLauncher": "0x0000000000000000000000000000000000000000", "feeRouter": "0x9f846ef584fd44d075b5e8df00ddb4416da61a80", "collector": "0xc13307272bbf73f2191ce57d0fb714c2a9200cf3", "quoteRegistry": "0xfb8ca710b9b242e9f5027272592ef4c19d37e200", "rakeback": "0x89e2e1819fc373e3cd840a0ceb2ef1eae79e2db8", "zap": "0x26bffd661c3d97b3285afa2a9f05232526a89129", "vaultCompounder": "0x0000000000000000000000000000000000000000", "usdc": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "usdt": "0xdac17f958d2ee523a2206206994597c13d831ec7", "weth": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "creatorFeeRegistry": "0x6cdec7f49036ccf1bba940011040fa044cfe6d08", "creatorFeeVault": "0x8cc7c8e76d9698e5b1026c720a94c1807e613e1f", "motocatNft": "0x0b024b3d4ad38bff1acd303cd308d2c49c936e0a", "motocatStaking": "0x11890ee9ee6099ac33fd4dfb702f8e8f6d651f17", "pveVault": "0x84303a885717c4dd9effbdd2179bbe4b01756740", "pveVaultV2": "0x0000000000000000000000000000000000000000", "buybackBurner": "0x85b5a2800d7e2d948f21cb8f6e610d2c9b19ae3d", "buybackDistributor": "0x0000000000000000000000000000000000000000", "rewardVestingEscrow": "0xb94bfadc535d96462854175d776afc168196976b", "treasuryVesting": "0x5730abdf3c06c37940fb8b5291277e62d2c15278", "launchpadCurve": "0xb65b67e986d7b097652920d72202f2c78319bc2c", "launchpadLens": "0xb631697124c8e6bb6c221cc017e47d4168bd3411", "motoUniswapPair": "0xad1a21f61d653c6101b92c335f61140459c81c79", "vampChef": "0x51e648f08a9a08a724591938d9cbc483c809aca4", "sustainChef": "0x939f348b6658ce4db7cde088341b00de341fe085", "uniswapZap": "0x532bfb5b8c9e3cef20d36910b90cfbd919b18c31", "uniswapV2Router": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d", "uniswapV2Factory": "0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f", "vaults": [ "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000" ] }, "splitSwapEnabled": true, "motofunEnabled": true, "bridgeMotoEnabled": false, "motofunUrl": "https://moto.fun", "pveComboEnabled": false, "launchPhase": "dex", "dexCutover": false, "misconfigured": [] } ``` There is no `rpcUrl` field: bring your own RPC endpoint for chain `1`. | Top-level field | Type | Meaning | | ------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `chainId` | number | The chain these addresses live on. | | `addresses` | object | The keys listed above. Lowercase `0x` strings; `vaults` is an array of four. | | `launchPhase` | `"prelaunch"`, `"vamp"` or `"dex"` | `vamp`: the farm is deployed and the DEX is not, which is how it read before the DEX opened. `dex`: the DEX is deployed. | | `splitSwapEnabled` | boolean | API configuration flag read by the Motoswap app: whether it submits a split fill as one `swapExact*Split` transaction (`true`) or as one swap per leg (`false`). Not an on-chain switch: the `FeeRouter` split entrypoints have no enable flag. | | `motofunEnabled` | boolean | Whether the moto.fun surface is on. When `false`, `launchpadCurve` and `launchpadLens` read `0x0`. | | `motofunUrl` | string | The moto.fun site URL, or `""`. | | `bridgeMotoEnabled` | boolean | Whether MOTO bridging is switched on in the app. | | `pveComboEnabled` | boolean | Dead flag. Nothing reads it. | | `misconfigured` | string\[] | Names of transaction-target addresses that are unset for the current phase. Empty in normal operation. | *** ## 2. Chain / Network Config [#2-chain--network-config] ### Supported chain IDs [#supported-chain-ids] Ethereum mainnet is chain `1`, and it is the only chain an integrator targets. The client also accepts a local development chain id; `NEXT_PUBLIC_CHAIN_ID` is validated against a fixed allowlist, so no other value is accepted. Read the chain you are pointed at from `/config.chainId` rather than assuming one. ### Public endpoints [#public-endpoints] * **REST API base URL:** `https://api.motoswap.org` * **RPC URL:** bring your own for chain `1`. `/config` does not return one. Do NOT ship an API-keyed URL in a browser bundle. * **CORS:** Simple GET requests work from a browser on any origin. Requests that need a CORS preflight, such as one where your script sets `If-None-Match`, only pass for the Motoswap app origin, so do conditional polling and any write from a server. * **Endpoint reference:** [API](/dev/api). * **Chain head heartbeat:** `GET /chain/head` (block number + timestamp, 1s max-age). ### WebSockets [#websockets] No WebSocket subscriptions are exposed to integrators. Use the REST API (cache-friendly, ETag-versioned; 304s are near-free) or watch on-chain events directly via your own RPC / event-log provider. ### Fill-in reference (from source) [#fill-in-reference-from-source] The environment variables that decide which chain and backend a client talks to: ``` NEXT_PUBLIC_CHAIN_ID # the target chain id NEXT_PUBLIC_RPC_URL # browser/wallet-safe (keyless) RPC_URL # server-side (may be keyed; not shipped) NEXT_PUBLIC_API_BASE_URL # absolute backend URL for the chain NEXT_PUBLIC_SENTRY_DSN # optional, disabled locally ``` *** ## 3. Domain / Wire Types [#3-domain--wire-types] All branded types live in `packages/domain/src/*.ts` and are re-exported from `@motoswap/shared/domain`. The rule: one runtime type end-to-end (DB, backend, frontend, chain), a separate WIRE type only when the runtime type is not JSON-serialisable (i.e. `bigint`). | Domain type | Runtime | Wire (JSON) | Zod schema | Example | | ----------- | --------------------------------------------------------------------- | -------------------------------------- | --------------- | -------------------------------- | | `Address` | `` `0x${string}` `` (always lowercase) | string | `addressSchema` | `"0xa0b86991…eb48"` | | `TxHash` | `` `0x${string}` `` (32-byte) | string | `txHashSchema` | `"0x00…01"` | | `Wei` | `bigint` | string (decimal) via `weiWire` | `weiSchema` | `"1000000000000000000"` | | `Uint256` | `bigint` | string (decimal) | `uint256Schema` | `"1000000000000000000"` | | `Usd` | `bigint` (µ-USD, 6dp integer) | string (decimal dollars) via `usdWire` | `usdSchema` | `"2500.50"` (= 2 500 500 000 µ$) | | `Seconds` | `number` (Unix seconds) | number | `secondsSchema` | `1723456789` | | `Percent` | `number` (float, e.g. `10.5` = +10.5%) | number | `percentSchema` | `10.5` | | `Apr` | `number` (non-negative **integer basis points**, `1250` = 12.50% APR) | number | `aprSchema` | `1250` | | `Ratio` | `bigint` (18-dp fixed point, `1.0` = `10^18`) | string (decimal) via `ratioWire` | `ratioSchema` | `"0.000300"` | There is no `Bps` brand. Basis-point fields (`swapFeeBps`, `protocolFeeBps`, `toleranceBps`, `priceImpactBps`, and so on) are plain integer `number`s on the wire, or `bigint` where they feed SDK amount math. ### Schema shape (example) [#schema-shape-example] ```ts // Address - lowercase, 0x-prefixed, hex-only, length 42 const addressSchema = z .string() .transform((v) => v.toLowerCase()) .refine((v) => v.startsWith("0x") && v.length === 42 && /^[a-f0-9]+$/.test(v.slice(2))) .brand("Address") .meta({ "x-type": "Address" }); // Wei - accepts bigint | string | number at parse time; runtime is bigint const weiSchema = z .union([z.bigint(), z.string(), z.number()]) .transform((v) => (typeof v === "bigint" ? v : BigInt(v))) .refine((n) => n >= 0n && n <= 2n ** 256n - 1n) .brand("Wei"); ``` ### Wire conversion helpers [#wire-conversion-helpers] * `usdToWire(usd: Usd): string` - bigint µ$ → decimal dollar string. * `usdFromWire(s: string): Usd` - decimal dollar string → bigint µ$. * `weiToString(wei: Wei): string` - bigint → decimal string. * `weiSchema.parse(v)` - accepts bigint / string / number; returns branded `Wei`. ### `null` policy for numeric fields [#null-policy-for-numeric-fields] * `usdValue`, `tvlUsd`, `volume*Usd`, `fees*Usd` - **`"0"` when zero, never `null`.** (Coerced at the handler edge to keep client math trivial.) * `priceUsd`, `aprBps`, `feeAprBps`, delta `*Pct`s, `rank`, `decimals` - kept as **`null` when genuinely unknown**. Rendering `0` would mislead (a guessed `18` for unknown decimals mis-scales a token by `10^(18-d)`). *** ## 4. Fee Schedule & Rakeback Constants [#4-fee-schedule--rakeback-constants] The constants in this section's fee tables are read from the Solidity source the contracts deploy from. Where a value is settable, read it live from the contract rather than trusting a number here. ### Protocol-level fee (`FeeRouter`) [#protocol-level-fee-feerouter] | Constant | Value | Meaning | | ------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `LAUNCH_PROTOCOL_FEE_BPS` | 70 | The launch fee (0.70%). A reference constant: `initialize` does not apply it. | | `MAX_PROTOCOL_FEE_BPS` | 100 | Owner-settable ceiling (1.00%). | | `protocolFeeBps` (live) | Read it from the contract | `protocolFeeBps` is an `initialize` argument with no default, capped at `MAX_PROTOCOL_FEE_BPS`, and owner-settable afterwards. It launched at 70. Read it live from the `feeRouter` address `/config` serves rather than trusting a number here. See [the fee model](/dev/integrations/aggregators#the-fee-model-in-basis-points). | | `BPS_DENOM` | 10 000 | Basis-point denominator. | | `MAX_SPLIT_LEGS` | 4 | Max legs in one `swap*Split` call. | | Fee side | Quote asset only | Charged on the quote leg per `QuoteAssetRegistry.quoteRank` (WETH > USDT > USDC > MOTO). Both-quote routes charge the higher-ranked leg, and a tie goes to the output. A route with no quote asset at either end takes the fee at the first quote asset inside the path. `NoQuoteSide()` reverts only when no asset anywhere in the path is a quote asset. See [which leg pays](/dev/integrations/aggregators#which-leg-pays). | ### Pair swap fee (`MotoSwapFactory`) [#pair-swap-fee-motoswapfactory] | Constant | Value | Meaning | | ----------------------- | ----- | ---------------------------------------------------------------------- | | `MAX_SWAP_FEE_BPS` | 200 | Hard ceiling on `swapFeeBps` (2.00%). | | `swapFeeBps` (launched) | 30 | 0.30% LP fee (entirely to LPs; `feeTo` is zero, so `_mintFee` no-ops). | **Total swap cost at the planned launch values:** 0.30% LP per hop + 0.70% protocol = **1.00%** on a one-hop route with no registered moto.fun token, when `protocolFeeBps == 70`. Each path endpoint with an active creator adds the [creator fee](#creator-fee-optional-per-token-via-creatorfeeregistry). While `protocolFeeBps` is 0 the protocol part is zero. ### Collector split (`Collector.buckets`) [#collector-split-collectorbuckets] Default seed on init: **2 : 2 : 2 : 1** (owner-reweightable). | Bucket | Recipient | Weight | Notify? | | ------ | ------------------------------ | ------ | ------------------------------------ | | 0 | MotoStaking (staking revshare) | 2 | Yes (`IStakingRewards.notifyReward`) | | 1 | Treasury | 2 | No | | 2 | Rakeback | 2 | No | | 3 | Buyback and burn | 1 | No | Rounding dust accrues to the last bucket. Held earmarks (a bucket whose payout reverted) are excluded from `distributable(token)` so a failing bucket never re-weights the healthy ones. ### Rakeback [#rakeback] | Constant | Value | | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `CLAIM_WINDOW` | 30 days, measured from a slice's activation | | `MIN_ACTIVATION_DELAY` / `MAX_ACTIVATION_DELAY` | Bounds on the delay between `postRoot` and `activate`. Values are not published here; read them, and the current `activationDelay()`, from the contract. | | `MAX_TOKENS_PER_EPOCH` | 8 | | Vest schedule (baked into the leaf at post time) | Week 0: 50% of R\[week] · Week 1: +25% · Week 2: the remainder | | Epoch length | Sunday-anchored week - `epoch = floor((seconds − 259200) / 604800)` | | Quote assets | WETH, USDT, USDC, MOTO | Payout is a merkle claim. One root per weekly epoch covers every token, posted by `postRoot` (poster-gated, capped at the token's measured `Collector` inflow), then opened per `(epoch, token)` slice by the permissionless `activate`. A leaf is `keccak256(abi.encodePacked(index, account, token, amount))` and is all-or-nothing: one bitmap bit, full amount, no partial claim and no cumulative counter. Unclaimed remainders leave via `expireRoot` after the window and become sweepable to the treasury. ### Creator fee (optional; per-token via `CreatorFeeRegistry`) [#creator-fee-optional-per-token-via-creatorfeeregistry] | Constant | Value | | ---------------------- | ------------------------------------------------------------------------------------- | | `MAX_CREATOR_FEE_BPS` | 50 (0.50% ceiling) | | `creatorFeeBps` (live) | 0 to 50 (owner-settable); the deploy script writes 20 and the owner sets 30 at launch | Applied on the same quote leg as the protocol fee. Both fees are subject to the caller's `amountOutMin` (**net** enforced). ### VampChef emission (launch farming) [#vampchef-emission-launch-farming] | Constant | Value | Notes | | ----------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | `DURATION` | 14 days | One flat emission window. `rewardPerSecond = rewardBudget / DURATION`. | | `emissionStart()` / `emissionEnd()` | read from the contract | Unix seconds. Accrual is zero outside the window. One campaign, no restart. | | Withdraw cooldown | none | `withdraw(pid, amount)` settles rewards and returns principal in one call. No `claimWithdrawal`. | | Deposit fee | none | `PoolAdded` and `PoolSet` carry no `depositFeeBps`. | | `MAX_START_DELAY` | 90 days | Bounds a scheduled `startTime`. | | `HARVEST_GRACE` | 30 days | Window after `emissionEnd` before `recoverUndistributedRewards` opens. | | `MAX_POOLS` | 500 | Hard cap. | | `ACC_PRECISION` | `1e12` | Reward-per-share scaler. | | Reward split | 50% liquid / 50% vested (`RewardVestingEscrow`, 180 days) | | ### MasterChef emission [#masterchef-emission] Deployed; no emission campaign is running and none is planned. These are the contract's constants, for reference. | Constant | Value | Notes | | -------------------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `HALVING_PERIOD` (default) | 42 days | Time-based, not block-based | | `NUM_PERIODS` (default) | 8 | \~336 days total | | `MAX_START_DELAY` | 90 days | Bounds scheduled `startTime` | | `HARVEST_GRACE` | 30 days | Post-`emissionEnd` window before `recoverUndistributedRewards` | | `COOLDOWN` | 24 hours | Withdraw cooldown while the pool earns. None when no campaign is running or the pool's weight is 0. Withdraw is queue, then claim | | `MAX_DEPOSIT_FEE_BPS` | 1000 (10%) | Per-pool ceiling | | `MAX_POOLS` | 500 | Hard cap | | `ACC_PRECISION` | `1e12` | Reward-per-share scaler | | Reward split | 50% liquid / 50% vested (`RewardVestingEscrow`, 180 days) | | ### MotoStaking locks [#motostaking-locks] | Duration | boost bps | Multiplier | | --------- | --------- | ---------- | | 3 months | 0 | 1.0x | | 6 months | 10 000 | 2.0x | | 12 months | 30 000 | 4.0x | | 24 months | 70 000 | 8.0x | `effectiveStake = amount × (10 000 + boostBps) / 10 000`. Principal vests linearly over the lock; `requestUnstake` moves the vested amount into a **30-day linear drip** (`UNSTAKE_DRIP`). ### Motocat staking [#motocat-staking] `MotocatStaking` V3 distributes no rewards on its own, so it has no reward constants. It tracks a per-wallet staked cat count as a checkpoint history: each `Checkpoint` packs a `uint32` block number and a `uint224` value into one slot, so a count can be read at any historical block. A write above either bound reverts with `CheckpointOverflow`; two writes in the same block overwrite rather than append. ### Rakeback vest math (concrete example) [#rakeback-vest-math-concrete-example] * A user earns `R` of a quote token in epoch 42. * **Epoch 42:** 0.5 × R claimable this week. * **Epoch 43:** +0.25 × R cumulative. * **Epoch 44:** +0.25 × R cumulative - total 100%. * **After the window:** each slice can be claimed for 30 days from its own activation, not from the epoch's end. Past that, anyone can call `expireRoot(epoch, token)`, the unclaimed remainder leaves the liability record, and the owner can `sweep` that surplus to the treasury. `sweep` takes no signature and can never touch a live claim. Each epoch settles independently - there's no FIFO across weeks. ### Points ledger (indexer-side) [#points-ledger-indexer-side] * Every fee-paying swap is scored by the season engine into `points_events`. The scoring internals (rates, curves, tiers, milestone bases, boost formula) are **not published**; do not reverse-engineer or state them. * **Referral rate: 10%** of the referee's points to the referrer, one level. * **Motocat boost:** the wallet's STAKED Motocat count (staked in `MotocatStaking`; a merely-held cat does not count), read at the block before the swap. Formula unpublished. * Balances publish once a day at the 00:00 UTC drop; per-swap deltas are never observable via the API. *** ## 5. Events - Integrator Reference [#5-events---integrator-reference] Grouped by contract. Watch them through your own RPC or event-log provider. Signatures below are the Solidity form, checked against the contract source. Hash the canonical form (types only, no names, no `indexed`) with `keccak256` to get `topic0`, or run `cast sig-event ""`. Filter on the address `/config` serves for each contract, never on a hard-coded one. The deployed implementations can trail the source: where an event below is in the source but not yet in the deployed code, the text says so. ### VampChef (launch farming, ended) [#vampchef-launch-farming-ended] ```solidity event Deposit(address indexed user, uint256 indexed pid, uint256 amount); event Withdraw(address indexed user, uint256 indexed pid, uint256 amount); event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount); event RewardPaid(address indexed user, uint256 indexed pid, uint256 amount); event RewardShortfall(address indexed user, uint256 indexed pid, uint256 owed, uint256 paid); event RewardForfeited(address indexed user, uint256 indexed pid, uint256 amount); event PoolAdded(uint256 indexed pid, address indexed lpToken, uint256 allocPoint, uint256 totalAllocPoint); event PoolSet(uint256 indexed pid, uint256 allocPoint, uint256 totalAllocPoint); event PoolUpdated(uint256 indexed pid, uint256 accRewardPerShare, uint256 lastRewardTime); event EmissionStarted(uint256 budget, uint256 rewardPerSecond, uint64 startTime, uint64 endTime); event RewardTokenSet(address rewardToken); event TrustedZapperSet(address trustedZapper); event UndistributedRewardsRecovered(address to, uint256 amount); ``` `RewardShortfall` is in the source but not in the deployed implementation, which has no such `topic0` in its bytecode. It arrives with a contract upgrade. Every other event above is in the deployed code. Emitter: the `vampChef` address from `/config`. It is not `MasterChef`: there is no `WithdrawQueued` or `WithdrawClaimed`, and `PoolAdded` / `PoolSet` / `EmissionStarted` have different arguments, so their `topic0` values differ from `MasterChef`'s. | Event | `topic0` | | -------------------------------------- | -------------------------------------------------------------------- | | `Deposit(address,uint256,uint256)` | `0x90890809c654f11d6e72a28fa60149770a0d11ec6c92319d6ceb2bb0a4ea1a15` | | `Withdraw(address,uint256,uint256)` | `0xf279e6a1f5e320cca91135676d9cb6e44ca8a08c0b88342bcdb1144f6511b568` | | `RewardPaid(address,uint256,uint256)` | `0xd6f2c8500df5b44f11e9e48b91ff9f1b9d81bc496d55570c2b1b75bf65243f51` | | `PoolUpdated(uint256,uint256,uint256)` | `0x17b8644f386d1c7c7138ef98b3c8035622bbe94d7be9b26f71d2654a547c2943` | `RewardPaid.amount` is the full harvest. Half of it moves to the wallet and half is deposited into `RewardVestingEscrow`, which emits its own `Deposited`. ### UniswapZap [#uniswapzap] ```solidity event Zapped( address indexed sender, address indexed pair, address tokenIn, uint256 amountIn, uint256 swapAmount, uint256 lpAmount ); ``` Emitter: the `uniswapZap` address from `/config`. A zap that stakes also produces a `VampChef.Deposit` whose `user` is the zapping wallet, not the zap contract. ### MotoToken [#mototoken] ```solidity event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); ``` Standard ERC-20 events. MOTO has 18 decimals, a fixed supply, ERC-2612 `permit`, no owner and no mint. The token also emitted a handful of one-time events when it launched. They do not recur, and `tradingOpen()` returns `true`, so transfers are unrestricted. ### FeeRouter (canonical trade event) [#feerouter-canonical-trade-event] ```solidity event Swap( address indexed user, address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut, address feeToken, // quote asset the fee was charged in uint256 feeAmount // exact protocol fee (quote-side) ); event ProtocolFeeSet(uint256 protocolFeeBps); event CreatorFee( address indexed token, address indexed creator, address quoteAsset, uint256 amount ); event Permit2Set(address permit2); event CreatorFeeConfigSet(address registry, address vault, uint256 creatorFeeBps); event Rescued(address indexed token, address indexed to, uint256 amount); ``` `FeeRouter.Swap` is the **canonical trade** - one row per fee-paying swap (multi-hop = 1 event). The pair `Swap` fires N times for N hops. `user` is `msg.sender`, the payer. It is never the `to` recipient. The two coincide for an ordinary wallet swap and diverge when a contract calls `FeeRouter`: an aggregator's settlement contract is `user` on every swap it routes, and Points, Rakeback and referral attribution all follow `user`. ### MotoSwapPair [#motoswappair] ```solidity event Swap( address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to ); event Mint(address indexed sender, uint256 amount0, uint256 amount1); event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); event Sync(uint112 reserve0, uint112 reserve1); event Transfer(address indexed from, address indexed to, uint256 value); // LP token event Approval(address indexed owner, address indexed spender, uint256 value); ``` `Sync` fires after every state-changing pair call and is the canonical reserves snapshot for indexers. ### MotoSwapFactory [#motoswapfactory] ```solidity event PairCreated(address indexed token0, address indexed token1, address pair, uint256 allPairsLength); event SwapFeeSet(uint256 swapFeeBps); event QuoteRegistrySet(address indexed quoteRegistry); event SwapAllowedSet(address indexed account, bool allowed); event FeeToSet(address indexed previousFeeTo, address indexed newFeeTo); event FeeToSetterTransferStarted(address indexed previousFeeToSetter, address indexed newFeeToSetter); event FeeToSetterTransferred(address indexed previousFeeToSetter, address indexed newFeeToSetter); ``` ### MasterChef [#masterchef] ```solidity event PoolAdded(uint256 indexed pid, address indexed lpToken, uint256 allocPoint, uint16 depositFeeBps, uint256 totalAllocPoint); event PoolSet(uint256 indexed pid, uint256 allocPoint, uint16 depositFeeBps, uint256 totalAllocPoint); event PoolUpdated(uint256 indexed pid, uint256 accRewardPerShare, uint256 lastRewardTime); event Deposit(address indexed user, uint256 indexed pid, uint256 amount); event WithdrawQueued(address indexed user, uint256 indexed pid, uint256 amount, uint64 cooldownEnd); event WithdrawClaimed(address indexed user, uint256 indexed pid, uint256 amount); event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount); event RewardPaid(address indexed user, uint256 indexed pid, uint256 amount); event RewardShortfall(address indexed user, uint256 indexed pid, uint256 owed, uint256 paid); event RewardForfeited(address indexed account, uint256 indexed pid, uint256 amount); event EmissionStarted(uint256 totalReward, uint256 initialPeriodReward, uint64 startTime, uint64 endTime); event EmissionScheduleSet(uint256 halvingPeriod, uint256 numPeriods); event FeeRecipientUpdated(address feeRecipient); event RewardTokenSet(address rewardToken); event VestingEscrowSet(address vestingEscrow); event TrustedZapperSet(address trustedZapper); event UndistributedRewardsRecovered(address to, uint256 amount); ``` ### MotoStaking [#motostaking] ```solidity event Staked(address indexed account, uint256 amount, uint8 durationOption); event Imported(address indexed account, uint256 amount, uint8 durationOption); event Appended(address indexed account, uint256 added, uint256 total, uint8 durationOption, uint64 start); event RewardNotified(address indexed token, uint256 amount, uint256 accRewardPerEffShare); event RewardForwardedToTreasury(address indexed token, uint256 amount); event Claimed(address indexed account, address indexed token, uint256 amount); event ClaimSkipped(address indexed account, address indexed token); event ClaimedAsMoto(address indexed account, address indexed token, uint256 rewardIn, uint256 motoOut); event UnstakeRequested(address indexed account, uint256 amount, uint64 start); event Unstaked(address indexed account, uint256 amount); event BoostSet(uint8 durationOption, uint256 bps); event RewardNotifierSet(address rewardNotifier); event FeeRouterSet(address feeRouter); event VestAnchorSet(uint64 anchor); event RewardTokenAdded(address indexed token); event RewardTokenRemoved(address indexed token); event RewardTokenPurged(address indexed token); ``` Every event above is in the deployed implementation. Two of them stay silent until a contract upgrade lands: the deployed `MotoStaking` has no reward notifier registered, and its `feeRouter()` is a `0x…dEaD` placeholder, so `claimAsMoto` reverts and `ClaimedAsMoto` and `RewardNotified` do not fire. ### RakebackV2 [#rakebackv2] ```solidity event RootPosted(uint256 indexed epoch, bytes32 root, uint64 postedAt); event SlicePosted(uint256 indexed epoch, address indexed token, uint256 declaredTotal); event Activated(uint256 indexed epoch, address indexed token, uint64 activatedAt); event Claimed( uint256 indexed epoch, address indexed token, uint256 index, address account, uint256 amount ); event ClaimedAsMoto( address indexed user, address indexed token, uint256 indexed epoch, uint256 amountIn, uint256 motoOut ); event Expired(uint256 indexed epoch, address indexed token, uint256 remainder); event PendingSliceExpired(uint256 indexed epoch, address indexed token, uint256 declared); event RootCancelled(uint256 indexed epoch); event InflowSynced(address indexed token, uint256 delta, uint256 lifetimeInflow); event Swept(address indexed token, uint256 amount); event PosterSet(address poster); event GuardianSet(address guardian); event ActivationDelaySet(uint64 activationDelay); event TreasurySet(address treasury); event FeeRouterSet(address feeRouter); ``` `RakebackV2.Claimed` has 2 indexed args (epoch, token) and 5 total; `MotoStaking.Claimed` has 2 indexed (account, token) and 3 total. Different signatures, so their `topic0` values do not collide - but both are named `Claimed`, so filter by emitter address, not by name. Note that the claimant is the un-indexed `account` field, so you cannot filter claims by wallet in a topic. ### MotocatStaking (NFT staking) [#motocatstaking-nft-staking] ```solidity event Staked(address indexed wallet, uint256[] tokenIds); event Unstaked(address indexed wallet, uint256[] tokenIds); event Seeded(address indexed wallet, uint256[] tokenIds); event SeedingClosed(); event SeederSet(address indexed seeder); event SeedSourceSet(address indexed seedSource); event OutboxAdded(address indexed outbox); event OutboxRemoved(address indexed outbox); event Broadcast(address indexed wallet, uint256 stakedCount); event BroadcastFailed(address indexed outbox, address indexed wallet); event ERC20Rescued(address indexed token, address indexed to, uint256 amount); event ERC721Rescued(address indexed token, address indexed to, uint256 tokenId); event ETHRescued(address indexed to, uint256 amount); event UpgradesRenounced(); ``` V3 pays no rewards, so it emits no `Reward*` events. `Staked` / `Unstaked` are the only events that move a wallet's cat count; `Broadcast` mirrors that count to a registered outbox and `BroadcastFailed` records an outbox that reverted without blocking the stake. ### CreatorFeeRegistry / CreatorFeeVault [#creatorfeeregistry--creatorfeevault] ```solidity // Registry event TokenRegistered(address indexed token, address indexed creator, address indexed registrar); event CreatorSet(address indexed token, address indexed creator); event CreatorSetBySelf(address indexed token, address indexed previousCreator, address indexed creator); event TokenDisabled(address indexed token, bool disabled); event RegistrarSet(address indexed registrar, bool allowed); // Vault - emits Accrued on every deposit, Claimed on every payout, DepositorSet on config event Accrued(address indexed token, address indexed quoteAsset, uint256 amount); event Claimed(address indexed token, address indexed creator, address indexed quoteAsset, uint256 amount); event DepositorSet(address indexed depositor, bool allowed); ``` ### Collector [#collector] ```solidity event BucketConfigured(uint256 indexed index, address recipient, uint96 weight, bool notify); event BucketRecipientSet(uint256 indexed index, address recipient, bool notify); event MinIntervalSet(uint256 minInterval); event Distributed(address indexed token, uint256 amount); event BucketPaid(address indexed token, uint256 indexed index, address recipient, uint256 amount); event BucketSkipped(address indexed token, uint256 indexed index, bytes reason); event BucketHeld(address indexed token, uint256 indexed index, address recipient, uint256 amount, uint256 totalHeld); event HeldReleased(address indexed token, uint256 indexed index, address recipient, uint256 amount); event HeldForceReleased(address indexed token, uint256 indexed index, uint256 amount); ``` ### BuybackBurner / BuybackDistributor [#buybackburner--buybackdistributor] ```solidity // Burner event KeeperSet(address keeper); event FeeRouterSet(address feeRouter); event BoughtBack(address indexed tokenIn, uint256 amountIn, uint256 motoOut); event Burned(uint256 amount, uint256 totalBurned); event PinnedPathSet(address indexed tokenIn, address[] path); event NothingToDo(address indexed tokenIn); event TwapToleranceSet(uint256 bps); event CheckpointPoked(address indexed tokenIn, address indexed pair, uint256 priceCumulative, uint32 timestamp); event TwapFloorBinding(address indexed tokenIn, uint256 keeperFloor, uint256 twapFloor); // Merkle distributor event Claimed(uint256 indexed index, address indexed account, uint256 amount); event Swept(address indexed to, uint256 amount); event SweptAll(address indexed to, uint256 amount); ``` The three TWAP events belong to the burner's price floor, which guards a buyback against a manipulated pool. Their configured values are not published here. `BuybackDistributor` is not deployed and nothing schedules it. ### RewardVestingEscrow / TreasuryVesting / Snapshotter [#rewardvestingescrow--treasuryvesting--snapshotter] ```solidity event Deposited(address indexed user, uint256 amount, uint256 scheduleTotal, uint64 start); event Claimed(address indexed user, uint256 amount); event DepositorSet(address indexed depositor, bool allowed); event Released(uint256 amount, uint256 totalReleased); // TreasuryVesting event SnapshotTaken(uint256 indexed blockNumber, uint256 timestamp); // Snapshotter ``` `RewardVestingEscrow.Deposited` has `topic0` `0x19e7166e374f41f05d851e7f5774e0d8424541e4b4353728a88a4c84fe7ba133` and `RewardVestingEscrow.Claimed(address,uint256)` has `0xd8138f8a3f377c5259ca548e70e4c2de94f129f5a11036a15b69513cba2b426a`. ### PveVault [#pvevault] ```solidity event LauncherSet(address launcher); event ExtraLauncherSet(address indexed launcher, bool allowed); event MotocatStakingSet(address staking); event PveCredited(address indexed token, uint256 amount, uint256 totalStaked, uint256 creditBlock); event PveHeld(address indexed token, uint256 amount); event PveClaimed(address indexed token, address indexed wallet, uint256 amount); event PveReceived(address indexed token, address indexed creator, uint256 amount); event Forwarded(address indexed token, address indexed to, uint256 amount); event ExcessRescued(address indexed token, address indexed to, uint256 amount); event PendingArrivalNoted(address indexed token); event PendingArrivalCleared(address indexed token); event PendingArrivalClearedByOwner(address indexed token, address indexed by); ``` ### QuoteAssetRegistry [#quoteassetregistry] ```solidity event QuoteSet(address indexed token, bool status); event QuoteRankSet(address indexed token, uint256 rank); ``` *** ## Integration Checklist [#integration-checklist] * Fetch addresses from `GET /config`. Treat `0x0000000000000000000000000000000000000000` as unset and never send to it. * Add all contract addresses to your event-log filter (if watching on-chain events yourself). * Decode via the canonical ABIs. The deployed contracts have verified source, and the event signatures on this page match it. * The **canonical trade event is `FeeRouter.Swap`**. The pair `Swap` is one-per-hop. * **Rakeback claims** need a merkle proof (`GET /rakeback/{address}`, or `GET /rakeback/{address}/proof` for proofs alone). * **Points** are indexer-derived and publish once a day at the 00:00 UTC drop - read via `GET /points/{address}`, not on-chain. * All USD values are **decimal-dollar strings** on the wire (e.g. `"2500.50"`); runtime µ-USD bigints exist only server-side. * All `bigint` values are **decimal strings** on the wire. * All addresses are **lowercased 0x-prefixed strings**. * The API caches heavily - send `If-None-Match` with the previous ETag; expect frequent `304`s. * List endpoints use **cursor pagination**; the cursor is HMAC-scope-bound (endpoint + filters). Do not reuse a cursor across queries. # Motoswap REST API - Full Inventory (/dev/reference/api-full-inventory) **Base URL:** `https://api.motoswap.org` ### CORS & Deployment [#cors--deployment] * **CORS:** the policy is set for the Motoswap app origin. From another origin a plain `GET` with no custom headers works in a browser, and every preflighted request is blocked (a `fetch` that sets `If-None-Match`, any JSON `POST`). Run conditional polling and writes server-side, or rely on the browser's own HTTP cache, which revalidates without a preflight. * **Rate limits and load shedding:** routes are rate-limited per IP (`429` + `Retry-After`), and heavy routes shed with `503` + `Retry-After` under load. Honor the header and back off. * **Chain ID:** this host serves Ethereum mainnet, chain id `1`. `GET /config` and `GET /version` both echo it. ### Which routes carry data [#which-routes-carry-data] Deployment status is per contract: see the status table on the [contract addresses page](/dev/addresses). `GET /config` is the machine-readable version, and a key holds the zero address while its contract is unset for the current phase. * Token, price, pool, swap-list, farm, stake and MOTO supply routes answer with data, from the Motoswap and Uniswap V2 pairs the indexer tracks. * Candle arrays, `/analytics/*`, `/stats/series`, `/rakeback/*`, `/creator/*`, `/pve/*` and LP fees are driven by the protocol fee, so a wallet, token or window with no fee-paying trades answers empty rather than with an error. * `launchPhase` on `GET /config` is the machine-checkable switch: `dex` with the DEX contracts deployed and open, `vamp` before the DEX opened. It does not track launch farming (ended), which kept paying for part of the day after the switch to `dex`; the farm's own end is `emissionEnd()` on `VampChef`. * Responses can carry fields that are not listed here. Ignore fields you do not recognise; do not build a parser that rejects them. * An empty answer has the same shape as a populated one. The examples below show populated shapes taken from the response contract. *** ## Wire types and encoding [#wire-types-and-encoding] All field types are JSON-safe. Here's the domain type mapping: | Domain Type | Wire Format | Notes | | ----------- | ------------------------ | -------------------------------------------------------------------------------------------- | | `Usd` | `usdWire` (string) | decimal-dollar string, e.g. `"2500.50"` = $2,500.50; parse as a decimal, never as an integer | | `Wei` | `weiWire` (string) | Token base units (scale by the token's `decimals`); bigint serialized as string | | `Address` | lowercase `0x…` (string) | Lowercased 0x-prefixed Ethereum address, branded type | | `TxHash` | `0x…` (string) | 0x-prefixed 32-byte transaction hash, branded | | `Seconds` | number (Unix) | Integer Unix seconds | | `Percent` | number | Percentage float (e.g., `5.2` = 5.2%); `null` when uncomputable | | `Apr` | number | Integer basis points (e.g., `1250` = 12.50% APR); `null` when uncomputable | Parse `Wei` strings with `BigInt(value)`, never `Number(value)`. USD examples on this page are decimal dollars with up to six decimal places, as served (`"878879.607649"`). *** ## Cache and ETag semantics [#cache-and-etag-semantics] Cached GET endpoints emit `Cache-Control: public, max-age=N, s-maxage=N, stale-while-revalidate=M` per tier: | Tier | Max-Age | SWR | Use Case | | ----------- | ------- | ---- | -------------------------------------------------------------------------------------------------------- | | `HEARTBEAT` | 1s | 9s | Chain head (block number) | | `BLOCK` | 3s | 9s | Indexer-fresh data (TVL, volume, pricing, per-user rakeback) | | `CRON` | 30s | 90s | Cron-derived data (`/analytics/breakdown`, leaderboard, weeks, leagues, candidates, `/explore/creators`) | | `STATIC` | 300s | 900s | Config, `/points/rules`, `/rakeback/stats`, immutable history | (`CRON_FAST` (15s/45s) is mounted on one route: `GET /profiles`.) **ETag Revalidation:** Cached endpoints emit a weak ETag (`W/"..."`) and support `If-None-Match`; send the tag back verbatim and a match answers 304 (not modified). Integrators should cache ETags per URL. `GET /config` carries `Cache-Control` but no ETag. `/health/live`, `/version` and every write are `private, no-store`. *** ## Cursor pagination model [#cursor-pagination-model] List endpoints use **keyset (cursor) pagination**: they return `nextCursor` (opaque HMAC-signed token). The cursor is **scope-bound** to the endpoint path and query filters: * A cursor is **not replayable** across queries with different `?filter=value` params. * Tampered/replayed cursors → 403 (not 400), body `{"error":"invalid cursor"}`. * Always pass the same filters on every page to continue. Cursor routes: `/activity`, `/dex/pools`, `/explore/tokens`, `/explore/farms`, `/explore/creators`. (The bounded sets `/farms` and `/swap/candidates` are served whole, unpaginated. `/tokens/{address}/swaps`, `/tokens/{address}/pools` and `/dex/pools/{pair}/swaps` take `?page=` instead of a cursor.) *** ## Error envelope [#error-envelope] All errors are JSON: ```json { "error": "message string" } ``` A path or query value that fails schema validation (a malformed address, for example) can answer with the validator's shape instead: `{ "success": false, "error": { "name": "ZodError", "message": "..." } }`. Branch on the status code, not the body. **Common status codes:** * `400` - bad request (validation error, invalid filter, etc.) * `403` - tampered/invalid cursor, signature mismatch, unauthorized * `404` - not found (unknown pair, unknown route) * `429` - per-IP rate limit, with `Retry-After` * `503` - service temporarily degraded (indexer lag, upstream) *** ## Route by route [#route-by-route] *** ### health.ts [#healthts] #### `GET /health/live` [#get-healthlive] **Summary:** Liveness probe. One `SELECT 1`, exempt from load shedding. This is the route to poll. **Response (200):** ```json { "ok": true } ``` **Response (503):** `{ "ok": false }` when the database is unreachable. **Caching:** `private, no-store` (no caching). **Notes:** The deep health routes are operational monitoring, not integration surfaces. They are rate-limited and their shape changes without notice. *** ### version.ts [#versionts] #### `GET /version` [#get-version] **Summary:** Build identity of the running API and the chain it serves. **Response (200):** ```json { "commit": "<40 hex characters>", "hasProvenance": true, "buildId": "", "service": "api", "chainId": 1, "bootedAt": 1789697310 } ``` **Response fields:** * `commit: string` - source commit of the build, or `"unknown"` * `hasProvenance: boolean` - whether the commit was stamped by the build * `buildId: string` - deploy id * `chainId: number` - the chain this deployment serves * `bootedAt: number` - Unix seconds of process start **Caching:** `private, no-store`. *** ### config.ts [#configts] #### `GET /config` [#get-config] **Summary:** Smart contract addresses and feature flags for the active chain. Critical for initializing the client. **Response (200):** ```json { "chainId": 1, "addresses": { "factory": "0x...", "router": "0x...", "permit2": "0x...", "motoStaking": "0x...", "motoToken": "0x...", "masterChef": "0x...", "tokenLauncher": "0x...", "feeRouter": "0x...", "collector": "0x...", "quoteRegistry": "0x...", "rakeback": "0x...", "zap": "0x...", "vaultCompounder": "0x...", "usdc": "0x...", "usdt": "0x...", "weth": "0x...", "creatorFeeRegistry": "0x...", "creatorFeeVault": "0x...", "motocatNft": "0x...", "motocatStaking": "0x...", "pveVault": "0x...", "pveVaultV2": "0x...", "buybackBurner": "0x...", "buybackDistributor": "0x...", "rewardVestingEscrow": "0x...", "treasuryVesting": "0x...", "launchpadCurve": "0x...", "launchpadLens": "0x...", "motoUniswapPair": "0x...", "vampChef": "0x...", "uniswapZap": "0x...", "uniswapV2Router": "0x...", "uniswapV2Factory": "0x...", "vaults": ["0x...", "0x...", "0x...", "0x..."] }, "splitSwapEnabled": false, "motofunEnabled": true, "bridgeMotoEnabled": false, "motofunUrl": "https://...", "pveComboEnabled": false, "launchPhase": "dex", "misconfigured": [] } ``` **Caching:** `Cache-Control: public, max-age=300, s-maxage=300, stale-while-revalidate=900` (5 minutes, STATIC tier). No ETag on this route. **Notes:** * The example gives the response shape, not values: every address is a placeholder and the flags are illustrative. Call the endpoint for current values. A key carries an address once its contract is deployed and reads `0x0000000000000000000000000000000000000000` while it is unset for the phase. Some keys stay zero permanently (optional or retired contracts), so check every key you use and treat a zero address as unset, never as a target. The per-contract status table is on the [contract addresses page](/dev/addresses). * `masterChef` and `vampChef` held the same contract while `launchPhase` was `vamp`. A farm is identified by `(chef, pid)`; read `chef` off each `/farms` row. * `permit2`, `zap`, `vaultCompounder`, `usdc`, `creatorFeeRegistry`, `creatorFeeVault` are optional (may be `0x0…`). * `splitSwapEnabled` is an API configuration flag read by the Motoswap app: `true` means the app submits a split fill as one `swapExact*Split` transaction, `false` means one swap per leg. It is not an on-chain switch. The `FeeRouter` split entrypoints have no enable flag. * `launchPhase` is `prelaunch | vamp | dex`. * `misconfigured: string[]` is the ops signal: it names the critical address keys left at `0x0…` for the current `launchPhase` (empty when all are set). The endpoint always answers 200, including when the list is non-empty, because it also carries `launchPhase` and a refusal would leave the client guessing the phase. * `motofunEnabled`, `motofunUrl`, `bridgeMotoEnabled` and `pveComboEnabled` are configuration flags carried alongside the addresses. *** ### chain.ts [#chaints] #### `GET /chain/head` [#get-chainhead] **Summary:** Current indexer tip (block number + timestamp). Integrators poll this to detect indexer staleness. **Response (200):** ```json { "chainId": 1, "blockNumber": 26003732, "blockTimestamp": 1789726607 } ``` **Response (200, no indexer state):** ```json { "chainId": 1, "blockNumber": 0, "blockTimestamp": 0 } ``` **Caching:** `Cache-Control: public, max-age=1, s-maxage=1, stale-while-revalidate=9` (HEARTBEAT - 1s). **Notes:** Used to version per-user and per-address ETags. Rapid polling (e.g., every 500ms) is safe; cache will serve 304. *** ### token.ts [#tokents] #### `GET /token/{address}/price` [#get-tokenaddressprice] **Summary:** Current USD price of a token. The price route is singular, `/token/{address}/price`. The chart route is plural, `/tokens/{address}/chart`. `GET /tokens/{address}/price` and `GET /token/{address}/chart` both answer 404. **Path params:** * `address` - token address, `0x` plus 40 hex characters. Case does not matter: the lowercase, checksummed and uppercase-hex forms all answer 200 with the same token's price. Any other length, or a missing `0x`, answers 400. **Response (200):** ```json { "priceUsd": "0.003295", "source": "onchain", "lastUpdated": "YYYY-MM-DDTHH:mm:ss.sssZ" } ``` **Response (200, no price):** ```json { "priceUsd": null, "source": null, "lastUpdated": null } ``` **Response fields:** * `priceUsd: string | null` - decimal-dollar string; `null` if unpriced * `source: string | null` - pricing source label (`"onchain"` for pool-derived prices) * `lastUpdated: string | null` - ISO 8601 timestamp with milliseconds, or null. It is the timestamp stored on the token's price row and it can trail the price itself, so use `GET /chain/head` to judge indexer freshness **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier - price moves on the write path per block). **Error responses:** 400 (invalid address). **Notes:** Prices are decimal-dollar strings; `ethPrice` (integer Wei per token) on `/tokens/{address}/stats` and `/holdings/tokens` carries full precision. An unknown token answers 200 with all three fields `null`, not 404. This is the per-token price read to use for MOTO: `GET /token/0xbd965230588eaa536de6aa45e8ebbc01638535e0/price`. *** ### tokens.ts [#tokensts] #### `GET /tokens` [#get-tokens] **Summary:** Token directory - all curated + launched tokens with symbol/name/decimals. **Response (200):** ```json { "tokens": [ { "address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", "symbol": "WBTC", "name": "Wrapped BTC", "decimals": 8, "verified": true, "hasLogo": false } ], "generatedAt": "YYYY-MM-DDTHH:mm:ss.sssZ" } ``` **Response fields:** * `tokens[].address: string` - token address * `tokens[].symbol: string` - symbol * `tokens[].name: string | null` - name or null * `tokens[].decimals: number | null` - decimals or null * `tokens[].hasLogo: boolean` - whether a logo upload exists * `tokens[].verified: boolean` - `true` = an env-canonical token (WETH/USDC/USDT/MOTO) or a curated registry/seed row; `false` = permissionless onchain discovery **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier, indexer-head versioned). **Notes:** * Merges the curated token directory with launched tokens (launched tokens take lower priority on conflict). * Launched tokens always have `decimals: 18`. * Curated tokens may have RPC-resolved decimals. * Empty response is never 404; returns `[]` if no tokens. *** #### `GET /tokens/{address}/chart` [#get-tokensaddresschart] **Summary:** Token price candlesticks + time-series volume/fees. **Path params:** * `address` - token address **Query params:** * `range` (`1m` | `5m` | `10m` | `30m` | `24h` | `7d` | `30d`, default `24h`) - chart frame **Response (200):** ```json { "price": "0.003295", "change5mPct": 0.5, "change30mPct": 1.2, "change6hPct": -2.1, "change24hPct": 5.0, "volume5mUsd": "2769.116479", "volume1hUsd": "26792.557773", "volume24hUsd": "1305195.246871", "fees24hUsd": "3915.584766", "swapCount24h": 1955, "bucketMinutes": 1, "candles": [ { "minute": 29828778, "open": "0.0003459298143979567603005925", "high": "0.0003459298143979567603005925", "low": "0.0003454649992475997730919503", "close": "0.0003454649992475997730919503", "volumeUsd": "199.325593" } ] } ``` **Response fields:** * `price: string | null` - current spot price (decimal-dollar string) or null * `change*Pct: number | null` - percent change (e.g., `5.0` = +5%), or null * `volume*Usd: string` - decimal-dollar volume string; `"0"` if none (never null) * `fees24hUsd: string` - 24h protocol fees (decimal-dollar string) * `swapCount24h: number` - transaction count * `bucketMinutes: number` - candle granularity (1, 30, or 120 min per range) * `candles[]: { minute, open, high, low, close, volumeUsd }` - sorted oldest→newest. `minute` is a number, the bucket start in unix MINUTES (multiply by 60 for seconds). OHLC values are decimal strings **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Error responses:** 400 (invalid address/range). **Notes:** * Empty chart (untraded token) returns candles: `[]`, price/deltas `null`, volumes `"0"`. Candles come from Motoswap pairs, so a token with no Motoswap trading history answers this way. For a spot price use `GET /token/{address}/price` or `GET /holdings/tokens`. * Change % is computed from the first non-zero bucket; null if all zeros. * The short frames `1m`/`5m`/`10m`/`30m` are named for their CANDLE interval, over windows of 6h/12h/24h/3d respectively. The long frames `24h`/`7d`/`30d` are named for their window, with 1/30/120-minute buckets. Read `bucketMinutes` off the response rather than inferring it from `range`. *** #### `GET /tokens/{address}/stats` [#get-tokensaddressstats] **Summary:** TVL + 24h volume + integer ETH price for token-detail stat tiles. **Path params:** * `address` - token address **Response (200):** ```json { "tvlUsd": "878879.607649", "volume24hUsd": "1329720.547149", "ethPrice": "1313311934801" } ``` **Response fields:** * `tvlUsd: string` - total liquidity value (decimal-dollar string); `"0"` if no pools * `volume24hUsd: string` - 24h volume (decimal-dollar string) * `ethPrice: string | null` - Wei per token (integer, full precision); null if unpriceable **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** * `ethPrice` is the canonical full-precision price used to compute FDV/market cap on the FE. * `tvlUsd` coerces to `"0"` (never null) when unpriced. *** #### `GET /tokens/{address}/swaps` [#get-tokensaddressswaps] **Summary:** Paginated swap transactions for this token. **Path params:** * `address` - token address **Query params:** * `page` (int, 1-200, default 1) - page number **Response (200):** ```json { "items": [ { "txHash": "0xabcd...", "logIndex": 195, "timestamp": 1789466771, "side": "sell", "tokenAmount": "133036509349955481108480", "tokenDecimals": 18, "counterToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "counterAmount": "180794401376581640", "counterDecimals": 18, "usdValue": "448.176348", "wallet": "0x..." } ], "page": 1, "pageSize": 50, "hasMore": true } ``` **Response fields:** * `side: "buy" | "sell"` - direction from this token's perspective * `tokenAmount, counterAmount: string` - Wei * `tokenDecimals: number` - this token's decimals * `counterToken: string | null`, `counterDecimals: number | null` - the other side of the trade, or null when it cannot be resolved * `usdValue: string | null` - decimal-dollar USD value at swap time, or null * `wallet: string` - swapper address * `poolCount?: number`, `outerInput?: { token, amount }` - optional, present only on rows that fold a multi-pool trade into one line * `page, pageSize, hasMore` - this list has no `total`. Keep requesting `page + 1` while `hasMore` is true **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Pagination:** Page-numbered, 50 items/page, `page` 1 to 200. *** #### `GET /tokens/{address}/pools` [#get-tokensaddresspools] **Summary:** Paginated pools containing this token. **Path params:** * `address` - token address **Query params:** * `page` (int, 1-200, default 1) **Response (200):** ```json { "items": [ { "pairAddress": "0x...", "token0": "0x...", "token1": "0x...", "reserve0": "133292018598175373111408005", "reserve1": "175053998838797275747", "lastUpdated": 1789726715, "tvlUsd": "878879.607649", "volume24hUsd": "1305195.246871", "fees24hUsd": "3915.584766", "feeAprBps": 16261, "tvlDeltaPct": 5.2, "volume24hDeltaPct": -3.1, "fees24hDeltaPct": 2.0 } ], "page": 1, "totalPages": 1, "total": 31 } ``` **Pagination:** Page-numbered, 50 pools/page, with `totalPages` and `total` (unlike the swaps list above). **Notes:** Pool deltas and APR are nullable when uncomputable. The three `*DeltaPct` fields are `null` until the pool has enough candle history to compare against. *** #### `GET /tokens/moto/supply` [#get-tokensmotosupply] **Summary:** MOTO supply breakdown, read from chain, with the circulating figure and its definition. **Response (200):** ```json { "totalSupply": "10000000000000000000000000000", "burned": "0", "treasuryVestingUnreleased": "1900000000000000000000000000", "treasuryLocked": "2099456366000000000000000256", "escrowUnreleased": "10306557902037501334536150", "chefUndistributed": "499307359307359307359314622", "circulating": "5490929716790603191306148972", "definition": "Total supply minus burned MOTO, minus ..." } ``` **Response fields:** * Every amount is a Wei string (18 decimals). Parse with `BigInt` * `circulating: string` - total supply minus the five deductions above it * `definition: string` - the plain-English definition of `circulating`, served with the number so a listing site can quote it **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). The chain reads behind it are cached in process for about a minute. *** ### token-logo.ts [#token-logots] #### `GET /launchpad/token/{address}/logo` [#get-launchpadtokenaddresslogo] **Summary:** Fetch uploaded logo image for a launched token. **Path params:** * `address` - launched token address **Response (200):** Binary image data (PNG or WebP). **Response Headers:** * `Content-Type: image/png | image/webp` * `ETag: W/"..."` (weak ETag for cache validation) * `Cache-Control: public, max-age=300, s-maxage=300, stale-while-revalidate=900` **Response (304):** If `If-None-Match` matches the ETag. **Response (404):** No logo uploaded yet. **Response (400):** Invalid address. **Caching:** 5-minute cache; weak ETag. *** ### activity.ts [#activityts] #### `GET /activity` [#get-activity] **Summary:** Paginated activity log (swaps, LP events, staking, etc.). **Query params:** * `type` (enum: swap | add\_liquidity | remove\_liquidity | farm\_stake | farm\_unstake | farm\_harvest | vault\_stake | vault\_unstake | vault\_claim | deploy\_token | motofun\_launch | motofun\_trade | motofun\_graduation, optional) * `status` (enum: pending | completed | failed | cancelled | stalled, optional) * `user` (address, optional) * `venue` (enum: motoswap | motofun | all, default `all`) - restricts the feed to one venue's kinds * `cursor` (opaque string, optional) - pagination cursor **Response (200):** ```json { "items": [ { "transactionId": "0xabcd...", "logIndex": 376, "user": "0x...", "kind": "farm_stake", "status": "completed", "timestamp": 1789726391, "blockNumber": 26003714, "data": { "ctx": { "pid": 0 }, "legs": [ { "token": "0xad1a21f61d653c6101b92c335f61140459c81c79", "amount": "3427820391788993158", "symbol": null, "decimals": 18 } ], "usdValue": "20.844042" } } ], "nextCursor": "ZXlKcFpDSTZOREkyTURkOQ.WZ6-aoIxCHpYZLEc", "hasMore": true } ``` **Response fields:** * `kind: string` - activity type * `status: string` - transaction status * `data: any` - event-specific JSON (schema varies by kind, and the field can be absent). Swap and farm rows carry `ctx` (for example `pairAddress` or `pid`), `legs[]: { token, amount, symbol, decimals }` with Wei-string amounts, and a decimal-dollar `usdValue` **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Pagination:** Keyset cursor, 25 items/page. Scope-bound to filters; changing filters invalidates the cursor. **Error responses:** * 400 - bad request (invalid filter) * 403 - invalid/tampered cursor **Notes:** * Cursor is HMAC-signed and scope-bound to endpoint + filters. * Empty result is never 404; returns `[]`. `nextCursor` is `null` on the last page. * The feed carries swaps on every pair the indexer tracks, Motoswap and Uniswap V2 both, plus farm stakes, unstakes and harvests. * The three `motofun_*` kinds and the `motofun` venue are reserved for a separate venue. On a deploy where it is off, `venue` is forced to `motoswap` and those kinds are never served. *** ### analytics.ts [#analyticsts] #### `GET /analytics/overview` [#get-analyticsoverview] **Summary:** Protocol TVL, volume, and fees for a time window. **Query params:** * `window` (24h | 7d | 30d | all, default 24h) * `rank` (tvl | volume, default tvl) - unused in response **Response (200):** ```json { "totals": { "tvlUsd": "5000000.25", "volumeUsd": "1000000.5", "feesUsd": "10000.005" }, "motofun": { "volumeUsd": "20000.75", "feesUsd": "240.009" } } ``` **Response fields:** * `totals: { tvlUsd, volumeUsd, feesUsd }` - DEX only * `motofun: { volumeUsd, feesUsd }` - OPTIONAL; absent (not zeroed) when the deploy has that flag off, so a flag-off response is byte-identical to one from before the field existed **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** A `window` of `all` includes the protocol's entire history. `totals` covers Motoswap pairs only, so a window with no Motoswap volume reads `"0"` across the board even while `/dex/pools` also lists tracked Uniswap V2 pairs. *** #### `GET /stats/series` [#get-statsseries] **Summary:** Time-series TVL / volume / fees. **Query params:** * `metric` (tvl | volume | fees, default tvl) * `range` (24h | 7d | 30d, default 30d) **Response (200):** ```json { "buckets": [ { "t": 1789725600, "value": "5000000.25" } ], "changePct": 5.3 } ``` **Response fields:** * `buckets: [{ t, value }]` - hourly buckets, oldest→newest * `t: number` - bucket start time (Unix seconds, hour boundary) * `value: string` - decimal-dollar (TVL is gauge/LOCF; volume/fees are additive) * `changePct: number | null` - % change last bucket vs first non-zero; null if all zeros **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** TVL is a gauge (last-observation-carry-forward); volume/fees are additive sums. No 404; empty series returns `[]`. *** #### `GET /analytics/revenue/series` [#get-analyticsrevenueseries] **Summary:** Swap fee revenue by category over time: LP, creator, staking, Rakeback, buyback and burn. **Query params:** * `range` (7d | 30d | 90d | 1y, default 30d) * `bucket` (day | epoch, default day) - calendar day or 7d rakeback epoch **Response (200):** ```json { "bucket": "day", "buckets": [ { "t": 1789689600, "total": "820", "lp": "300", "creator": "20", "staking": "200", "rakeback": "200", "buybackBurn": "100" } ] } ``` **Response fields:** * `t: number` - bucket-start Unix seconds * `total: string` - sum of the five legs below (decimal-dollar string) * `lp: string` - the one computed leg: 0.30% of measured volume over the bucket * `creator: string` - measured creator fees (creator\_fee\_events) * `staking, rakeback, buybackBurn: string` - per-bucket sums of the measured Collector distribution events, one derivation each; nothing is estimated from a weight ratio There is no `protocolFee` field and no `treasury` field. `total` is what was distributed to participants, not the venue's whole take. **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** Every leg but `lp` is a write-time USD snapshot, never re-priced. Bucket boundaries are UTC midnight (day) or the Rakeback epoch, which rolls Sunday 00:00 UTC (epoch). *** #### `GET /analytics/revshare/series` [#get-analyticsrevshareseries] **Summary:** MOTO staker rewards distributed over time. **Query params:** * `range` (7d | 30d | 90d | 1y, default 30d) * `bucket` (day | epoch, default day) **Response (200):** ```json { "bucket": "day", "buckets": [ { "t": 1789689600, "value": "175" } ] } ``` **Response fields:** * `value: string` - decimal-dollar of actual distributions (from vault\_reward\_events) **Notes:** Write-time USD snapshots; null prices are dropped. Revshare differs from the theoretical split due to distribution timing and Collector re-weighting. *** #### `GET /analytics/creator-fees/series` [#get-analyticscreator-feesseries] **Summary:** Creator fee accruals to token creators over time. **Query params:** * `range` (7d | 30d | 90d | 1y, default 30d) * `bucket` (day | epoch, default day) **Response (200):** Same shape as revshare series. **Notes:** From creator\_fee\_events (FeeRouter.CreatorFee); write-time USD snapshots. *** #### `GET /analytics/breakdown` [#get-analyticsbreakdown] **Summary:** Lifetime measured revenue payouts. **Response (200):** ```json { "rakebackAccruedUsd": "50000.25", "revshareToStakersUsd": "40000.5", "creatorPayoutsUsd": "5000.75" } ``` **Response fields:** * `rakebackAccruedUsd, revshareToStakersUsd, creatorPayoutsUsd: string` - real measured lifetime sums over the same ledgers the windowed series bucket **Caching:** `Cache-Control: public, max-age=30, s-maxage=30, stale-while-revalidate=90` (CRON tier). **Notes:** The former `protocolFeeUsd` and `split` fields were removed: they were a second, derived allocation of the same money and disagreed with the measured ledgers. *** ### dex.ts [#dexts] #### `GET /dex/pools` [#get-dexpools] **Summary:** Paginated pool directory, sortable by TVL/volume/fees. **Query params:** * `sort` (tvl | volume24h | fees24h, default tvl) * `dir` (asc | desc, default desc) * `cursor` (opaque string, optional) **Response (200):** ```json { "items": [ { "pairAddress": "0x...", "token0": "0x...", "token1": "0x...", "reserve0": "133292018598175373111408005", "reserve1": "175053998838797275747", "lastUpdated": 1789726715, "tvlUsd": "878879.607649", "volume24hUsd": "1305195.246871", "fees24hUsd": "3915.584766", "feeAprBps": 16261, "tvlDeltaPct": 5.2, "volume24hDeltaPct": -3.1, "fees24hDeltaPct": 2.0, "volume30dUsd": "0" } ], "nextCursor": null, "hasMore": false } ``` **Pagination:** Keyset cursor, 100 items/page. Cursor scope-bound to `` `{ sort, dir }` ``. **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Error responses:** * 400 - bad request * 403 - invalid cursor **Notes:** Nulls rank last in all sort orders (e.g., pools with no APR sort to the bottom). `volume30dUsd` is unique to the list (not on detail). `nextCursor` is `null` when `hasMore` is false. The list holds every pair the indexer tracks, Motoswap pairs and the Uniswap V2 pairs the launch farm used. *** #### `GET /dex/pools/{pair}` [#get-dexpoolspair] **Summary:** Single pool detail + reserves + current stats. **Path params:** * `pair` - pair/LP token address **Response (200):** ```json { "pairAddress": "0x...", "token0": "0x...", "token1": "0x...", "reserve0": "133292018598175373111408005", "reserve1": "175053998838797275747", "lastUpdated": 1789726715, "tvlUsd": "878879.607649", "volume24hUsd": "1305195.246871", "fees24hUsd": "3915.584766", "feeAprBps": 16261, "tvlDeltaPct": 5.2, "volume24hDeltaPct": -3.1, "fees24hDeltaPct": 2.0 } ``` **Response (404):** Pair not indexed. **Response (200, empty pool):** If the pair is real but has never been priced: ```json { "pairAddress": "0x...", "token0": "0x...", "token1": "0x...", "reserve0": "0", "reserve1": "0", "lastUpdated": 1723456789, "tvlUsd": "0", "volume24hUsd": "0", "fees24hUsd": "0", "feeAprBps": null, "tvlDeltaPct": null, "volume24hDeltaPct": null, "fees24hDeltaPct": null } ``` **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** Empty/drained pools return genuine $0 state instead of 404 (so LP panels can add/remove liquidity). Reserves are authoritative from pair\_stats; USD aggregates are "0" per zero-metric rule; deltas/APR stay null (uncomputable). *** #### `GET /dex/pools/{pair}/chart` [#get-dexpoolspairchart] **Summary:** Price candles, volume, fees, and deltas for a pool. **Path params:** * `pair` - pair address **Query params:** * `range` (`24h` | `7d` | `30d` | `1y`, default `24h`) - anything else fails validation Bucket width follows the range and is echoed back as `bucketMinutes`: `24h` serves one-minute candles, `7d` 15-minute candles, `30d` hourly candles and `1y` daily candles. Every range arrives whole (at most 1,441, 673, 721 and 366 rows). Rows are sparse: a bucket with no trade has no candle. `minute` is a number, the bucket's start **in unix minutes, not seconds**. Multiply by 60 to get a timestamp. Re-bucket on this field and `bucketMinutes` rather than assuming a fixed spacing. **Response (200):** ```json { "spot": "0.000001313311934801", "tvlUsd": "878879.607649", "change5mPct": 0.5, "change30mPct": 1.2, "change6hPct": -2.1, "change24hPct": 5.0, "volume5mUsd": "2769.116479", "volume1hUsd": "26792.557773", "volume24hUsd": "1305195.246871", "fees24hUsd": "3915.584766", "swapCount24h": 1955, "bucketMinutes": 1, "candles": [ { "minute": 29828778, "open": "2.4", "high": "2.6", "low": "2.3", "close": "2.5" } ] } ``` Pool-chart candles carry OHLC only - no per-candle `volumeUsd` (the window volumes above are the volume surface). **Response (200, empty pool):** `spot: null`, `tvlUsd: null`, volumes `"0"`, deltas `null`, `candles: []`. **Response (404):** Pair not indexed. **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** Spot ratio is `token1 / token0` serialized as a decimal string. Empty/drained pools return a genuine empty chart (not 404). The tracked Uniswap V2 pairs answer with `spot`, `tvlUsd` and the window volumes filled, and with `candles: []` and `null` deltas, because candles come from Motoswap pairs. *** #### `GET /dex/pools/{pair}/swaps` [#get-dexpoolspairswaps] **Summary:** Paginated swaps in this pool. **Path params:** * `pair` - pair address **Query params:** * `page` (int, 1-200, default 1) **Response (200):** `items[]` rows have the same fields as `/tokens/{address}/swaps` (without the optional `poolCount` and `outerInput`), but the envelope differs: ```json { "items": [ ], "page": 1, "totalPages": 200, "total": 10000 } ``` The list reaches back 200 pages, so `total` tops out at `10000` on a busy pool. **Pagination:** Page-numbered, 50 swaps/page, `page` 1 to 200. *** #### `GET /dex/pools/holder/{address}/pairs` [#get-dexpoolsholderaddresspairs] **Summary:** Pairs where this wallet ever received LP (auto-detect candidate set). **Path params:** * `address` - wallet address **Response (200):** ```json { "pairs": [ { "pairAddress": "0x...", "token0": "0x...", "token1": "0x...", "reserve0": "1000000000000000000", "reserve1": "500000000000000000" } ] } ``` **Response (200, empty):** `{ "pairs": [] }`. **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** Read from LP transfer events, so it is always live. Hard-capped at 500 distinct pairs to prevent unbounded queries. Call `balanceOf` on each pair to find the wallet's active positions. *** ### explore.ts [#explorets] #### `GET /explore/farms` [#get-explorefarms] **Summary:** Paginated farm ranking by TVL/APR/multiplier. **Query params:** * `sort` (tvl | apr | multiplier, default tvl) * `dir` (asc | desc, default desc) * `cursor` (opaque string, optional) - integer rank cursor **Response (200):** ```json { "farms": [ { "rank": 2, "chef": "0x51e648f08a9a08a724591938d9cbc483c809aca4", "pid": 0, "isSingleSided": false, "token0": "0xbd965230588eaa536de6aa45e8ebbc01638535e0", "token1": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "lpToken": "0xad1a21f61d653c6101b92c335f61140459c81c79", "rewardSymbol": "MOTO", "multiplier": 800, "tvlUsd": "271155.767887", "aprBps": 707893 } ], "nextCursor": null, "hasMore": false } ``` **Response fields:** * `rank: number` - per-page rank (1, 2, 3, …) * `chef: string` - the farming contract this pool lives in. A `pid` is unique only within one chef, so address a farm as `(chef, pid)` * `pid: number` - pool ID inside that chef * `isSingleSided: boolean` - true if staking single asset * `token0, token1: string | null` - the LP's tokens; both `null` on a single-sided farm * `lpToken: string` - staked token address (pair or single token) * `multiplier: number` - allocPoint (used to compute APR) * `tvlUsd: string` - computed TVL (decimal-dollar string); `"0"` if unpriced * `aprBps: number | null` - projected APR in basis points, or null if uncomputable **Pagination:** In-memory rank cursor, 100 items/page. Nulls (APR) rank last. **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** APR is null when MOTO price is unset or TVL is zero. The farm set is small/curated; ranking is done in-memory. The array is named `farms`, not `items`. The launch farm's pools are Uniswap V2 LP tokens plus single-sided MOTO. *** #### `GET /explore/tokens` [#get-exploretokens] **Summary:** Paginated token ranking by TVL/volume/price. **Query params:** * `sort` (tvl | volume24h, default tvl) - anything else fails validation * `dir` (asc | desc, default desc) * `cursor` (opaque string, optional) - composite sort key + token cursor **Response (200):** ```json { "items": [ { "rank": 1, "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "priceUsd": "2510.31", "ethPrice": "1000000000000000000", "change1hPct": 2.5, "change1dPct": 5.0, "volume24hUsd": "7810664.29", "tvlUsd": "28315196.48", "spark": [2498.12, 2503.4, 2510.31] } ], "nextCursor": null, "hasMore": false } ``` **Response fields:** * `rank: number` - per-page rank * `priceUsd: string | null` - decimal-dollar price string or null * `ethPrice: string | null` - Wei per token (full precision) or null * `change1hPct, change1dPct: number | null` - percent deltas or null * `volume24hUsd, tvlUsd: string` - fixed-format strings (e.g., `"1234567.89"`) * `spark: number[]` - 7d sparkline of raw prices (not normalized); `[]` when there is no price history **Pagination:** Composite keyset cursor (sort key + token address), 100 items/page. Bound to `` `{ sort, dir }` ``. **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** Change % is null if no historical candle. `change1hPct` and `change1dPct` are `null` and `spark` is `[]` on a row with no Motoswap candle history, because candles come from Motoswap pairs. *** #### `GET /explore/creators` [#get-explorecreators] **Summary:** Creators ranked by lifetime creator-fee earnings. **Query params:** * `venue` (all | motoswap | motofun, default all) * `cursor` (opaque string, optional) **Response (200):** ```json { "creators": [ { "rank": 1, "creator": "0x...", "totalUsd": "1250.5", "tokenCount": 3, "unpricedTokens": 0 } ], "nextCursor": null, "hasMore": false, "updatedAt": 1789726576 } ``` **Response fields:** * `totalUsd: string` - lifetime creator fees, decimal-dollar string * `tokenCount: number` - the creator's tokens with a positive priced total, not tokens created * `unpricedTokens: number` - tokens with at least one unpriced fee leg; when above 0, `totalUsd` is a lower bound * `updatedAt: number | null` - Unix seconds of the last ranking refresh, or null if it has never run **Caching:** `Cache-Control: public, max-age=30, s-maxage=30, stale-while-revalidate=90` (CRON tier). **Error responses:** 400 (bad request), 403 (invalid cursor). **Notes:** The array is named `creators`. It answers empty when no registered token has accrued a creator fee. *** ### farms.ts [#farmsts] #### `GET /farms` [#get-farms] **Summary:** Full farm directory with global chef state (unpaginated). **Response (200):** ```json { "chainId": 1, "currentBlock": 26002074, "pools": [ { "chef": "0x51e648f08a9a08a724591938d9cbc483c809aca4", "pid": 0, "lpToken": "0xad1a21f61d653c6101b92c335f61140459c81c79", "allocPoint": 800, "depositFeeBps": 0, "totalStaked": "41669322746313790009193", "isSingleSided": false, "pair": { "pairAddress": "0xad1a21f61d653c6101b92c335f61140459c81c79", "token0": "0xbd965230588eaa536de6aa45e8ebbc01638535e0", "token1": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "reserve0": "125435543370865961114919638", "reserve1": "177491136820610639233" }, "displayName": null, "lastUpdated": 1789413083, "tvlUsd": "258219.642354", "aprBps": 782785, "volume24hUsd": "993278.337482", "fees24hUsd": "2979.834289" } ], "global": { "id": 1, "totalAllocPoint": "1100", "initialPeriodReward": "248015873015873015873", "totalEmission": "300000000000000000000000000", "emissionStart": 1789415087, "emissionEnd": 1790624687, "currentRewardPerSecond": "248015873015873015873", "lastUpdated": "YYYY-MM-DDTHH:mm:ss.sssZ" }, "motoDistributed": "72321428571428571428566800" } ``` **Response fields:** * `pools[].chef: string` - the farming contract the pool lives in. Send `userInfo` / `pendingReward` reads and deposits to THIS address, and pass it to `GET /pnl/{address}/farm/{chef}/{pid}` * `pools[].pair: object | null` - null if single-sided or unindexed * `displayName: string | null` - human-readable pool name or null * `global: object | null` - null if not yet initialized * `volume24hUsd, fees24hUsd: string` - the staked pair's 24h figures; `"0"` on a single-sided farm **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** Unpaginated (governance-curated set, one add() per pool). FE filters client-side. *** ### holdings.ts [#holdingsts] #### `GET /holdings/tokens` [#get-holdingstokens] **Summary:** Curated token set with current + 24h-ago prices (for user balance displays). **Response (200):** ```json { "tokens": [ { "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "decimals": 18, "priceUsd": "2510.31", "ethPrice": "1000000000000000000", "priceUsd24hAgo": "2498.12" }, { "address": "0xbd965230588eaa536de6aa45e8ebbc01638535e0", "decimals": 18, "priceUsd": "0.003296", "ethPrice": "1313369318829", "priceUsd24hAgo": null } ] } ``` **Response fields:** * `decimals: number` - token decimals (RPC-resolved or fallback 18) * `priceUsd: string | null` - decimal-dollar price string or null (unknown) * `ethPrice: string | null` - WETH per whole token, scaled by 1e18 (a Wei-style integer string), or null. Full precision; prefer it over `priceUsd` for low-priced tokens * `priceUsd24hAgo: string | null` - LOCF from 24h-ago candle, or null. `null` on a row with no candle 24 hours back, because it comes from candles **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** * `priceUsd: null` renders as a dash on the FE (not $0). * `priceUsd24hAgo: null` means no historical data; FE shows no delta. * Bounded set: WETH, USDC, USDT, WBTC and MOTO, plus graduated moto.fun coins that have a price. *** ### motocats.ts [#motocatsts] #### `GET /motocats/{address}` [#get-motocatsaddress] **Summary:** Motocat token IDs held in a wallet (not staked). **Path params:** * `address` - wallet address **Response (200):** ```json { "tokenIds": ["1", "42", "1337"] } ``` **Response (200, no cats):** `{ "tokenIds": [] }`. **Response (400):** Invalid address. **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** * "Owned" = current holder (latest transfer's `to`). * Staked cats are held by the staking contract, so excluded. * Read from `motocat_nft_transfer_events` (write-path table, live). *** ### launchpad.ts [#launchpadts] #### `GET /launchpad/recent` [#get-launchpadrecent] **Summary:** 12 most recent token launches (newest first). **Response (200):** ```json { "items": [ { "address": "0x...", "symbol": "NEWTOKEN", "name": "New Token", "deployer": "0x...", "timestamp": 1790600000, "origin": "motofun", "graduatedAt": null, "priceUsd": "0.000012", "liquidityUsd": "50000.25" } ] } ``` **Response fields:** * `timestamp: number` - launch time (Unix seconds) * `origin: "deployer" | "motofun"` - which launch surface created the token. `deployer` marks coins from the retired launch contract * `graduatedAt: number | null` - Unix seconds of graduation for a moto.fun coin, or null * `priceUsd: string | null` - decimal-dollar price string or null (not yet priced) * `liquidityUsd: string | null` - sum of both pair TVLs (MOTO + quote), or null **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** Chronological (newest first); never ranked. Priced/liquidity are the first values from the stats sinks (null if not yet computed). Answers `{ "items": [] }` when nothing matches the window. *** #### `GET /launchpad/token/{address}` [#get-launchpadtokenaddress] **Summary:** Single launched token detail + deployer's other launches. **Path params:** * `address` - launched token address **Response (200):** ```json { "launch": { "address": "0x...", "symbol": "NEWTOKEN", "name": "New Token", "deployer": "0x...", "timestamp": 1790600000, "origin": "motofun", "graduatedAt": null, "priceUsd": "0.000012", "liquidityUsd": "50000.25" }, "otherLaunches": [ ], "otherLaunchesTotal": 5 } ``` **Response (200, not a launched token):** `{ "launch": null, "otherLaunches": [], "otherLaunchesTotal": 0 }`. There is no 404 on this route. **Response (400):** Invalid address. **Response fields:** * `launch: LaunchItem | null` - the requested token, or null when the address was not launched here * `otherLaunches: LaunchItem[]` - up to 8 other launches by the same deployer (newest first) * `otherLaunchesTotal: number` - true total of the deployer's OTHER launches (to render "N other launches" copy) **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** LaunchItem shape is same as `/launchpad/recent`. *** ### pnl.ts [#pnlts] #### `GET /pnl/{address}/token/{token}` [#get-pnladdresstokentoken] **Summary:** Per-wallet realized P\&L for a token. **Path params:** * `address` - wallet address * `token` - token address (cannot be a quote asset) **Response (200):** ```json { "token": "0x...", "partial": false, "buys": 5, "sells": 3, "unitsBought": "286415823120588715703305", "unitsSold": "127623434821278976299310", "boughtUsd": "975.981157", "soldUsd": "431.269881", "costOfSoldUsd": "434.885427", "realizedPnlUsd": "-3.615546", "realizedPnlBps": -83, "firstBuyTimestamp": 1789446107, "lastSellTimestamp": 1789726391 } ``` **Response fields:** * `partial: boolean` - true if unpriced rows excluded (FE must NOT render a P\&L number) * `unitsBought, unitsSold: string` - Wei (raw units) * `*Usd: string` - decimal-dollar string; `realizedPnlUsd` is signed * `realizedPnlBps: number | null` - basis points gain/loss, signed * `firstBuyTimestamp, lastSellTimestamp: number | null` - Unix seconds **Response (400):** Invalid address/token, or token is a quote asset. **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** * Quote assets (USDC, WETH, USDT, MOTO) cannot be analyzed, because P\&L is measured in them. * `partial: true` means the FE should show a warning ("data incomplete, unpriced swaps excluded"). * Covers MOTO trades on the tracked Uniswap V2 pairs as well as Motoswap pairs. *** #### `GET /pnl/{address}/farm/{chef}/{pid}` [#get-pnladdressfarmchefpid] **Summary:** Per-wallet staking P\&L for a farm. **Path params:** * `address` - wallet address * `chef` - the farming contract address, from the pool's `chef` field on `GET /farms` * `pid` (int) - pool ID inside that chef The older two-segment form, `/pnl/{address}/farm/{pid}`, no longer exists and answers `404`. **Response (200):** ```json { "chef": "0x51e648f08a9a08a724591938d9cbc483c809aca4", "pid": 0, "partial": false, "stakes": 11, "unstakes": 0, "harvests": 21, "stakedInUsd": "2811.39529", "unstakedOutUsd": "0", "harvestedUsd": "1722.881042", "harvestedMotoWei": "509945373012941070495206", "stakeUnitsWei": "469278446511578501145", "netStakeUnits": "469278446511578501145", "firstStakeTimestamp": 1789446107 } ``` **Response fields:** * `stakedInUsd, unstakedOutUsd, harvestedUsd: string` - decimal-dollar strings, frozen at write time * `harvestedMotoWei?: string` - MOTO harvested, in Wei. Optional * `stakeUnitsWei?: string` - LP (or single token) units staked, in Wei. Optional * `netStakeUnits: string` - stakes minus unstakes, in Wei * `firstStakeTimestamp: number | null` - Unix seconds, or null for a wallet that never staked **Response (400):** Invalid address, chef or pid. **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** * `partial: true` if rows lacked prices. * All USD values are frozen write-time snapshots. * A wallet with no position answers 200 with zero counts, not 404. *** ### points.ts [#pointsts] #### `GET /points/leaderboard` [#get-pointsleaderboard] **Summary:** Top 100 wallets by total points (the daily-drop projection). **Response (200):** ```json { "rows": [ { "rank": 1, "address": "0x...", "totalPoints": 10000, "swapCount": 500 } ] } ``` **Response fields:** * `totalPoints: number` - integer points (1/1000 milli precision) * All figures are from the daily-dropped snapshot (not live ledger) **Caching:** `Cache-Control: public, max-age=30, s-maxage=30, stale-while-revalidate=90` (CRON tier - recomputed at the daily drop / settlement, not per block). **Notes:** Excludes wallets with 0 dropped points. Only the top 100 are listed. An empty board answers `{ "rows": [] }`. *** #### `GET /points/{address}` [#get-pointsaddress] **Summary:** Per-wallet points, rank, season state, recent swaps. **Path params:** * `address` - wallet address **Response (200):** ```json { "address": "0x...", "season": { "catsWorking": 2 }, "totalPoints": 5000, "swapCount": 250, "referralPoints": 500, "referralPointsDisplay": "500", "rank": 42, "percentile": 42, "totalRanked": 10000, "firstSwapAt": 1789446107, "referral": { "linkable": false, "reason": "already_scored" }, "droppedAt": 1789689600, "todayActions": 12, "recentSwaps": [ { "txHash": "0x...", "logIndex": 0, "timestamp": 1723456789, "pair": "0x...", "tokenIn": "0x...", "tokenOut": "0x...", "amountIn": "1000000000000000000", "amountOut": "500000000000000000" } ] } ``` **Response fields:** * `season.catsWorking: number` - bucketed count of the cats this wallet has staked in `MotocatStaking`. Cats held in the wallet do not count, and the figure is bucketed rather than exact so the multiplier cannot be read straight out of a probe * `totalPoints: number` - integer points from the daily drop * `referralPoints: number` - 10% of referees' points * `referralPointsDisplay: string` - the same figure as display text: `"0"`, a `"` is the `chainId` from `GET /config`, and the blank lines are part of the message: ```text Activate your Motoswap referral link. This is a free signature. It does not send a transaction, spend gas, or approve any funds. It links your wallet to the person who referred you. Referred by: Your wallet: motoswap-referral::: ``` **Request body:** ```json { "referee": "0x...", "referrer": "0x...", "signature": "0x...", "source": "twitter" } ``` **Request fields:** * `referee, referrer: string` - 0x-addresses * `signature: string` - 0x-hex EIP-191 personal\_sign of the referral message * `source: string` - optional freetext (sanitized, max 256 chars) **Response (200, bound):** ```json { "bound": true, "binding": { "refereeAddress": "0x...", "referrerAddress": "0x...", "source": "twitter", "boundAt": "2024-08-12T10:30:00Z" } } ``` **Response (200, already bound):** ```json { "bound": false, "already": true, "binding": { "..." : "..." } } ``` **Response (400):** `{ "bound": false, "error": "" }`. Reasons include a self-referral or cycle, `referee-already-active` (the referee has already scored Points) and `referrer-cap-reached`. A malformed body answers `{ "error": "referee and referrer must both be 0x-addresses" }`. **Response (403):** `{ "bound": false, "error": "signature does not authorize this binding" }`. EOA recovery failed and the ERC-1271 check did not pass. **Caching:** `no-store` (authentication required). **Notes:** * First-touch binding is permanent; a referee can only have one referrer. * Signature proves control of the referee address (EOA or smart account). * Binding fails once the referee has scored Points (to prevent retroactive harvesting). `GET /points/{address}` answers the same question ahead of time in its `referral` field. * Free-text `source` is sanitized at the zod layer (NUL byte protection). *** #### `GET /referrals/{address}` [#get-referralsaddress] **Summary:** Referral summary for a wallet (referrer info + referee count + points). **Path params:** * `address` - wallet address **Response (200):** ```json { "address": "0x...", "referrer": "0x...", "boundAt": "2024-08-12T10:30:00Z", "refereeCount": 42, "referralPoints": 5000, "referralPointsDisplay": "5000", "generatedAt": "YYYY-MM-DDTHH:mm:ss.sssZ" } ``` **Response (200, no binding):** ```json { "address": "0x...", "referrer": null, "boundAt": null, "refereeCount": 0, "referralPoints": 0, "referralPointsDisplay": "0", "generatedAt": "YYYY-MM-DDTHH:mm:ss.sssZ" } ``` **Response fields:** * `referrer: string | null` - wallet's referrer (if bound) * `refereeCount: number` - distinct referees (10% of their points accrue to referrer) * `referralPoints: number` - 10% of referees' lifetime points * `referralPointsDisplay: string` - the same figure as display text (see `GET /points/{address}`) **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Error responses:** 400 (invalid address). *** ### stake.ts [#stakets] #### `GET /stake/vaults` [#get-stakevaults] **Summary:** Lock-up vault directory with TVL and projected APR. **Response (200):** ```json { "vaults": [ { "vaultId": 0, "label": "", "lockDuration": "", "rewardMultiplierBps": "", "tvl": "20789840112855468232743", "projectedAprBps": null } ], "motoPriceUsd": "0.003296" } ``` The label, duration and multiplier values are elided above. Read them from the response. **Response fields:** * `vaultId: number` - the lock-duration option, 0 to 3. These are options on one `MotoStaking` position, not four separate vaults * `label: string` - human-readable name * `lockDuration: number` - seconds * `rewardMultiplierBps: number` - the stake-weight multiplier for the option, in bps. On chain, `MotoStaking.boostBps(durationOption)` stores the EXTRA on top of a 1x base, so `multiplier = 1 + boostBps / 10000` and `rewardMultiplierBps = 10000 + boostBps`. This field is a static value; governance can change `boostBps`, and the chain is the source of truth * `tvl: string` - Wei (total staked) * `projectedAprBps: number | null` - annualized return basis points, or null if uncomputable * `motoPriceUsd: string | null` - decimal-dollar MOTO price string for FE display **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Notes:** * APR is null when MOTO price is unset or total effective stake is zero. Staking revenue is a share of swap fees, so a period with no fee inflow leaves it `null`. * TVL is not per-user. Read a wallet's position from `MotoStaking` on chain. * Read the label and duration from this route. Do not hardcode them. *** ### moto.ts [#motots] #### `GET /moto/overview` [#get-motooverview] **Summary:** Where MOTO sits: staked per lock option, staked in total, and held in indexed pools. **Response (200):** ```json { "stakedByTier": [ { "vaultId": 0, "staked": "20789840112855468232743" } ], "stakedTotal": "1000164587070035394350762857", "inMotoswapPools": "142861905011747233674438803", "motoswapPoolCount": 31, "pveLaunchCount": 0, "motoPriceUsd": "0.003296" } ``` **Response fields:** * `stakedByTier[]: { vaultId, staked }` - Wei staked per lock option, four rows * `stakedTotal, inMotoswapPools: string` - Wei * `motoswapPoolCount: number` - indexed pools that hold MOTO, Motoswap and Uniswap V2 both * `pveLaunchCount: number` - distinct launched tokens that sent a PvE allocation * `motoPriceUsd: string | null` - decimal-dollar MOTO price **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). *** ### swap.ts [#swapts] #### `GET /swap/candidates` [#get-swapcandidates] **Summary:** Candidate swap path pairs (topology only, no reserves). **Query params:** * `tokenIn` (address) - input token * `tokenOut` (address) - output token **Response (200):** ```json { "pairs": [ { "address": "0x...", "token0": "0x...", "token1": "0x..." } ] } ``` **Response (200, no path):** `{ "pairs": [] }`. **Response (400):** Invalid tokenIn or tokenOut. **Caching:** `Cache-Control: public, max-age=30, s-maxage=30, stale-while-revalidate=90` (CRON tier). **Notes:** * Candidates are edge topologies; FE uses live `getReserves()` to quote. * Versioned on indexer head (no reserves, so versioning is automatic). * No amount param - candidate set is amount-independent. *** ### user.ts [#userts] #### `GET /{address}/meta` [#get-addressmeta] **Summary:** User metadata (last activity block for cache invalidation). **Path params:** * `address` - wallet address **Response (200):** ```json { "address": "0x...", "lastActivityBlock": 6123456 } ``` **Response (200, no activity):** ```json { "address": "0x...", "lastActivityBlock": 0 } ``` **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Error responses:** 400 (invalid address). **Notes:** Used to version per-user caches. Block is updated on activity (referral binding, P\&L calcs, etc.). *** ### profile.ts [#profilets] A wallet's public profile: a Motocat picture and an optional linked X handle. Every write is authorized by a signature from the wallet itself. The profile object returned by `GET /profile/{address}`, `POST /profile/pfp`, `POST /profile/x/link` and `POST /profile/x/unlink` is the same shape: ```json { "address": "0x...", "motocatTokenId": "42", "xHandle": "someone", "xVerified": true } ``` * `motocatTokenId: string | null` - decimal token id of the chosen Motocat, or null * `xHandle: string | null` - linked handle, or null * `xVerified: boolean` - false when no handle is linked #### `GET /profile/{address}` [#get-profileaddress] **Path params:** `address` - wallet address. **Response (200):** the profile object. A wallet with no row still answers 200 with nulls. **Response (400):** invalid address. **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier, versioned on the profile row's `updated_at`). #### `GET /profiles` [#get-profiles] **Summary:** Batch profile lookup for rendering a list of wallets. **Query params:** * `addresses` (required) - comma-separated 0x-addresses, 1 to 120 of them **Response (200):** ```json { "profiles": { "0xabc...": { "address": "0xabc...", "motocatTokenId": null, "xHandle": null, "xVerified": false } } } ``` Keyed by lowercased address; addresses with no row are simply absent from the map. **Response (400):** empty list, more than 120 addresses, or a malformed address. **Caching:** `Cache-Control: public, max-age=15, s-maxage=15, stale-while-revalidate=45` (CRON\_FAST tier - the only route on it). #### `POST /profile/pfp` (wallet-signed) [#post-profilepfp-wallet-signed] **Summary:** Set or clear the wallet's Motocat picture. **Request body:** ```json { "address": "0x...", "tokenId": "42", "signature": "0x..." } ``` * `tokenId: string | null` - decimal token id, or null to clear * `signature` - signature over the pfp message for this address, token id and chain id **Response (200):** the updated profile object. **Response (400):** missing or malformed fields. **Response (403):** signature does not authorize the change, or the wallet neither holds nor stakes that Motocat. **Caching:** `no-store`. #### `GET /profile/x/status` [#get-profilexstatus] **Summary:** Whether X linking is configured on this deploy, and the OAuth parameters a client needs to start the flow. **Response (200):** ```json { "enabled": false, "clientId": null, "authorizeUrl": "https://x.com/i/oauth2/authorize", "scopes": "users.read tweet.read" } ``` `clientId` is `null` while linking is disabled. `scopes` is one space-separated string. **Caching:** `no-store`. #### `POST /profile/x/link` (wallet-signed) [#post-profilexlink-wallet-signed] **Summary:** Complete the X OAuth flow and attach the handle to the wallet. **Request body:** `address`, `signature`, `state`, `code`, `codeVerifier`, `redirectUri`. **Response (200):** the updated profile object with `xVerified: true`. **Response (400):** missing or malformed fields. **Response (403):** signature does not authorize the change. **Response (502):** the code exchange with X failed. **Response (503):** X linking is disabled on this deploy. **Caching:** `no-store`. #### `POST /profile/x/unlink` (wallet-signed) [#post-profilexunlink-wallet-signed] **Summary:** Detach the linked handle. **Request body:** `address`, `signature`. **Response (200):** the updated profile object with `xHandle: null`, `xVerified: false`. **Response (400):** missing or malformed fields. **Response (403):** signature does not authorize the change. **Caching:** `no-store`. *** ### creator.ts [#creatorts] #### `GET /creator/token/{token}` [#get-creatortokentoken] **Summary:** Token creator info and fee accruals for a specific token. **Path params:** * `token` - token address **Response (200):** ```json { "token": "0x...", "creator": "0x...", "disabled": false, "feesToDate": [ { "quoteAsset": "0x...", "amount": "1000000000000000000", "usd": "2510.31" } ] } ``` **Response (200, no creator):** ```json { "token": "0x...", "creator": null, "disabled": false, "feesToDate": [] } ``` This is what a token with no recorded creator fees answers. **Response fields:** * `creator: string | null` - creator address or null * `disabled: boolean` - whether creator fees are disabled * `feesToDate[].amount: string` - Wei * `feesToDate[].usd: string | null` - write-time USD or null (frozen at accrual) **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Error responses:** 400 (invalid token). *** #### `GET /creator/{address}` [#get-creatoraddress] **Summary:** Creator dashboard - lifetime earnings breakdown (token x quote asset). **Path params:** * `address` - creator address **Response (200):** ```json { "address": "0x...", "tokens": [ { "token": "0x...", "disabled": false, "venue": "deployer", "lastClaimAt": 1723456000, "usdEarned": "500.00", "quotes": [ { "quoteAsset": "0x...", "earned": "1000000000000000000", "usdEarned": "500.00", "swaps": 120, "claimed": "500000000000000000", "unclaimed": "500000000000000000" } ] } ], "series": [ { "day": 20310, "usdEarned": "12.50", "swaps": 4 } ], "totalUsdEarned": "500.00" } ``` **Response (200, no earnings):** `{ "address": "0x...", "tokens": [], "series": [], "totalUsdEarned": "0" }`. **Response fields:** * `address: string` - the creator address (top-level identifier; there is no `creator` field on this route) * `tokens[].disabled: boolean` - whether creator fees are disabled for the token * `tokens[].venue: "motofun" | "deployer"` - which surface launched the token. `deployer` marks coins from the retired launch contract. Both accrue into the same vault, so this only labels the row; it never changes how the claim is made * `tokens[].lastClaimAt: number | null` - Unix seconds of the last claim, or null * `tokens[].usdEarned: string` - sum over the token's quotes * `quotes[].earned: string` - total accrued (Wei) * `quotes[].usdEarned: string` - frozen write-time USD sum (never re-priced); unpriced accruals contribute 0 * `quotes[].swaps: number` - fee-paying swap count for this quote asset * `quotes[].claimed, unclaimed: string` - claimed via on-chain withdrawal + remaining * `series[]: { day, usdEarned, swaps }` - ascending by day * `totalUsdEarned: string` - lifetime total across every token **Caching:** `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` (BLOCK tier). **Error responses:** 400 (invalid address). *** ### pve.ts [#pvets] #### `GET /pve/token/{token}` [#get-pvetokentoken] **Summary:** A token's full PvE escrow state: received / distributed / escrowed, the credit (null until `PveCredited`), held-pending amount, claim totals, and per-creator receipts. **Response fields (strict):** `token`, `vault`, `received`, `distributed`, `escrowed`, `receivedUsd | null`, `receipts`, `distributions`, `lastBlock | null`, `credit: { amount, totalStaked, creditBlock } | null`, `heldPending`, `claimedTotal`, `claims`, `creators[]: { creator, received, receivedUsd | null, receipts }`. `vault: Address` is the vault instance holding this token's escrow (claims target it). Rows indexed before the column existed carry no vault of their own and fall back to the deploy's default PvE vault, so the field is always an address and never null. **Caching:** BLOCK tier, token-scoped version. #### `GET /pve/creator/{address}` [#get-pvecreatoraddress] **Summary:** A creator's PvE receipts across their launched tokens. **Response fields:** `address`, `tokens[]: { token, received, distributed, escrowed, receivedUsd | null, receipts }`, `totalReceivedUsd | null`. **Caching:** BLOCK tier, address-scoped version. #### `GET /pve/wallet/{address}` [#get-pvewalletaddress] **Summary:** A wallet's PvE claims: per token, the credit and this wallet's claim (null while unclaimed - eligibility and the live claimable amount come from `PveVault.claimable` on chain). **Response fields:** `address`, `tokens[]: { token, vault, credit: { amount, totalStaked, creditBlock }, creditTimestamp, claimed: { amount, txHash, blockNumber, timestamp } | null, ethPrice?: string | null, decimals?: number | null }`. The optional `ethPrice` (WETH per whole token, scaled by 1e18) and `decimals` let a client value the share without a second call. `vault: Address` is the vault instance that credited the token - the address `claim(token)` must target. **Caching:** BLOCK tier, address-scoped version. **Notes:** USD fields are frozen write-time snapshots (never re-priced). A wallet with no PvE credit answers with empty lists and zero amounts, and `vault` is the zero address. *** ## Internal and non-public routes [#internal-and-non-public-routes] The backend also mounts operational routes that are **NOT for integrators**: deep health checks, scheduled jobs, admin controls, an indexer webhook, compliance checks for the web app, and the web app's own RPC proxy. They are token-gated or rate-limited for the app and they change without notice. Anything the host answers outside this page is not a support commitment. Bring your own RPC endpoint for chain reads. *** ## Summary [#summary] * **65 public endpoints** on this page: 61 reads and 4 wallet-signed writes * **Cursor pagination** (keyset) for list endpoints; scope-bound HMAC validation. Three per-token and per-pool lists take `?page=` * **Cache tiers** from 1s (HEARTBEAT) to 300s (STATIC) * **Wire types**: decimal-dollar strings, Wei strings, lowercased 0x-addresses * **Error envelope, two shapes**: handler errors return `{ "error": "message" }` (400/403/404/503); schema validation failures return the validator's default `{ "success": false, "error": { "name": "ZodError", … } }` * **ETag revalidation** on cached GETs Build against the routes on this page. Anything else the host answers is operational and can change without notice. # Motoswap Contracts - Full Inventory (/dev/reference/contracts-full-inventory) Every signature on this page was checked against the current contracts source. Deployment status is per contract. The [deployment status table](/dev/addresses#deployment-status-on-ethereum-chain-1) on the addresses page says which addresses the site pins and which you read from `GET /config`. A key that reads the zero address there is unset for the current phase. Read addresses from `/config`. Do not integrate against a zero address. Some deployed proxies run an implementation older than this source. Where a function is in the source but not on the deployed implementation, the entry says "not live yet, arrives with a contract upgrade". Check the verified implementation on a block explorer before you call one of those. ### MotoSwapERC20 (packages/contracts/src/dex/core/MotoSwapERC20.sol) [#motoswaperc20-packagescontractssrcdexcoremotoswaperc20sol] LP token for Motoswap pairs with ERC-2612 permit. Fork-safe DOMAIN\_SEPARATOR computed on-demand from chainid + address; no constructor since pairs are minimal proxies. **Constants / Immutables** * `name = "Motoswap V1"` * `symbol = "MOTO-V1-LP"` * `decimals = 18` * `PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9` **State Variables** * `totalSupply: uint256` - total LP supply * `balanceOf[address => uint256]` - LP balance per address * `allowance[address => mapping(address => uint256)]` - ERC20 approval allowances * `nonces[address => uint256]` - EIP-712 permit nonces **External / Public Functions** * `DOMAIN_SEPARATOR() view returns (bytes32)` - EIP-712 domain separator, computed from block.chainid and address(this) * `approve(address spender, uint256 value) returns (bool)` - ERC20 approve * `transfer(address to, uint256 value) returns (bool)` - ERC20 transfer * `transferFrom(address from, address to, uint256 value) returns (bool)` - ERC20 transferFrom; allows unlimited if prior approval is max uint256 * `permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)` - EIP-2612 permit; signs (owner, spender, value, nonces\[owner]++, deadline); reverts if expired or signature invalid **Events** * `event Approval(indexed address owner, indexed address spender, uint256 value)` * `event Transfer(indexed address from, indexed address to, uint256 value)` **Notes** * `_NAME_HASH = keccak256("Motoswap V1")` must remain in sync with `name` for permit signatures to validate. The EIP-712 domain is `name = "Motoswap V1"`, `version = "1"`, chain id, pair address **Reverts** * `"MotoSwap: EXPIRED"` - `permit` past its `deadline` * `"MotoSwap: INVALID_SIGNATURE"` - `permit` signature does not recover to `owner` * Balance and allowance shortfalls revert as an arithmetic panic (`0x11`), not a string. `transfer` and `transferFrom` never return `false` * Pairs never run the constructor, so all state is zero-initialized; `DOMAIN_SEPARATOR()` reads live from block.chainid and address(this) *** ### MotoSwapFactory (packages/contracts/src/dex/core/MotoSwapFactory.sol) [#motoswapfactory-packagescontractssrcdexcoremotoswapfactorysol] Deploys Motoswap pairs as ERC-1967 beacon proxies at deterministic CREATE2 addresses. All pairs delegate through a single upgradeable beacon. The factory is itself a UUPS proxy. **Constructor / Init** * `constructor()` - disables initializers * `initialize(address _feeToSetter)` - deploy the pair logic, create the beacon, seed the fee; `_feeToSetter` is admin and upgrade authority, and the factory itself owns the beacon; reverts if zero. The contract default seeded here is `swapFeeBps = 100`. `feeTo` is left unset. The deploy scripts then call `setSwapFee(30)` and `setFeeTo(address(0))`, which is a deploy-time setting, not a contract constant **Constants / Immutables** * `MAX_SWAP_FEE_BPS = 200` - hard cap on pair swap fee (2.00%) **State Variables** * `beacon: address` - UpgradeableBeacon pointing to pair logic * `pairCodeHash, INIT_CODE_PAIR_HASH: bytes32` - CREATE2 init-code-hash for beacon proxies (same value, kept in sync) * `feeTo: address` - protocol fee recipient (zero = no protocol fee; LPs get 100% of pair fee) * `feeToSetter: address` - admin role (two-step) * `pendingFeeToSetter: address` - proposed next feeToSetter * `swapFeeBps: uint256` - the pair swap fee in bps, one global value every pair reads live on each swap. Contract default 100, deploy scripts set 30, the admin can move it anywhere from 0 to `MAX_SWAP_FEE_BPS`. Read the value live from the factory rather than assuming either number * `getPair[address => mapping(address => address)]` - pair address lookup by (token0, token1) * `allPairs: address[]` - all deployed pair addresses * `quoteRegistry: address` - optional quote-asset registry; when set, `createPair` requires one side to be a quote asset * `swapAllowed[address => bool]` - the swap perimeter. Only listed addresses may call `MotoSwapPair.swap` or the core router's swap entrypoints. The deploy seeds exactly two: `MotoSwapRouter02` and `FeeRouter`. Outside integrators swap through the `FeeRouter` **External / Public Functions** * `swapConfig(address account) view returns (bool allowed, uint256 feeBps)` - `swapAllowed[account]` and `swapFeeBps` in one call; what every pair reads on each swap * `implementation() view returns (address)` - current pair logic implementation * `upgradePairImpl(address newPairImpl)` - retarget beacon at new pair logic (one upgrade applies to EVERY live pair); onlyFeeToSetter * `renouncePairUpgradeability()` - freeze pair logic forever by renouncing beacon ownership; one-way; onlyFeeToSetter * `renounceUpgradeability()` - freeze factory implementation; one-way; onlyFeeToSetter * `setSwapAllowed(address account, bool allowed)` - governance function; onlyFeeToSetter; emits SwapAllowedSet. It maintains the protocol's own perimeter (the core router and the `FeeRouter`) and doubles as the halt lever. It is not an integrator path: outside integrators route through the `FeeRouter` * `setSwapAllowedBatch(address[] calldata accounts, bool allowed)` - governance function; batch form of setSwapAllowed, used to seed the perimeter at deploy * `setSwapFee(uint256 _swapFeeBps)` - set live pair fee (up to MAX\_SWAP\_FEE\_BPS); onlyFeeToSetter; emits SwapFeeSet * `allPairsLength() view returns (uint256)` - number of pairs deployed * `createPair(address tokenA, address tokenB) returns (address pair)` - deploy a new pair; requires both tokens deployed, checks quote-registry gate if set, reverts if pair exists; emits PairCreated * `setQuoteRegistry(address _quoteRegistry)` - set/unset quote-registry gate; onlyFeeToSetter * `setFeeTo(address _feeTo)` - set protocol fee recipient, or switch the protocol's LP-fee share off with zero; onlyFeeToSetter; emits FeeToSet * `setFeeToSetter(address _feeToSetter)` - step 1 of two-step: propose new feeToSetter; onlyFeeToSetter; emits FeeToSetterTransferStarted * `acceptFeeToSetter()` - step 2: claim the feeToSetter role if you are the pending setter; emits FeeToSetterTransferred **Events** * `event FeeToSet(indexed address previousFeeTo, indexed address newFeeTo)` * `event SwapFeeSet(uint256 swapFeeBps)` * `event QuoteRegistrySet(indexed address quoteRegistry)` * `event SwapAllowedSet(indexed address account, bool allowed)` * `event PairCreated(indexed address token0, indexed address token1, address pair, uint256)` \[from IMotoSwapFactory] - the unnamed fourth field is `allPairs.length` after the push * `event FeeToSetterTransferStarted(indexed address previousFeeToSetter, indexed address newFeeToSetter)` \[from IMotoSwapFactory] * `event FeeToSetterTransferred(indexed address previousFeeToSetter, indexed address newFeeToSetter)` \[from IMotoSwapFactory] **Reverts (string reverts, not custom errors)** * `"MotoSwap: ZERO_ADDRESS"` - `initialize` received a zero `_feeToSetter`, or `token0` is zero after sort in `createPair` * `"MotoSwap: FORBIDDEN"` - caller is not `feeToSetter` (or not `pendingFeeToSetter` on `acceptFeeToSetter`) * `"MotoSwap: FEE_TOO_HIGH"` - `setSwapFee` above `MAX_SWAP_FEE_BPS` * `"MotoSwap: IDENTICAL_ADDRESSES"` - `createPair` with `tokenA == tokenB` * `"MotoSwap: TOKEN_NOT_DEPLOYED"` - one side has no code * `"MotoSwap: NO_QUOTE"` - createPair with a quote registry set and neither token a registered quote asset (every pool must have a quote side) * `"MotoSwap: PAIR_EXISTS"` - pair already deployed for the sorted token order MotoSwapFactory declares no custom errors. **Notes** * Pairs are Solady `LibClone` ERC-1967 beacon proxies deployed with CREATE2, `salt = keccak256(abi.encodePacked(token0, token1))`. The init code hash depends on the deployment's beacon address, so it is different on every chain and is NOT the Uniswap V2 constant. Read `pairCodeHash()` (alias `INIT_CODE_PAIR_HASH()`) from the factory. Never hardcode it * `feeToSetter` is never renounced. It keeps the fee rate, `feeTo`, the quote registry and the swap perimeter for the life of the deployment *** ### MotoSwapPair (packages/contracts/src/dex/core/MotoSwapPair.sol) [#motoswappair-packagescontractssrcdexcoremotoswappairsol] Uniswap-V2-style AMM pair; each pair is an ERC-1967 beacon proxy. Uses transient storage reentrancy lock (EIP-1153), computes fees live from factory.swapFeeBps, streams prices to TWAP accumulators. **Constructor** * `constructor(address _factory)` - bakes immutable factory address **Constants** * `MINIMUM_LIQUIDITY = 1000` * Transient lock slot: `0x1dce58e7bd12b13f77dfd7a57473337c10d495bb3ac0b21edffa2e9514fa5402` **State Variables** * `factory: address` - immutable factory * `token0, token1: address` - sorted token addresses * `reserve0, reserve1: uint112` - cached reserves * `blockTimestampLast: uint32` - last block.timestamp a Sync was emitted * `price0CumulativeLast, price1CumulativeLast: uint256` - TWAP numerators * `kLast: uint256` - reserve0 \* reserve1 at last liquidity event (for fee accrual) **External / Public Functions** * `initialize(address _token0, address _token1)` - one-shot initialization; onlyFactory; reverts if already initialized or called by non-factory * `getReserves() view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast)` - fetch cached reserves and timestamp * `mint(address to) lock returns (uint256 liquidity)` - deposit proportional amounts of token0/token1, mint LP to `to`, optionally collect protocol fee; low-level, requires tokens already transferred * `burn(address to) lock returns (uint256 amount0, uint256 amount1)` - remove liquidity: burn LP from pair, transfer tokens to `to`; requires LP already transferred * `swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) lock` - atomic swap; reads `factory.swapConfig(msg.sender)`, requires the caller to be on the swap perimeter, enforces the constant-product check with the live fee; `data` MUST be empty, there is no flash-swap callback; emits Swap * `skim(address to) lock` - send the balance above reserves to `to`. Does not emit Sync * `sync() lock` - resync reserves to balances **Events** * `event Mint(indexed address sender, uint256 amount0, uint256 amount1)` * `event Burn(indexed address sender, uint256 amount0, uint256 amount1, indexed address to)` * `event Swap(indexed address sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, indexed address to)` * `event Sync(uint112 reserve0, uint112 reserve1)` * `event Transfer(indexed address from, indexed address to, uint256 value)` \[from MotoSwapERC20] * `event Approval(indexed address owner, indexed address spender, uint256 value)` \[from MotoSwapERC20] **Reverts (string reverts, not custom errors)** * `"MotoSwap: LOCKED"` - transient reentrancy lock held * `"MotoSwap: FORBIDDEN"` - onlyFactory on initialize * `"MotoSwap: ALREADY_INITIALIZED"` * `"MotoSwap: TRANSFER_FAILED"` - low-level token transfer returned false or reverted * `"MotoSwap: OVERFLOW"` - balance > uint112.max * `"MotoSwap: INSUFFICIENT_LIQUIDITY_MINTED"` * `"MotoSwap: INSUFFICIENT_LIQUIDITY_BURNED"` * `"MotoSwap: SWAPPER_NOT_ALLOWED"` - caller not in `factory.swapAllowed` (protocol-fee perimeter) * `"MotoSwap: INSUFFICIENT_OUTPUT_AMOUNT"` - zero output * `"MotoSwap: INSUFFICIENT_LIQUIDITY"` - not enough reserves * `"MotoSwap: INVALID_TO"` - `to` is token0 or token1 * `"MotoSwap: FLASH_SWAPS_CLOSED"` - `swap` called with non-empty `data` * `"MotoSwap: INSUFFICIENT_INPUT_AMOUNT"` - zero input * `"MotoSwap: K"` - constant product violated after fee **Notes** * Flash swaps are closed by an enforced `require(data.length == 0)`. `motoSwapCall` is never invoked. `IMotoSwapCallee` remains in the source tree as an interface file only * `mint` runs `_mintFee` before `_mint(to, liquidity)` and the `Mint` event has no recipient, the same order as Uniswap V2. While `factory.feeTo()` is non-zero, a `Transfer(0x0 -> feeTo)` precedes the user's `Transfer(0x0 -> to)`. See the watch-events guide * `_mintFee` share: `liquidity = totalSupply * (rootK - rootKLast) * 3 / (rootK * 2 + rootKLast * 3)`, about 60% of fee growth to `feeTo` * Swap fee is read LIVE from factory; a fee change mid-swap reprices it, bounded by MAX\_SWAP\_FEE\_BPS * Protocol-fee `_mintFee` no-ops entirely when `feeTo == address(0)`; deployed config sets feeTo=0, so 100% of the pair fee (30 bps) accrues to LPs * Reentrancy lock uses EIP-1153 transient storage; requires evm\_version=cancun *** ### MotoSwapLibrary (packages/contracts/src/dex/periphery/MotoSwapLibrary.sol) [#motoswaplibrary-packagescontractssrcdexperipherymotoswaplibrarysol] All functions are `internal` - the library is compiled into its callers, not integrator-callable. Use `MotoSwapRouter02`'s public `getAmountsOut`/`getAmountsIn`/`quote` instead. Pure/view helpers for swaps, reserves, and CREATE2 pair addresses. **Internal functions** * `sortTokens(address tokenA, address tokenB) pure returns (address token0, address token1)` - sort tokens and validate * `pairFor(address factory, address tokenA, address tokenB) view returns (address pair)` - compute CREATE2 address; reads pairCodeHash() live from factory * `getReserves(address factory, address tokenA, address tokenB) view returns (uint256 reserveA, uint256 reserveB)` - fetch reserves, sorted to match token order * `quote(uint256 amountA, uint256 reserveA, uint256 reserveB) pure returns (uint256 amountB)` - proportional amount: amountB = (amountA \* reserveB) / reserveA * `getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut, uint256 feeBps) pure returns (uint256 amountOut)` - constant-product output after fee * `getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut, uint256 feeBps) pure returns (uint256 amountIn)` - constant-product input for target output after fee * `getAmountsOut(address factory, uint256 amountIn, address[] memory path) view returns (uint256[] memory amounts)` - route amounts through a path * `getAmountsIn(address factory, uint256 amountOut, address[] memory path) view returns (uint256[] memory amounts)` - route amounts in reverse through a path **Reverts (string reverts, not custom errors)** * `"MotoSwapLibrary: IDENTICAL_ADDRESSES"` - tokenA == tokenB * `"MotoSwapLibrary: ZERO_ADDRESS"` - token0 is zero after sort * `"MotoSwapLibrary: INSUFFICIENT_AMOUNT"` - `quote` called with `amountA == 0` * `"MotoSwapLibrary: INSUFFICIENT_INPUT_AMOUNT"` - `getAmountOut` called with `amountIn == 0` * `"MotoSwapLibrary: INSUFFICIENT_OUTPUT_AMOUNT"` - `getAmountIn` called with `amountOut == 0` * `"MotoSwapLibrary: INSUFFICIENT_LIQUIDITY"` - reserves are zero * `"MotoSwapLibrary: INVALID_PATH"` - path shorter than 2 tokens *** ### MotoSwapRouter02 (packages/contracts/src/dex/periphery/MotoSwapRouter02.sol) [#motoswaprouter02-packagescontractssrcdexperipherymotoswaprouter02sol] Stateless router for adding/removing liquidity and swapping. Modeled on Uniswap V2 Router02. Swap entrypoints are gated on factory.swapAllowed (protocol-fee perimeter). **Constructor / Init** * `constructor(address _factory, address _WETH)` - immutable factory and WETH **Constants** * `factory, WETH: address` - immutable **External / Public Functions** *Liquidity (permissionless):* * `addLiquidity(address tokenA, address tokenB, uint256 amountADesired, uint256 amountBDesired, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline) ensure returns (uint256 amountA, uint256 amountB, uint256 liquidity)` - add liquidity for token-token pair * `addLiquidityETH(address token, uint256 amountTokenDesired, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline) payable ensure returns (uint256 amountToken, uint256 amountETH, uint256 liquidity)` - add liquidity for token-ETH pair * `removeLiquidity(address tokenA, address tokenB, uint256 liquidity, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline) ensure returns (uint256 amountA, uint256 amountB)` - remove liquidity for token-token pair * `removeLiquidityETH(address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline) ensure returns (uint256 amountToken, uint256 amountETH)` - remove liquidity for token-ETH pair * `removeLiquidityWithPermit(address tokenA, address tokenB, uint256 liquidity, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline, bool approveMax, uint8 v, bytes32 r, bytes32 s) returns (uint256 amountA, uint256 amountB)` - remove liquidity with EIP-2612 permit * `removeLiquidityETHWithPermit(address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline, bool approveMax, uint8 v, bytes32 r, bytes32 s) returns (uint256 amountToken, uint256 amountETH)` * `removeLiquidityETHSupportingFeeOnTransferTokens(address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline) ensure returns (uint256 amountETH)` - for fee-on-transfer tokens * `removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline, bool approveMax, uint8 v, bytes32 r, bytes32 s) returns (uint256 amountETH)` *Swaps (onlyPerimeter):* * `swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) ensure onlyPerimeter returns (uint256[] memory amounts)` - exact input * `swapTokensForExactTokens(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to, uint256 deadline) ensure onlyPerimeter returns (uint256[] memory amounts)` - exact output * `swapExactETHForTokens(uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) payable ensure onlyPerimeter returns (uint256[] memory amounts)` - path\[0] must be WETH * `swapTokensForExactETH(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to, uint256 deadline) ensure onlyPerimeter returns (uint256[] memory amounts)` - path\[last] must be WETH * `swapExactTokensForETH(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) ensure onlyPerimeter returns (uint256[] memory amounts)` - path\[last] must be WETH * `swapETHForExactTokens(uint256 amountOut, address[] calldata path, address to, uint256 deadline) payable ensure onlyPerimeter returns (uint256[] memory amounts)` - path\[0] must be WETH * `swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) ensure onlyPerimeter` - for fee-on-transfer; measures realized balance deltas * `swapExactETHForTokensSupportingFeeOnTransferTokens(uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) payable ensure onlyPerimeter` * `swapExactTokensForETHSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) ensure onlyPerimeter` *Views (pure/view helpers):* * `quote(uint256 amountA, uint256 reserveA, uint256 reserveB) pure returns (uint256 amountB)` - delegates to MotoSwapLibrary.quote * `getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) view returns (uint256 amountOut)` - reads live factory.swapFeeBps * `getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) view returns (uint256 amountIn)` - reads live factory.swapFeeBps * `getAmountsOut(uint256 amountIn, address[] memory path) view returns (uint256[] memory amounts)` * `getAmountsIn(uint256 amountOut, address[] memory path) view returns (uint256[] memory amounts)` **Events** (None emitted by router; pairs emit Swap/Mint/Burn/Transfer) **Reverts (string reverts, not custom errors)** * `"MotoSwapRouter: EXPIRED"` - `deadline < block.timestamp` * `"MotoSwapRouter: DESIRED_BELOW_MIN"` - an `amount*Min` above its `amount*Desired` on add-liquidity * `"MotoSwapRouter: INSUFFICIENT_OUTPUT_AMOUNT"` * `"MotoSwapRouter: EXCESSIVE_INPUT_AMOUNT"` * `"MotoSwapRouter: INSUFFICIENT_A_AMOUNT"`, `"MotoSwapRouter: INSUFFICIENT_B_AMOUNT"` * `"MotoSwapRouter: INVALID_PATH"` * `"MotoSwapRouter: SWAPPER_NOT_ALLOWED"` - onlyPerimeter guard: `factory.swapAllowed(msg.sender)` is false * `"MotoSwapRouter: LP_TRANSFER_FAILED"` - the pair's `transferFrom` returned false. `MotoSwapERC20` reverts with a panic instead, so in practice a missing LP allowance shows as panic `0x11` **Notes** * Every swap entrypoint is gated on `factory.swapAllowed(msg.sender)`. Only the `FeeRouter` (and the router itself) is admitted, so an outside caller always reverts here. The exact-output entrypoints exist on this router but are reachable by nobody outside: the `FeeRouter` is exact-input only * `addLiquidity` creates a missing pair through `factory.createPair`, which requires both tokens to have code and one side to be a quote asset * Fee-on-transfer variants measure realized balance deltas instead of nominal amounts * Router keeps low-level TransferHelper (not OZ SafeERC20) matching the reference *** ### FeeRouter (packages/contracts/src/fees/FeeRouter.sol) [#feerouter-packagescontractssrcfeesfeeroutersol] The swap entrypoint for users and outside integrators. Skims the protocol fee (`protocolFeeBps`) from the quote leg, forwards it to the Collector, then routes through the core router. Exact input only: there is no exact-output entrypoint and no quote view. Supports three path types: both endpoints quote, one endpoint quote, or a split at the first quote asset inside the path. A creator fee is skimmed on top when a path endpoint has an active creator. UUPS proxy, Ownable2Step. **Constructor / Init** * `constructor()` - disables initializers * `initialize(address router_, address registry_, address collector_, address permit2_, uint256 protocolFeeBps_, address owner_)` - wire router, quote registry, collector, Permit2, starting fee; owner-only upgrade auth; reverts ZeroAddress on a zero router, registry, collector or owner, FeeTooHigh above the cap **Constants** * `LAUNCH_PROTOCOL_FEE_BPS = 70` - the launch value of the protocol fee (0.70%). A constant for reference; the live value is `protocolFeeBps` * `MAX_PROTOCOL_FEE_BPS = 100` - hard cap on protocol fee (1.00%) * `MAX_CREATOR_FEE_BPS = 50` - hard cap on creator fee (0.50%) * `BPS_DENOM = 10_000` * `MAX_SPLIT_LEGS = 4` - max legs in one split call * `SwapLeg` struct: `(uint256 amountIn, address[] path)` - one leg of a split fill **State Variables** * `router: IMotoSwapRouter02` - core router * `registry: IQuoteAssetRegistry` - quote-asset registry * `collector: address` - fee recipient * `WETH: address` - read from the router at initialize * `permit2: IPermit2` - Permit2 for the two signature entrypoints. Zero means they are not available (they revert Permit2NotSet) and the plain approve path applies * `protocolFeeBps: uint256` - live fee (settable, capped at MAX\_PROTOCOL\_FEE\_BPS) * `creatorRegistry: ICreatorFeeRegistry` - optional creator-fee config * `creatorVault: ICreatorFeeVault` - creator-fee payout vault * `creatorFeeBps: uint256` - creator-fee rate (settable, capped) **External / Public Functions** *Config (onlyOwner):* * `setProtocolFeeBps(uint256 bps)` - retune live protocol fee * `setCreatorFeeConfig(address registry_, address vault_, uint256 bps)` - wire creator-fee components (zero bps or registry disables it); with `bps > 0` the vault must already list this router as a depositor, else VaultDoesNotAdmitRouter * `setPermit2(address permit2_)` - re-point or disable signature entrypoints * `renounceUpgradeability()` - freeze implementation; one-way * `rescueERC20(address token, address to) onlyOwner nonReentrant` - sweep stranded tokens * `rescueETH(address to) onlyOwner nonReentrant` - sweep stranded ETH *Queries (view):* * `feeOnOutput(address[] calldata path) view returns (bool)` - true if the fee is taken from the output leg (else input). Looks only at `path[0]` and the last element; returns `true` for a path with no quote endpoint, where it means nothing. This is the only swap-related view: to quote, take `MotoSwapRouter02.getAmountsOut` and apply the fee legs yourself (see the swap-via-fee-router guide) *Token→Token Swaps:* * `swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) nonReentrant returns (uint256[] memory amounts)` - exact input; pulls via `safeTransferFrom` * `swapExactTokensForTokensPermit2(IPermit2.PermitTransferFrom calldata permit, bytes calldata signature, uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) nonReentrant returns (uint256[] memory amounts)` - same, input via Permit2 signature * `swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) nonReentrant returns (uint256 delivered)` - for fee-on-transfer tokens; measures realized balances *ETH→Token Swaps:* * `swapExactETHForTokens(uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) payable nonReentrant returns (uint256[] memory amounts)` - path\[0] must be WETH * `swapExactETHForTokensSupportingFeeOnTransferTokens(uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) payable nonReentrant returns (uint256 delivered)` *Token→ETH Swaps:* * `swapExactTokensForETH(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) nonReentrant returns (uint256[] memory amounts)` - path\[last] must be WETH; pulls via safeTransferFrom * `swapExactTokensForETHPermit2(IPermit2.PermitTransferFrom calldata permit, bytes calldata signature, uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) nonReentrant returns (uint256[] memory amounts)` * `swapExactTokensForETHSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) nonReentrant returns (uint256 net)` *Atomic Split Fills:* * `swapExactTokensForTokensSplit(SwapLeg[] calldata legs, uint256 minTotalOut, address to, uint256 deadline) nonReentrant returns (uint256[] memory legOuts, uint256 totalOut)` - token-in; each leg shares path\[0]/path\[last]; enforces minTotalOut on sum; emits one Swap per leg * `swapExactETHForTokensSplit(SwapLeg[] calldata legs, uint256 minTotalOut, address to, uint256 deadline) payable nonReentrant returns (uint256[] memory legOuts, uint256 totalOut)` - eth-in; all legs path\[0] == WETH * `swapExactTokensForETHSplit(SwapLeg[] calldata legs, uint256 minTotalOut, address to, uint256 deadline) nonReentrant returns (uint256[] memory legOuts, uint256 totalOut)` - token-in, eth-out; all legs path\[last] == WETH **Events** * `event Swap(indexed address user, indexed address tokenIn, indexed address tokenOut, uint256 amountIn, uint256 amountOut, address feeToken, uint256 feeAmount)` - `user` is `msg.sender` of the FeeRouter call (the payer), never `to`. `amountIn` is the gross input, `amountOut` the net delivered to `to`, `feeAmount` the protocol fee only, in `feeToken` * `event Permit2Set(address permit2)` * `event ProtocolFeeSet(uint256 protocolFeeBps)` * `event CreatorFee(indexed address token, indexed address creator, address quoteAsset, uint256 amount)` - creator fee skim * `event CreatorFeeConfigSet(address registry, address vault, uint256 creatorFeeBps)` * `event Rescued(indexed address token, indexed address to, uint256 amount)` **Custom Errors** * `error InvalidPath()` - swap path invalid (ETH routes must start/end with WETH) * `error InsufficientOutput()` - net out below amountOutMin * `error EthTransferFailed()` - ETH transfer reverted or no receive * `error NoQuoteSide()` - swap route has no quote asset endpoint or mid-quote * `error FeeTooHigh()` - fee > MAX\_PROTOCOL\_FEE\_BPS or MAX\_CREATOR\_FEE\_BPS * `error ZeroAddress()` - initialize or config received zero address * `error Permit2NotSet()` - signature swap called but permit2 not wired * `error PermitTokenMismatch()` - Permit2 token != path\[0] * `error NoLegs()` - split called with empty legs array or zero-amount leg * `error TooManyLegs()` - legs > MAX\_SPLIT\_LEGS * `error LegMismatch()` - split legs don't share same endpoints * `error ValueMismatch()` - `swapExactETHForTokensSplit` called with `msg.value != totalIn` * `error VaultDoesNotAdmitRouter()` - `setCreatorFeeConfig` pointed a live creator fee at a vault that does not list this router as a depositor **Notes** * The protocol fee is charged once, on the quote leg only (or at the first quote asset inside the path if neither endpoint is a quote asset). With two quote endpoints the higher `quoteRank` side pays; a tie charges the output * The deadline is not checked here. `MotoSwapRouter02` checks it and reverts `"MotoSwapRouter: EXPIRED"` * `amountOutMin` is enforced on the net amount delivered, after the protocol fee and any creator fee * Returned `amounts`: `amounts[0]` is the gross input and the last element the net delivered. On a mid-path fee the array does not chain across the fee hop. `swapExactTokensForETH` and its Permit2 twin fill only the first and last elements. The fee-on-transfer entrypoints return one measured number * Permit2 twins exist for `swapExactTokensForTokens` and `swapExactTokensForETH` only. The fee-on-transfer and token-in split entrypoints need a normal allowance to this contract * `receive()` accepts ETH from the router and WETH only; anything else reverts EthTransferFailed * Both endpoints with an active creator pay the creator fee twice (each `creatorFeeBps` of the quote leg) * Creator fees are optional and charged on the SAME quote leg as the protocol fee * Entrypoints emit one Swap event per logical trade; split fills emit per-leg * The `SwapLeg` struct contains only `(amountIn, path[])` and relies on validation to ensure legs share `path[0]` and `path[last]` *** ### RakebackV2 (packages/contracts/src/fees/RakebackV2.sol) [#rakebackv2-packagescontractssrcfeesrakebackv2sol] Merkle-root Rakeback distribution. No signer and no hot key on the claim path: the backend derives one tree per weekly epoch from public chain data, an automated poster puts the root on chain, and a claim is a pure merkle proof. One tree per epoch with `token` folded into the leaf, but the money is accounted per quote asset, so declared totals, funding, activation, the claim bitmap and expiry are all keyed per `(epoch, token)` slice. UUPS proxy. **Constructor / Init** * `constructor()` - disables initializers * `initialize(address owner_, address treasury_, address moto_, address poster_, address guardian_, uint64 activationDelay_, address upgradeAuthority_)` - wire treasury destination, MOTO token, the poster and guardian roles, the activation delay and the upgrade authority (zero leaves upgrades with the owner); reverts `BadDelay()` if the delay is outside `[MIN_ACTIVATION_DELAY, MAX_ACTIVATION_DELAY]`. There is no router argument: `claimAsMoto` swaps through `feeRouter` **Constants** * `CLAIM_WINDOW = 30 days` - claimable span, measured from a slice's `Activated` timestamp (not from epoch close) * `MIN_ACTIVATION_DELAY`, `MAX_ACTIVATION_DELAY` - bounds on the live `activationDelay`. Read the live values from the contract * `MAX_TOKENS_PER_EPOCH = 8` - cap on tokens in one epoch's `postRoot` **State Variables** * `treasury: address` - sweep destination (settable) * `poster: address` - the automated role allowed to call `postRoot` (settable) * `guardian: address` - hot ops role allowed to `cancelRoot` a still-pending root (settable) * `moto: address` - MOTO token * `feeRouter: IFeeRouter` - set post-init by `setFeeRouter`; the swap venue for `claimAsMoto` * `activationDelay: uint64` - live delay between `postRoot` and the earliest `activate` * `epochRoots[uint256 => EpochRoot]` - epoch -> `(bytes32 root, uint64 postedAt)` * `slices[uint256 => mapping(address => TokenSlice)]` - epoch -> token -> `(uint256 declaredTotal, uint256 claimedTotal, uint64 activatedAt, bool expired)` * `outstanding[address => uint256]` - token -> unclaimed liability across all live slices; `sweep` must retain it * `activeOutstanding[address => uint256]` - the same over ACTIVE slices only; this is the activation funding gate * `totalDeclared[address => uint256]` - token -> everything ever declared; the liability-cap numerator * `lifetimeInflow[address => uint256]` - measured Collector inflow, credited by `syncInflow`; the cap ceiling * `accountedBalance[address => uint256]` - last-synced balance, so `syncInflow` credits only genuine new inflow **External / Public Functions** *Config (onlyOwner):* * `setFeeRouter(address feeRouter_)` - wire fee-paying swap front door post-init * `setPoster(address poster_)` - rotate the poster role * `setGuardian(address guardian_)` - rotate the guardian role * `setActivationDelay(uint64 delay_)` - retune the delay within its constant bounds * `setTreasury(address treasury_)` - set sweep destination * `expirePendingSlice(uint256 epoch, address token)` - release a slice that was posted but never activatable, once its window has fully lapsed and the pot still cannot fund it. Owner-only in the code, whatever its comment says *Upgrade path:* * `upgradeAuthority() view returns (address)` - the address that may upgrade; zero means the owner * `setUpgradeAuthority(address newAuthority)` - callable by the current authority (the owner while it is zero). Must be a contract, and once set can never go back to zero * `renounceUpgradeability()` - freeze implementation; one-way; gated on the upgrade authority *Views (view / pure):* * `isClaimed(uint256 epoch, address token, uint256 index) view returns (bool)` - the leaf's bitmap bit; the authority on whether a claim already landed * `epochEnd(uint256 epoch) pure returns (uint256)` - Sunday-anchored epoch boundary, pinned to the backend anchor *Publishing (poster / guardian):* * `postRoot(uint256 epoch, bytes32 root, address[] calldata tokens, uint256[] calldata declaredTotals) nonReentrant` - calls `syncInflow` per token itself, then publishes an epoch's root with its per-token declared totals; poster-gated; the epoch must be closed, tokens sorted and at most `MAX_TOKENS_PER_EPOCH`, each declared > 0, and `totalDeclared[token] + declared` must stay within the token's measured `lifetimeInflow`. That cap is the on-chain blast-radius bound on a compromised poster * `cancelRoot(uint256 epoch)` - void a still-PENDING root and rewind its books so a corrected root can be re-posted; guardian or owner; reverts `RootActive()` once any slice of the epoch has activated, so live claims cannot be rugged *Permissionless keeping:* * `syncInflow(address token)` - credit genuine Collector inflow into the liability-cap ceiling * `activate(uint256 epoch, address token)` - open claims for a posted slice; requires the activation delay to have elapsed AND the pot to cover every already-active slice for the token plus this one. That establishes `balance >= activeOutstanding` at activation, so an active claim can never fail on funds; an unfunded slice stays pending, meaning claims are delayed, never partial * `expireRoot(uint256 epoch, address token)` - after the claim window, release an activated slice's unclaimed remainder from `outstanding` so it becomes sweepable; reverts NotActivated for a slice that never activated *Claims:* * `claim(uint256 epoch, address token, uint256 index, address account, uint256 amount, bytes32[] calldata proof) nonReentrant returns (uint256)` - verify the leaf against the epoch root and pay `amount` to `account`. All-or-nothing: one bitmap bit, no partial claim, no cumulative counter. Delivery is permissionless, so anyone can push a claim to its rightful owner * `claimMany(uint256[] calldata epochs, address[] calldata tokens, uint256[] calldata indexes, address[] calldata accounts, uint256[] calldata amounts, bytes32[][] calldata proofs) nonReentrant returns (uint256 total)` - batch form, ATOMIC: any single entry reverting reverts the whole call, so send only entries known to be claimable * `claimAsMoto(uint256 epoch, address token, uint256 index, uint256 amount, bytes32[] calldata proof, uint256 minMotoOut, address[] calldata path, uint256 deadline) nonReentrant returns (uint256 motoOut)` - claim your OWN leaf (the proof must bind `msg.sender`) and swap it to MOTO through the FeeRouter, so the swap pays the protocol fee; `path` must run token→MOTO. When `token` is MOTO the swap is skipped *Sweep:* * `sweep(address token, uint256 amount) onlyOwner nonReentrant` - reclaim only the SURPLUS above the token's live liability, to the treasury address only; bounded by `ExceedsSweepable()` **Events** * `event RootPosted(indexed uint256 epoch, bytes32 root, uint64 postedAt)` * `event SlicePosted(indexed uint256 epoch, indexed address token, uint256 declaredTotal)` * `event Activated(indexed uint256 epoch, indexed address token, uint64 activatedAt)` * `event Claimed(indexed uint256 epoch, indexed address token, uint256 index, address account, uint256 amount)` * `event ClaimedAsMoto(indexed address user, indexed address token, indexed uint256 epoch, uint256 amountIn, uint256 motoOut)` * `event Expired(indexed uint256 epoch, indexed address token, uint256 remainder)` * `event PendingSliceExpired(indexed uint256 epoch, indexed address token, uint256 declared)` * `event RootCancelled(indexed uint256 epoch)` * `event InflowSynced(indexed address token, uint256 delta, uint256 lifetimeInflow)` * `event Swept(indexed address token, uint256 amount)` * `event PosterSet(address poster)`, `event GuardianSet(address guardian)`, `event ActivationDelaySet(uint64 activationDelay)`, `event TreasurySet(address treasury)`, `event FeeRouterSet(address feeRouter)` **Custom Errors** * `error ZeroAddress()` - init received zero address * `error NotPoster()` / `error NotGuardian()` - role gate on postRoot / cancelRoot * `error BadDelay()` - activation delay outside `[MIN_ACTIVATION_DELAY, MAX_ACTIVATION_DELAY]` * `error EpochNotClosed()` - postRoot ran before the epoch ended * `error RootExists()` / `error NoRoot()` - an epoch already has a root / has none * `error TokensNotSorted()` / `error TooManyTokens()` / `error ZeroDeclared()` / `error ZeroRoot()` / `error BadPerToken()` - postRoot argument validation * `error ExceedsInflowCap()` - declared total would exceed the token's measured Collector inflow * `error RootPending()` - claim on a slice that has not activated yet * `error NotActivated()` - `expireRoot` on a slice that never activated * `error NotYetActivatable()` / `error NotFunded()` / `error AlreadyActivated()` / `error SliceNotFound()` - activate gates * `error RootActive()` - cancelRoot after a slice activated * `error InvalidProof()` - proof does not verify against the epoch root * `error AlreadyClaimed()` - the bitmap bit for that index is already set * `error Overclaim()` - the leaf would push `claimedTotal` past the slice's `declaredTotal`; the bound that stops a poisoned root draining the pot * `error RootExpired()` - past `activatedAt + CLAIM_WINDOW` * `error ClaimWindowOpen()` / `error SliceActivated()` / `error SliceStillActivatable()` / `error SliceIsFundable()` / `error AlreadyExpired()` - expiry-path gates * `error LengthMismatch()` - claimMany arrays different lengths * `error BadPath()` - claimAsMoto path invalid * `error ExceedsSweepable()` - sweep tried to dip into live liability **Notes** * The 50/25/25 vest is applied by the backend at post time and baked into the leaf `amount`. There is no read-time vest on chain: epoch x's published pool is `slice0(R[x]) + slice1(R[x-1]) + slice2(R[x-2])`, and the third slice is the remainder so the three sum to R exactly * Leaf = `keccak256(abi.encodePacked(uint256 index, address account, address token, uint256 amount))`, in a sorted-pair (commutative) tree matching OpenZeppelin `MerkleProof.verify`, with an odd trailing node promoted unhashed. This is NOT `StandardMerkleTree`'s double-hashed leaf. Leaves are ordered by token then account, and `index` is the global 0-based position * Security of the automated poster is on chain, not in a key: a root can never declare more than the token's measured inflow, it sits behind an activation delay, and it is cancellable by the guardian while pending. Cancel is harmless by construction, since the worst it can do is delay a good root * Creator fees never inflate Rakeback and Points (feeAmount in Swap event is protocol fee only, not creator fees) *** ### Collector (packages/contracts/src/fees/Collector.sol) [#collector-packagescontractssrcfeescollectorsol] Dynamic fee-distribution hub: receives 0.7% protocol fee from FeeRouter, splits across weighted buckets (staking notify, treasury, rakeback, buyback). Handles failed payouts via earmarks (heldFor). Supports per-token distribution rate-limiting. UUPS proxy. **Constructor / Init** * `initialize(address initialOwner, address stakingRecipient_, address treasury_, address rakeback_, address buybackBurn_, address upgradeAuthority_, address quoteRegistry_)` - seed 2:2:2:1 split (staking notify, treasury, rakeback, buyback), the upgrade authority (zero leaves upgrades with the owner) and the quote registry `distribute` is gated on **Constants** * `MAX_BUCKETS = 16` - max bucket count * `MAX_MIN_INTERVAL` - ceiling on `minInterval`. Read it from the contract **State Variables (Buckets)** * `Bucket[] buckets` - `` `{ address recipient, uint96 weight, bool notify }` ``; notify=true uses IStakingRewards.notifyReward * `totalWeight: uint256` - sum of all bucket weights **State Variables (Timing & Holds)** * `minInterval: uint256` - min seconds between distributes per token (0 = unrestricted) * `lastDistributed[address => uint256]` - last distribute timestamp for token * `heldFor[address => mapping(uint256 => uint256)]` - token -> bucket index -> earmark balance (payouts that reverted) * `reserved[address => uint256]` - token -> total held (excluded from distributable balance) * `heldIndexCount[uint256 => uint256]` - bucket index -> count of distinct tokens with earmarks there (strand guard) * `quoteRegistry: IQuoteAssetRegistry` - the quote-asset whitelist `distribute` is gated on **External / Public Functions** *Config (onlyOwner):* * `setBuckets(Bucket[] calldata newBuckets)` - replace bucket list atomically; reverts if shrink would strand live earmarks (HeldWouldStrand) * `setBucketRecipient(uint256 index, address recipient, bool notify)` - re-point one bucket's destination and state how it is paid (`notify = true` calls `notifyReward` on it) * `addBucket(address recipient, uint96 weight, bool notify)` - append one bucket * `setMinInterval(uint256 minInterval_)` - set per-token rate limit; IntervalTooLong above `MAX_MIN_INTERVAL` * `forceReleaseHeld(address token, uint256 index) returns (uint256 amount)` - abandon an earmark whose payout cannot succeed: clears the accounting without paying and returns the value to the ordinary split; emits HeldForceReleased *Upgrade path:* * `upgradeAuthority() view returns (address)`, `setUpgradeAuthority(address newAuthority)` - same split as RakebackV2 * `renounceUpgradeability()` - freeze implementation; one-way; gated on the upgrade authority *Distribution (permissionless, rate-limited per token):* * `distribute(address token) nonReentrant returns (uint256 paid)` - `token` must be a registered quote asset (NotQuoteAsset otherwise); split distributable balance across buckets pro-rata; skips buckets whose payout reverts (earmarks them); enforces minInterval per token; emits Distributed * `releaseHeld(address token, uint256 index) nonReentrant returns (uint256 amount)` - pay a bucket's earmark once available; retries the payout (reverts if it fails again) *Views:* * `bucketsLength() view returns (uint256)` * `distributable(address token) view returns (uint256)` - balance available to distribute (excluding reserved earmarks) * `previewSplit(uint256 amount) view returns (uint256[] memory shares)` - show how amount would split **Events** * `event BucketConfigured(indexed uint256 index, address recipient, uint96 weight, bool notify)` - bucket added/updated * `event BucketRecipientSet(indexed uint256 index, address recipient, bool notify)` - recipient changed. Three fields: a decoder built on the older two-field shape will not match * `event MinIntervalSet(uint256 minInterval)` * `event Distributed(indexed address token, uint256 amount)` - total paid (excluding skipped buckets) * `event BucketPaid(indexed address token, indexed uint256 index, address recipient, uint256 amount)` - one bucket paid * `event BucketSkipped(indexed address token, indexed uint256 index, bytes reason)` - payout reverted; earmark held * `event BucketHeld(indexed address token, indexed uint256 index, address recipient, uint256 amount, uint256 totalHeld)` * `event HeldReleased(indexed address token, indexed uint256 index, address recipient, uint256 amount)` - an earmark was paid * `event HeldForceReleased(indexed address token, indexed uint256 index, uint256 amount)` - an earmark was abandoned without a payout **Custom Errors** * `error ZeroAddress()` - bucket recipient is zero * `error ZeroWeight()` - bucket weight is zero * `error NoBuckets()` - setBuckets called with empty array * `error TooManyBuckets()` - bucket count > MAX\_BUCKETS * `error BadIndex()` - index >= buckets.length * `error TooSoon()` - distribute called within minInterval * `error OnlySelf()` - payoutBucket called externally * `error NothingHeld()` - releaseHeld called with zero earmark * `error HeldWouldStrand(uint256 index)` - setBuckets would drop a bucket with live earmarks * `error HeldWouldRedirect(uint256 index, address held, address replacement)` - a bucket change would send a live earmark to a different recipient * `error NotQuoteAsset(address token)` - `distribute` for a token the quote registry does not know * `error IntervalTooLong()` - `setMinInterval` above `MAX_MIN_INTERVAL` **Notes** * Rounding dust goes to the last bucket (buyback and burn by default) * Held earmarks stay out of the distributable balance, so a persistently-failing bucket never re-weights the healthy ones * The staking bucket is seeded to the `MotoStaking` proxy at deploy. Until the reward tokens are registered on the deployed `MotoStaking` implementation, which needs a contract upgrade, the staking share is held at the Collector (`BucketHeld`) and released later *** ### QuoteAssetRegistry (packages/contracts/src/fees/QuoteAssetRegistry.sol) [#quoteassetregistry-packagescontractssrcfeesquoteassetregistrysol] Admin-managed whitelist of quote assets (WETH/USDT/USDC/MOTO). Used by FeeRouter and Factory for fee-side selection and pair-creation gating. Plain Ownable2Step, non-upgradeable. **Constructor** * `constructor(address initialOwner, address[] memory initialQuotes)` - seed initial quote assets **State Variables** * `isQuote[address => bool]` - whether an asset is a quote * `quoteRank[address => uint256]` - fee-side priority (higher = more senior); tiebreaks when both swap legs are quote * `_quoteList: address[]` - enumerable mirror of isQuote (for indexers) **External / Public Functions** *Config (onlyOwner):* * `setQuote(address token, bool status)` - add/remove from quote set; updates \_quoteList; no-ops if status unchanged * `setQuoteBatch(address[] calldata tokens, bool status)` - batch form * `setQuoteRank(address token, uint256 rank)` - set fee-side priority for a quote asset * `setQuoteRankBatch(address[] calldata tokens, uint256[] calldata ranks)` - batch form *Views:* * `quoteAssets() view returns (address[] memory)` - enumerable quote set **Events** * `event QuoteSet(indexed address token, bool status)` * `event QuoteRankSet(indexed address token, uint256 rank)` **Custom Errors** * `error ZeroAddress()` - setQuote received zero token * `error LengthMismatch()` - setQuoteRankBatch arrays different lengths **Notes** * No persistence after removal: setQuote(token, false) removes it from \_quoteList and isQuote * quoteRank defaults to 0 (unranked); FeeRouter defaults to output-side when tied *** ### BuybackBurner (packages/contracts/src/fees/BuybackBurner.sol) [#buybackburner-packagescontractssrcfeesbuybackburnersol] Keeper-driven MOTO buyback and burn. Receives quote assets from Collector, swaps each to MOTO through the FeeRouter along an owner-pinned route, and burns to 0x...dEaD. Every sale must clear the contract's own TWAP floor as well as the keeper's. UUPS proxy, Ownable2Step. **Constructor / Init** * `initialize(address initialOwner, address moto_, address router_, address keeper_, address upgradeAuthority_)` - wire MOTO token, the core router (used to find the factory), keeper (may be zero: owner-only until one is appointed), and the upgrade authority **Constants** * `BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD` * `MIN_TWAP_WINDOW: uint32`, `MAX_TWAP_WINDOW: uint32` - the shortest and longest reference window a sale is judged over. Not published here. Read them from the contract * `DEFAULT_TOLERANCE_BPS`, `MIN_TOLERANCE_BPS`, `MAX_TOLERANCE_BPS` - the default TWAP tolerance and the bounds on `setTwapToleranceBps`. Not published here. Read them from the contract **State Variables** * `moto: IERC20` - MOTO token * `router: IMotoSwapRouter02` - core router, read for its factory * `keeper: address` - an operational role address (settable; zero = owner-only) * `totalBurned: uint256` - cumulative MOTO burned * `feeRouter: IFeeRouter` - set post-init; the swap venue * `twapToleranceBps: uint256` - raw tolerance, 0 meaning unset. Read `twapTolerance()` instead **External / Public Functions** *Config (onlyOwner):* * `setFeeRouter(address feeRouter_)` - wire the swap venue post-init * `setKeeper(address keeper_)` - set or clear keeper (zero = owner-only) * `setPinnedPath(address tokenIn, address[] calldata path)` - pin the one route `tokenIn` may be sold through, or clear it with an empty path. A pinned path is exactly `[tokenIn, MOTO]`; anything else reverts BadPinnedPath * `setTwapToleranceBps(uint256 bps)` - move the tolerance within its bounds; ToleranceOutOfRange outside them *Upgrade path:* * `upgradeAuthority() view returns (address)`, `setUpgradeAuthority(address newAuthority)` - same split as RakebackV2 * `renounceUpgradeability()` - freeze implementation; one-way; gated on the upgrade authority *Views:* * `pinnedPath(address tokenIn) view returns (address[] memory)` * `pairFor(address tokenIn) view returns (address)` - the pair a pinned token is priced against: the first hop of its pinned route * `observation(address tokenIn) view returns (uint256 priceCumulative, uint32 timestamp)` - the cumulative-price reference held for `tokenIn`. Zeros mean never anchored * `twapTolerance() view returns (uint256)` - the tolerance in force, resolving the unset sentinel * `twapFloor(address tokenIn, uint256 amountIn) view returns (uint256)` - the MOTO floor this contract will not fill below for that sale: the window's average price, less the protocol fee, twice the creator fee, the pair fee and the tolerance. Reverts NoUsableTwap when there is no reference inside the window *Operations:* * `pokeCheckpoint(address tokenIn)` - permissionless. Re-anchor the reference for `tokenIn`. Reverts CheckpointFresh inside `MIN_TWAP_WINDOW` of the last anchor, and CheckpointUsable while the existing reference is still inside the window * `buybackAndBurn(uint256 sellAmount, uint256 minMotoOut, address[] calldata path, uint256 deadline) onlyKeeper nonReentrant returns (uint256 motoOut)` - sell `sellAmount` of `path[0]` to MOTO and burn it plus any MOTO already held. `sellAmount` is clamped to the balance and `0` means the whole balance. `path` must equal the pinned route. The effective floor is the higher of `minMotoOut` and `twapFloor`, enforced on the measured MOTO balance delta. With nothing to sell and nothing to burn it emits NothingToDo and returns 0 instead of reverting * `burn() nonReentrant` - burn all MOTO held (permissionless) **Events** * `event KeeperSet(address keeper)` * `event FeeRouterSet(address feeRouter)` * `event BoughtBack(indexed address tokenIn, uint256 amountIn, uint256 motoOut)` * `event Burned(uint256 amount, uint256 totalBurned)` * `event PinnedPathSet(indexed address tokenIn, address[] path)` * `event NothingToDo(indexed address tokenIn)` - a keeper run found nothing to sell and nothing to burn * `event TwapToleranceSet(uint256 bps)` * `event CheckpointPoked(indexed address tokenIn, indexed address pair, uint256 priceCumulative, uint32 timestamp)` * `event TwapFloorBinding(indexed address tokenIn, uint256 keeperFloor, uint256 twapFloor)` - the contract's floor was the binding one, not the keeper's **Custom Errors** * `error ZeroAddress()` - initialize, a setter, or a sale with no feeRouter wired * `error NotKeeper()` - caller not keeper and not owner * `error BadPath()` - path shorter than 2, `path[0] == MOTO`, or `path[last] != MOTO` * `error NothingToBurn()` - `burn` with no MOTO held * `error BelowMinMotoOut(uint256 motoOut, uint256 minMotoOut)` - the measured MOTO received is below the effective floor * `error BelowTwapFloor(uint256 motoOut, uint256 twapFloor)` - declared * `error TokenNotPinned(address tokenIn)` - no pinned route for `path[0]` * `error PathNotPinnedRoute()` - `path` differs from the pinned route * `error BadPinnedPath()` - `setPinnedPath` with anything but `[tokenIn, MOTO]` * `error ToleranceOutOfRange(uint256 bps)` * `error NoUsableTwap(address tokenIn)` - never anchored, window too short, window too old, or the pair's last write too old * `error CheckpointFresh()` - `pokeCheckpoint` inside `MIN_TWAP_WINDOW` of the last anchor * `error CheckpointUsable(address tokenIn, uint32 age)` - `pokeCheckpoint` while the reference is still usable * `error NoPairForToken(address tokenIn)` - no pinned route, or no pair behind it **Notes** * Swaps go through FeeRouter, so the buyback pays the protocol fee like any other trade. The `Swap` event for a buyback names this contract as `user` * Cumulative totalBurned never decreases (for indexer/stats) * The `BuybackBurner` has no entrypoint for an outside integrator beyond `burn()` and `pokeCheckpoint()` *** ### BuybackDistributor (packages/contracts/src/fees/BuybackDistributor.sol) [#buybackdistributor-packagescontractssrcfeesbuybackdistributorsol] Merkle-tree distributor over `(index, account, amount)` leaves with a fixed total allocation; claims stay open forever but surplus is sweepable post-deadline (after SWEEP\_GRACE, unclaimed shares are recoverable). Plain Ownable2Step, non-upgradeable. **Constructor** * `constructor(address initialOwner, address token_, bytes32 merkleRoot_, uint256 claimDeadline_, uint256 totalAllocation_)` - immutable root, deadline, and allocation; rejects zero root, past deadline, or zero allocation **Constants** * `SWEEP_GRACE = 180 days` - grace window after claimDeadline before unclaimed shares become sweepable **State Variables (Immutable)** * `token: IERC20` - payout asset * `merkleRoot: bytes32` - Merkle root over (index, account, amount) leaves * `claimDeadline: uint256` - claim window close time * `totalAllocation: uint256` - sum of all leaf amounts **State Variables** * `totalClaimed: uint256` - cumulative claimed amount * `claimedBitMap[uint256 => uint256]` - bitmap for O(1) claim tracking **External / Public Functions** *Claims (permissionless):* * `claim(uint256 index, address account, uint256 amount, bytes32[] calldata merkleProof)` - claim `amount` for `account` at leaf index; verifies proof; reverts if already claimed; advances `totalClaimed` *Sweeps (onlyOwner):* * `sweep(address to)` - reclaim surplus above outstanding liability after claimDeadline; reverts if window still open (ClaimWindowOpen) or nothing to sweep * `sweepAll(address to)` - reclaim entire balance after claimDeadline + SWEEP\_GRACE (the genuinely-abandoned remainder); reverts if grace window still open (GracePeriodOpen) *Views:* * `isClaimed(uint256 index) view returns (bool)` - check if leaf already claimed * `outstanding() view returns (uint256)` - unclaimed liability (totalAllocation - totalClaimed) * `sweepableSurplus() view returns (uint256)` - balance above outstanding (ready for sweep now) **Events** * `event Claimed(indexed uint256 index, indexed address account, uint256 amount)` * `event Swept(indexed address to, uint256 amount)` - reclaimed surplus * `event SweptAll(indexed address to, uint256 amount)` - reclaimed remainder after grace **Custom Errors** * `error AlreadyClaimed()` - index already claimed * `error InvalidProof()` - Merkle proof doesn't match leaf * `error ClaimWindowOpen()` - sweep before claimDeadline * `error InvalidDeadline()` - `deadline <= block.timestamp` * `error ZeroRoot()` - merkleRoot is bytes32(0) * `error ZeroToken()` - token is zero address * `error ZeroAllocation()` - totalAllocation is zero * `error NothingToSweep()` - surplus or remainder is zero * `error GracePeriodOpen()` - sweepAll before grace window closes * `error Overclaim()` - a claim would push `totalClaimed` past `totalAllocation` **Notes** * Two-stage sweep: surplus (after deadline) only covers dust over outstanding liability; unclaimed shares stay funded until grace expires * The shape matches Uniswap's MerkleDistributor (bitmap-tracked) *** ### CreatorFeeRegistry (packages/contracts/src/fees/CreatorFeeRegistry.sol) [#creatorfeeregistry-packagescontractssrcfeescreatorfeeregistrysol] Allowlist of creator-fee-eligible tokens and their creators. Registrar-settable (the moto.fun curve); creator-settable for their own payout address; admin-settable for takeovers. Plain Ownable2Step, non-upgradeable. **State Variables** * `tokens[address => TokenConfig]` - token -> (creator address, disabled flag) * `isRegistrar[address => bool]` - who may call register() **External / Public Functions** *Registrar Path:* * `register(address token, address creator)` - register token by an authorized registrar (the moto.fun curve); reverts if already registered or zero addresses; onlyRegistrar *Creator Path:* * `setCreator(address token, address creator)` - called BY the token's current creator to move their own payout address. Gated on `creatorOf`, so it also works on a disabled token; NotCreator otherwise; emits CreatorSetBySelf *Admin Path (onlyOwner):* * `adminSetCreator(address token, address creator)` - override creator (community takeover); reverts if token not registered * `setDisabled(address token, bool disabled)` - enable/disable a token's creator fee; reverts if not registered * `setRegistrar(address registrar, bool allowed)` - add/remove registrar *Views:* * `activeCreator(address token) view returns (address)` - creator if registered and not disabled, else zero. Answers "should the router skim?" * `creatorOf(address token) view returns (address)` - the registered creator, ignoring `disabled`. Answers "whose money is it?" and is what the vault gates claims on **Events** * `event TokenRegistered(indexed address token, indexed address creator, indexed address registrar)` * `event CreatorSet(indexed address token, indexed address creator)` - admin override * `event CreatorSetBySelf(indexed address token, indexed address previousCreator, indexed address creator)` - the creator moved their own payout address * `event TokenDisabled(indexed address token, bool disabled)` - toggle disabled flag * `event RegistrarSet(indexed address registrar, bool allowed)` - registrar authorized/revoked **Custom Errors** * `error ZeroAddress()` - register/adminSetCreator/setRegistrar received zero * `error NotRegistrar()` - caller not an authorized registrar * `error AlreadyRegistered()` - token already has a creator * `error NotRegistered()` - adminSetCreator or setDisabled on unregistered token * `error NotCreator()` - `setCreator` by anyone but the token's current creator *** ### CreatorFeeVault (packages/contracts/src/fees/CreatorFeeVault.sol) [#creatorfeevault-packagescontractssrcfeescreatorfeevaultsol] Claim-based escrow for creator fees, per token, in quote assets. No hooks, no rescue. Integrates with CreatorFeeRegistry for creator validation. Plain Ownable2Step + ReentrancyGuard, non-upgradeable. **Constructor** * `constructor(address initialOwner, address registry_, address weth_)` - immutable registry and WETH **State Variables** * `registry: ICreatorFeeRegistry` - immutable * `WETH: address` - immutable * `accrued[address => mapping(address => uint256)]` - token -> quote asset -> balance claimable by token's current creator * `isDepositor[address => bool]` - who may call notifyDeposit (FeeRouter) **External / Public Functions** *Config (onlyOwner):* * `setDepositor(address depositor, bool allowed)` - wire depositor (FeeRouter) *Core:* * `notifyDeposit(address token, address quoteAsset, uint256 amount) onlyDepositor` - record fee skim (depositor must have already transferred tokens); emits Accrued * `claim(address token, address[] calldata quoteAssets, bool unwrapWeth) nonReentrant` - claim `token`'s accrued balances across quote assets; caller must be the token's creator (`registry.creatorOf`, so a disabled token's balance stays claimable); unwraps WETH if requested; emits Claimed * `claimMany(address[] calldata tokens, address[] calldata quoteAssets, bool unwrapWeth) nonReentrant` - the same for several tokens, one quote-asset list for all. A token the caller is not the creator of reverts the WHOLE batch; an empty `tokens` reverts NothingToClaim; duplicates are harmless **Events** * `event Accrued(indexed address token, indexed address quoteAsset, uint256 amount)` * `event Claimed(indexed address token, indexed address creator, indexed address quoteAsset, uint256 amount)` * `event DepositorSet(indexed address depositor, bool allowed)` **Custom Errors** * `error ZeroAddress()` - constructor received zero address * `error NotDepositor()` - notifyDeposit called by non-depositor * `error NotCreator()` - claim called by non-creator * `error NothingToClaim()` - `claimMany` with an empty token list * `error EthTransferFailed()` - ETH send failed or non-WETH receive **Notes** * Solvency: vault balance of each quote asset == sum of accrued\[\*]\[quoteAsset] * A creator takeover captures unclaimed balance (by design; accrued is per token, not per old creator) * WETH unwrapping on claim is optional (creator may take as WETH or unwrap to ETH) *** ### MasterChef (packages/contracts/src/chef/MasterChef.sol) [#masterchef-packagescontractssrcchefmasterchefsol] Fixed-supply staking farm with time-based halving-curve emissions (the halving period is settable state, 42 days at initialize). Rewards: 50% liquid + 50% streamed through RewardVestingEscrow (180 days). Per-pool allocation, deposit fees, 24h unstake cooldown while the pool earns (none when no campaign is emitting or the pool's weight is 0); withdraw is queue then claim. UUPS proxy, Ownable2Step. Deployed with the DEX. While `launchPhase` was `vamp`, `/config.addresses.masterChef` served the launch farm, which is a `VampChef`. Not running: no emission campaign has been started on this contract, and none is planned. **Constructor / Init** * `constructor()` - disables initializers * `initialize(IERC20 _rewardToken, address _vestingEscrow, address _feeRecipient, address _owner, address upgradeAuthority_)` - set reward token, vesting escrow (must match token), fee recipient, upgrade authority (zero leaves upgrades with the owner); defaults: halvingPeriod=42d, numPeriods=8 **Constants** * `ACC_PRECISION = 1e12` * `COOLDOWN = 24 hours` * `MAX_DEPOSIT_FEE_BPS = 1000` (10%) * `MAX_HALVING_PERIOD = 365 days` * `MAX_START_DELAY = 90 days` * `HARVEST_GRACE = 30 days` (grace period after emissionEnd before recovery opens) * `MAX_POOLS = 500` **State Variables** * `rewardToken: IERC20` - (settable pre-launch only) * `vestingEscrow: IRewardVestingEscrow` - (settable pre-launch via setVestingEscrow) * `halvingPeriod, numPeriods: uint256` - next campaign schedule (prospective-only; snapshot at startEmission) * `campaignHalvingPeriod, campaignNumPeriods: uint256` - running campaign schedule snapshot * `initialPeriodReward, totalEmission: uint256` - running campaign reward config * `emissionStart, emissionEnd: uint64` - running campaign window * `feeRecipient: address` - deposit fee recipient (settable) * `trustedZapper: address` - zap-and-stake exempt from depositFor third-party guard (set once) * `totalStakedRewardToken, pendingWithdrawalRewardToken: uint256` - reward-token principal tracking (never paid as reward) * `totalAllocPoint, totalCredited, totalHarvested, forfeitedReward: uint256` - allocation sum, reward credited into pools, reward paid, reward forfeited (by emergencyWithdraw or a clipped payout) * `poolExistence[address => bool]` - which LP tokens already have a pool * `PoolInfo[] poolInfo` - `` `{ lpToken, allocPoint, lastRewardTime, accRewardPerShare, lpSupply, depositFeeBps }` `` * `userInfo[pid => mapping(user => UserInfo)]` - `` `{ amount, rewardDebt, pendingWithdrawal, cooldownEnd }` `` * `withdrawUnlockTime[pid => mapping(user => uint64)]` - campaign-based unlock timestamp (sticky) **External / Public Functions** *Campaign Config (onlyOwner):* * `startEmission(uint256 _initialPeriodReward, uint64 _startTime)` - launch campaign with halving curve; pulls exact geometric total upfront; rejects ghost curves (`<1 wei/s` in final period), retroactive starts, or starts >90d future; runs massUpdatePools first; emits EmissionStarted * `setEmissionSchedule(uint256 _halvingPeriod, uint256 _numPeriods)` - prospective-only config for NEXT campaign; enforces `_halvingPeriod >= 1 day` and `<= MAX_HALVING_PERIOD`, \_numPeriods in \[1,64] * `setRewardToken(IERC20 _rewardToken)` - change reward token pre-launch (no campaign started, no pools); validates escrow binding * `setVestingEscrow(address _vestingEscrow)` - swap escrow and reward token atomically pre-launch * `setFeeRecipient(address _feeRecipient)` - set deposit fee destination * `setTrustedZapper(address _trustedZapper)` - exempt one zap-and-stake router from third-party guard on top-ups. ONE-SHOT: a second call reverts TrustedZapperAlreadySet, zero reverts ZeroAddress *Upgrade path:* * `upgradeAuthority() view returns (address)`, `setUpgradeAuthority(address newAuthority)` - same split as RakebackV2 * `renounceUpgradeability()` - freeze implementation; one-way; gated on the upgrade authority *Pool Management (onlyOwner):* * `add(uint256 _allocPoint, IERC20 _lpToken, uint16 _depositFeeBps)` - add new pool (no duplicate LPs); runs massUpdatePools; reverts if depositFeeBps > MAX\_DEPOSIT\_FEE\_BPS or already MAX\_POOLS; emits PoolAdded * `set(uint256 _pid, uint256 _allocPoint, uint16 _depositFeeBps)` - update pool config; runs massUpdatePools first; emits PoolSet * `setMany(uint256[] calldata _pids, uint256[] calldata _allocPoints)` - retune several pools' allocation with one settle; LengthMismatch on empty or unequal arrays; emits PoolSet per pool * `massUpdatePools()` - settle all pools (gas-intensive; O(poolCount)) * `updatePool(uint256 _pid)` - settle one pool *Staking:* * `deposit(uint256 _pid, uint256 _amount) nonReentrant` - stake LP for caller * `depositFor(uint256 _pid, uint256 _amount, address _user) nonReentrant` - stake LP for `_user`. A third party may only open a NEW position: if `_user` already has a stake in the pool it reverts Unauthorized unless the caller is `_user` or `trustedZapper`. A third-party call with `_amount == 0` returns silently * `harvest(uint256 _pid) nonReentrant` - settle and pay the caller's pending reward without changing stake; behaviourally identical to `deposit(_pid, 0)`, split out so a wallet shows "Harvest" rather than "Deposit" in the confirmation * `withdraw(uint256 _pid, uint256 _amount) nonReentrant` - queue a withdrawal: settles rewards and sets `cooldownEnd = min(now + COOLDOWN, emissionEnd)`, or `now` when `cooldownWaivedFor(_pid)`. One queued withdrawal per (user, pool): while one is queued, `withdraw`, `deposit`, `depositFor` and `harvest` revert CooldownActive * `claimWithdrawal(uint256 _pid) nonReentrant` - pay the queued withdrawal at or after `cooldownEnd` * `emergencyWithdraw(uint256 _pid) nonReentrant` - return the whole ACTIVE stake instantly and forfeit pending rewards (RewardForfeited). An amount already queued by `withdraw` is untouched and still exits through `claimWithdrawal` *Views:* * `stakedBalance(uint256 _pid, address _user) view returns (uint256)` - actively-staked LP amount * `pendingReward(uint256 _pid, address _user) view returns (uint256)` - settled but unclaimed reward * `availableReward() view returns (uint256)` - reward tokens left to distribute (excludes staked + cooldown principal) * `recoverableReward() view returns (uint256)` - uncredited emission (owed to no one) * `poolLength() view returns (uint256)` * `emittedByTime(uint256 _t) view returns (uint256)` - closed-form cumulative emission up to time \_t * `emissionBetween(uint256 _from, uint256 _to) view returns (uint256)` - emission in time window * `currentRewardPerSecond() view returns (uint256)` - live per-second rate (0 outside window) * `cooldownWaived() view returns (bool)` - true when no campaign emitting (no start, scheduled-not-begun, or ended) * `cooldownWaivedFor(uint256 _pid) view returns (bool)` - `cooldownWaived()`, or the pool (or every pool) has zero allocation. What `withdraw` actually uses * `HALVING_PERIOD(), NUM_PERIODS() view` - next campaign schedule (back-compat) *Harvest & Recovery:* * `recoverUndistributedRewards(address _to, uint256 _amount) onlyOwner` - recover emission that credited no pool (after emissionEnd + HARVEST\_GRACE); bounded by recoverableReward(); emits UndistributedRewardsRecovered **Events** * `event PoolAdded(indexed uint256 pid, indexed address lpToken, uint256 allocPoint, uint16 depositFeeBps, uint256 totalAllocPoint)` * `event PoolSet(indexed uint256 pid, uint256 allocPoint, uint16 depositFeeBps, uint256 totalAllocPoint)` * `event PoolUpdated(indexed uint256 pid, uint256 accRewardPerShare, uint256 lastRewardTime)` * `event Deposit(indexed address user, indexed uint256 pid, uint256 amount)` * `event WithdrawQueued(indexed address user, indexed uint256 pid, uint256 amount, uint64 cooldownEnd)` * `event WithdrawClaimed(indexed address user, indexed uint256 pid, uint256 amount)` * `event EmergencyWithdraw(indexed address user, indexed uint256 pid, uint256 amount)` * `event RewardPaid(indexed address user, indexed uint256 pid, uint256 amount)` - the FULL reward, both the liquid half and the half sent to the escrow * `event RewardShortfall(indexed address user, indexed uint256 pid, uint256 owed, uint256 paid)` - the pot could not cover a payout; `paid` went out, the rest was forfeited * `event RewardForfeited(indexed address account, indexed uint256 pid, uint256 amount)` - pending reward abandoned by emergencyWithdraw * `event EmissionStarted(uint256 totalReward, uint256 initialPeriodReward, uint64 startTime, uint64 endTime)` * `event FeeRecipientUpdated(address feeRecipient)` * `event EmissionScheduleSet(uint256 halvingPeriod, uint256 numPeriods)` * `event RewardTokenSet(address rewardToken)` * `event VestingEscrowSet(address vestingEscrow)` * `event TrustedZapperSet(address trustedZapper)` * `event UndistributedRewardsRecovered(address to, uint256 amount)` **Custom Errors** * `error RewardsExhausted()` - `recoverUndistributedRewards` above `recoverableReward()`. A user payout never reverts with it: a short pot pays what it has and emits RewardShortfall * `error CooldownActive()` - deposit, depositFor, harvest or withdraw while a withdrawal is queued on that pool * `error CooldownNotElapsed()` - claimWithdrawal before `cooldownEnd` * `error LengthMismatch()` - setMany with empty or unequal arrays * `error TrustedZapperAlreadySet()` - second setTrustedZapper * `error NoPendingWithdrawal()` - claimWithdrawal with no queued withdrawal * `error DuplicatePool()` - add tries to add LP token already in a pool * `error InvalidPool()` - \_pid >= poolInfo.length * `error DepositFeeTooHigh()` - depositFeeBps > MAX\_DEPOSIT\_FEE\_BPS * `error InsufficientStaked()` - withdraw amount > staked balance * `error ZeroAddress()` - init or config received zero * `error Unauthorized()` - depositFor third-party top-up without trustedZapper exemption * `error EmissionActive()` - startEmission or setEmissionSchedule while a campaign is running; recovery inside the window plus grace * `error InvalidEmission()` - startEmission with ghost curve, past time, or too-far-future * `error TooManyPools()` - pool count would exceed MAX\_POOLS * `error PoolsExist()` - setRewardToken while pools exist * `error CampaignRan()` - setRewardToken after campaign started * `error RewardTokenMismatch()` - escrow token != reward token * `error HalvingPeriodTooLong()` - \_halvingPeriod > MAX\_HALVING\_PERIOD **Notes** * Emissions are TIME-BASED (not block-based); immune to block-time drift * Halving curve: period k emits (initialPeriodReward >> k) spread linearly over halvingPeriod * add/set ALWAYS call massUpdatePools first (no \_withUpdate flag); settled balances are the only safe state for allocation changes * Reward-token principal is tracked separately (totalStakedRewardToken) and never paid as reward * The unstake cooldown is `min(now + 24h, emissionEnd)` snapshotted at queue time: whichever comes FIRST. Waived when no campaign is emitting or the pool has zero allocation * Reward payout: `vested = amount / 2` goes to `RewardVestingEscrow.depositFor(user, vested)`, `amount - vested` is transferred liquid, so an odd wei favors the liquid half *** ### RewardVestingEscrow (packages/contracts/src/chef/RewardVestingEscrow\.sol) [#rewardvestingescrow-packagescontractssrcchefrewardvestingescrowsol] Per-user 180-day linear vesting for farm rewards. Folding grant (O(1) storage): claim pays released + banked, depositFor banks released-unclaimed and re-spreads unreleased+new over a fresh 180 days. UUPS proxy. Deployed on Ethereum at `/config.addresses.rewardVestingEscrow`. **Constructor / Init** * `initialize(IERC20 _token, address owner_, address upgradeAuthority_)` - token is set once and has no setter. The deployed implementation predates the third argument, the upgrade-authority functions and two-step ownership: those are not live yet and arrive with a contract upgrade **Constants** * `VEST_DURATION = 180 days` **State Variables** * `token: IERC20` - the vested token (MOTO); no setter * `grants[user]: Grant` - ONE schedule per user: `` `{ total, claimed, withdrawable, start }` `` * `isDepositor[address => bool]` - who may call depositFor (MasterChef, VampChef) **External / Public Functions** *Config (onlyOwner):* * `setDepositor(address depositor, bool allowed)` - authorize or revoke a depositor; emits DepositorSet *Upgrade path:* * `upgradeAuthority() view returns (address)`, `setUpgradeAuthority(address newAuthority)` - same split as RakebackV2. Not live yet, arrives with a contract upgrade * `renounceUpgradeability()` - freeze implementation; one-way; gated on the upgrade authority *Core (depositor-gated deposit, user-only claim):* * `depositFor(address user, uint256 amount) nonReentrant` - depositor-gated (`isDepositor[msg.sender]`, else NotDepositor): pull amount from caller, bank released-unclaimed from old schedule, re-spread unreleased+amount over fresh 180d; emits Deposited * `claim() nonReentrant returns (uint256 amount)` - pay caller's released + banked, without resetting clock; emits Claimed *Views:* * `claimable(address account) view returns (uint256)` - what claim() would pay now (released + banked) * `vesting(address account) view returns (uint256)` - unreleased tail of current schedule * `vestEndOf(address account) view returns (uint64)` - when the account's CURRENT schedule finishes (`start + 180 days`); zero when there is no grant. Any later depositFor moves it, which is why the UI shows it before a user confirms a harvest **Events** * `event Deposited(indexed address user, uint256 amount, uint256 scheduleTotal, uint64 start)` * `event Claimed(indexed address user, uint256 amount)` * `event DepositorSet(indexed address depositor, bool allowed)` **Custom Errors** * `error ZeroAddress()` - init received zero * `error ZeroAmount()` - depositFor called with zero * `error NotDepositor()` - depositFor called by an address that is not on the allowlist **Notes** * No escape hatch: only the user can claim, so only the user can exit * Repeated dust deposits geometrically delay tail release (accepted: cost to griefer is gas+dust, harm is negligible timing shift) * MasterChef \_safeRewardTransfer does the 50/50 split and depositFor call atomically *** ### VampChef (packages/contracts/src/chef/VampChef.sol) [#vampchef-packagescontractssrcchefvampchefsol] Deployed on Ethereum at `/config.addresses.vampChef`. The launch farming contract, contract A of the two-contract farm design. Distributes a fixed, pre-minted MOTO pool to stakers of Uniswap V2 LP tokens, and of MOTO on its own in pool 1, pro-rata by allocation points and time, over a single fixed 14-day window, then goes cold; that window has closed. Its counterpart contract is `MasterChef` (contract B), built for a halving farm on Motoswap LP; it is deployed, no emission campaign is running there, and none is planned. UUPS proxy. Deliberately a trimmed MasterChef. Both share the same `accRewardPerShare` x time x allocPoint accrual core; the launch farm drops the rest rather than mode-switching it. Emission is FLAT (`rewardPerSecond = budget / DURATION`), not a 42-day halving, so the launch farming APR is easy to read. There is NO unstake cooldown queue: `withdraw` settles rewards and returns principal in the same call, because a 24h cooldown only protects a long distribution. There are NO deposit fees on any of its pools, the single-sided MOTO pool included. What is kept verbatim is what is a safety property rather than phase policy: reward-token-principal tracking, the `RewardsExhausted` backstop, `emergencyWithdraw`, the `MAX_POOLS` bound, and the one-way `renounceUpgradeability`. **Constructor / Init** * `constructor()` - disables initializers * `initialize(IERC20 _rewardToken, address _vestingEscrow, address _owner)` - wire the pre-minted reward token and the escrow that streams the vested half. Reverts RewardTokenMismatch unless `_rewardToken == vestingEscrow.token()`, since a mismatch would send half of every harvest into an escrow denominated in a different token **Constants** * `ACC_PRECISION = 1e12` * `DURATION = 14 days` - the single fixed emission window, flat over its whole length * `MAX_START_DELAY = 90 days` - bound on a scheduled start; there is no cancel path, so a wildly-future start would lock the pulled budget * `HARVEST_GRACE = 30 days` - window after emissionEnd during which recovery stays shut, so every staker has a published stretch of time to harvest first * `MAX_POOLS = 500` **State Variables** * `rewardToken: IERC20` - the pre-minted reward token (MOTO) * `vestingEscrow: IRewardVestingEscrow` - streams the vested 50% of every harvest over 180 days * `rewardBudget: uint256` - the pulled budget (300M at launch) * `rewardPerSecond: uint256` - `rewardBudget / DURATION`. The `rewardBudget % DURATION` wei remainder is un-emitted dust * `emissionStart, emissionEnd: uint64` - the window. `emissionStart == 0` means no campaign has ever started * `totalStakedRewardToken: uint256` - reward-token principal staked across all pools; never paid as reward * `totalCredited, totalHarvested, forfeitedReward: uint256` - reward credited into pools, actually paid out, and abandoned by emergencyWithdraw. `totalCredited - totalHarvested - forfeitedReward` is the stakers' outstanding claim * `poolInfo[]: PoolInfo` - `` `{ lpToken, allocPoint, lastRewardTime, accRewardPerShare, lpSupply }` `` * `userInfo[pid => mapping(user => UserInfo)]` - `` `{ amount, rewardDebt }` `` * `totalAllocPoint: uint256` * `poolExistence[address => bool]` - which LP tokens already have a pool * `trustedZapper: address` - the one contract allowed to deposit on a user's behalf (`UniswapZap`). Set once **External / Public Functions** *Campaign and pool config (onlyOwner):* * `startEmission(uint256 _budget, uint64 _startTime)` - start the single campaign. Pulls `_budget` up front via `transferFrom` (approve first), runs massUpdatePools, sets `emissionEnd = start + 14 days`. One-shot: reverts EmissionActive once `emissionStart` is set. Rejects a ghost curve (`_budget / DURATION == 0`, which also covers zero), a retroactive start, and a start more than MAX\_START\_DELAY out. `_startTime == 0` means now * `setRewardToken(IERC20 _rewardToken)` - swap the reward token before the farm ever runs; locked out once a campaign has started (CampaignRan) or any pool exists (PoolsExist). Must keep matching `vestingEscrow.token()`, and there is no setter to re-point the escrow * `add(uint256 _allocPoint, IERC20 _lpToken)` - add a pool. No duplicate LP tokens, no deposit-fee argument. Runs massUpdatePools first, since changing totalAllocPoint without settling re-weights every existing pool's elapsed interval * `set(uint256 _pid, uint256 _allocPoint)` - retune allocation; also settles first * `setMany(uint256[] calldata _pids, uint256[] calldata _allocPoints)` - retune several pools with one settle; LengthMismatch on empty or unequal arrays * `setTrustedZapper(address _trustedZapper)` - name the trusted zapper. ONE-SHOT: no re-point, no clear; a second call reverts TrustedZapperAlreadySet * `renounceUpgradeability()` - freeze the implementation; one-way; onlyOwner *Staking:* * `deposit(uint256 _pid, uint256 _amount) nonReentrant` - stake LP, harvesting pending first. Credits the realized balance delta, so a fee-on-transfer LP token is accounted at what actually arrived * `depositFor(uint256 _pid, uint256 _amount, address _user) nonReentrant` - deposit and credit `_user`. A third party may only open a NEW position: if `_user` already has a stake in the pool it reverts Unauthorized unless the caller is `_user` or `trustedZapper`. A third-party call with `_amount == 0` returns silently * `harvest(uint256 _pid) nonReentrant` - settle and pay pending without changing stake. Behaviourally identical to `deposit(_pid, 0)`, split out so a wallet shows "Harvest" instead of "Deposit" in the confirmation. Self-only * `withdraw(uint256 _pid, uint256 _amount) nonReentrant` - harvest and return principal in the same call. No cooldown. Reverts InsufficientStaked on zero or more than staked * `emergencyWithdraw(uint256 _pid) nonReentrant` - take principal immediately, forfeiting all pending reward. The forfeited amount is recorded so it stops counting as an outstanding claim forever *Settlement:* * `massUpdatePools()` - settle every pool; O(pools) * `updatePool(uint256 _pid)` - settle one pool *Views:* * `poolLength() view returns (uint256)` * `stakedBalance(uint256 _pid, address _user) view returns (uint256)` * `pendingReward(uint256 _pid, address _user) view returns (uint256)` * `emittedByTime(uint256 _t) view returns (uint256)` - cumulative emission by time `_t` across all pools, before the per-pool split. Linear, clamped to the window: 0 before the start, `rewardPerSecond * DURATION` after the end * `emissionBetween(uint256 _from, uint256 _to) view returns (uint256)` - emission in a window; 0 outside it, so accrual auto-stops * `currentRewardPerSecond() view returns (uint256)` - live rate, 0 outside the window * `availableReward() view returns (uint256)` - reward-token balance less staked reward-token principal * `recoverableReward() view returns (uint256)` - `availableReward()` less the stakers' outstanding claim. As a plain view it settles only to each pool's last updatePool, so it can read slightly high between settlements *Recovery:* * `recoverUndistributedRewards(address _to, uint256 _amount) onlyOwner` - recover emission that streamed while a pool sat at zero stake (or `totalAllocPoint == 0`) and so was credited to nobody. Runs massUpdatePools first, then bounds the amount by `recoverableReward()`, so it can never reach staked principal or an accrued reward. Gated to `block.timestamp >= emissionEnd + HARVEST_GRACE`, unless no campaign ever ran - VampChef is one-shot, so without that second clause any token mis-sent to a never-started deployment would be permanently unrecoverable **Events** * `event PoolAdded(indexed uint256 pid, indexed address lpToken, uint256 allocPoint, uint256 totalAllocPoint)` * `event PoolSet(indexed uint256 pid, uint256 allocPoint, uint256 totalAllocPoint)` * `event PoolUpdated(indexed uint256 pid, uint256 accRewardPerShare, uint256 lastRewardTime)` * `event Deposit(indexed address user, indexed uint256 pid, uint256 amount)` * `event Withdraw(indexed address user, indexed uint256 pid, uint256 amount)` * `event EmergencyWithdraw(indexed address user, indexed uint256 pid, uint256 amount)` * `event RewardPaid(indexed address user, indexed uint256 pid, uint256 amount)` - the FULL reward, both halves * `event RewardShortfall(indexed address user, indexed uint256 pid, uint256 owed, uint256 paid)` - the pot could not cover a payout; `paid` went out, the rest was forfeited * `event RewardForfeited(indexed address user, indexed uint256 pid, uint256 amount)` * `event TrustedZapperSet(address trustedZapper)` * `event EmissionStarted(uint256 budget, uint256 rewardPerSecond, uint64 startTime, uint64 endTime)` * `event RewardTokenSet(address rewardToken)` * `event UndistributedRewardsRecovered(address to, uint256 amount)` **Custom Errors** * `error RewardsExhausted()` - recovery exceeds recoverableReward. A user payout never reverts with it: a short pot pays what it has and emits RewardShortfall * `error LengthMismatch()` - setMany with empty or unequal arrays * `error TrustedZapperAlreadySet()` - second setTrustedZapper * `error Unauthorized()` - depositFor top-up of someone else's existing position * `error DuplicatePool()` - add for an LP token that already has a pool * `error InvalidPool()` - `_pid >= poolInfo.length` * `error InsufficientStaked()` - withdraw of zero, or more than staked * `error ZeroAddress()` - init, add, depositFor, setTrustedZapper or recovery received zero * `error EmissionActive()` - startEmission called twice, or recovery inside the window plus grace * `error InvalidEmission()` - ghost curve, retroactive start, or a start beyond MAX\_START\_DELAY * `error TooManyPools()` - pool count would exceed MAX\_POOLS * `error PoolsExist()` - setRewardToken while pools exist * `error CampaignRan()` - setRewardToken after the campaign started * `error RewardTokenMismatch()` - reward token differs from `vestingEscrow.token()` **Notes** * Every harvest pays 50% liquid and folds 50% into the same 180-day RewardVestingEscrow MasterChef uses; odd wei favours the liquid half * Pre-minted: the whole budget is pulled at startEmission, so a RewardShortfall is a backstop, never a normal path * One campaign, ever. The launch farming window does not restart * If a single-sided MOTO pool is added, its staked principal is reserved out of availableReward and can never be paid as reward * `add` at 499 existing pools measures around 483k gas, roughly 1.6% of a 30M block, because massUpdatePools skips pools with nothing staked *** ### MotoStaking (packages/contracts/src/stake/MotoStaking.sol) [#motostaking-packagescontractssrcstakemotostakingsol] MOTO staking with lock options (3/6/12/24 months) at boosted weights (1x/2x/4x/8x). Multi-token rewards (quote assets) streamed per reward token. Principal vests linearly; requestUnstake queues into 30-day linear release. UUPS proxy. Deployed on Ethereum at `/config.addresses.motoStaking`. The deployed implementation predates parts of this source: `addRewardToken`, the upgrade-authority functions and the two-argument `initializeV2` are not live yet and arrive with a contract upgrade. Its `feeRouter()` is a `0x…dEaD` placeholder, so `claimAsMoto` is not live yet either. **Constructor / Init** * `constructor()` - disables initializers * `initialize(address initialOwner, address moto_, address router_, address feeRouter_, address treasury_)` - set MOTO, routers, treasury for unclaimed rewards when no stake; defaults: durations=\[90d,180d,365d,730d], boosts=\[0,10000,30000,70000] **Constants** * `UNSTAKE_DRIP = 30 days` - linear unstake release window * `MAX_REWARD_TOKENS = 32` * `MAX_BOOST_BPS = 100_000` - ceiling on `setBoost` (11x; the shipped ladder tops out at 70\_000) * `MAX_OPEN_TICKETS = 64` - cap on a wallet's concurrent unstake release tickets * `MAX_PURGE_RESIDUAL = 1e6` - hard ceiling on `purgeRewardToken`'s `maxResidual` * `NUM_OPTIONS = 4` - lock tiers * `ACC = 1e30` - reward accumulator precision (private) * `BPS = 10_000` (private) **State Variables** * `moto: IERC20` - staked principal and reward token * `legacyRouter: IMotoSwapRouter02` - the router address passed to `initialize`, recorded for continuity and never read. `claimAsMoto` swaps through `feeRouter` * `feeRouter: IFeeRouter` - the swap venue `claimAsMoto` routes through, installed by `initializeV2` * `treasury: address` - reward destination when no effective stake * `durations[4]: uint256` - lock lengths per tier * `boostBps[4]: uint256` - extra weight per tier (prospective-only; snapshots at open). Weight is `amount * (BPS + boostBps) / BPS`, so the multiplier is `1 + boostBps / 10000` * `rewardNotifier: address` - allowed to call notifyReward (Collector) * `vestAnchor: uint64` - optional vest-clock anchor for imported stakers (non-retroactive) * `positions[user]: Position` - `` `{ amount, original, durationOption, start, boostBps, duration, availableFloor }` ``; `duration` and `boostBps` are snapshotted at open so a later config change never reprices a live position, and `availableFloor` is the withdrawable figure crystallised at append time so `available()` can never fall * `tickets[user][]: Ticket[]` - unstaking release streams `` `{ amount, start, claimed }` `` * `totalEffectiveStake: uint256` - sum of all positions' effective stake (with boosts) * `rewardTokens[]: address[]` - enumerable reward tokens * `isRewardToken[token => bool]` - whether token is active * `accRewardPerEffShare[token => uint256]` - per-effective-share accumulator (scaled by ACC) * `userRewardDebt[user => mapping(token => uint256)]` - snapshot of accRewardPerEffShare at user's last settle * `claimableStored[user => mapping(token => uint256)]` - settled reward ready to claim * `wasRewardToken[token => bool]` - delisted but not yet purged (stays in settle loop) * `everRewardToken[token => bool]` - ever been a reward token; permanent flag (prevents re-listing) * `pendingClaims[token => uint256]` - the sum of settled, unclaimed `claimableStored` for the token * `rewardOwed[token => uint256]` - reward notified in minus reward paid out; what a purge must leave alone **External / Public Functions** *Config (onlyOwner):* * `initializeV2(address feeRouter_, address upgradeAuthority_) onlyOwner reinitializer(2)` - not live yet (the deployed implementation has a one-argument form). The v1 to v2 upgrade step, meant to run inside `upgradeToAndCall`: installs the FeeRouter that `claimAsMoto` swaps through and the upgrade authority; emits FeeRouterSet. `legacyRouter` is deliberately left untouched * `addRewardToken(address token)` - approve `token` as a reward the notifier may pay in. Refuses an address that was ever delisted, refuses past `MAX_REWARD_TOKENS`, RewardTokenAlreadyApproved on a repeat. Not live yet, arrives with a contract upgrade * `claimReward(address account, address token) nonReentrant returns (uint256 amount)` - push `account`'s settled reward to `account`. Exists so one abandoned wallet cannot block a purge; it can only ever pay the staker * `setRewardNotifier(address notifier)` - set fee Collector (re-pointable) * `setVestAnchor(uint64 anchor)` - set vest-clock anchor (0 disables; bounds checking: 90d future max, \< durations\[0] in past) * `setBoost(uint8 durationOption, uint256 bps)` - retune boost (prospective-only; capped at MAX\_BOOST\_BPS) * `removeRewardToken(address token)` - delist from accepts (stays in settle loop until purgeRewardToken) * `purgeRewardToken(address token, uint256 maxResidual)` - drop a fully-drained delisted token from the settle loop, writing off up to `maxResidual` of unclaimable residue to `treasury`. The index truncates in both `notifyReward` and `_settle`, so a few wei always remain and an exact-zero gate could never free the slot. MOTO is rejected outright (CannotPurgeMoto) because its balance is staked principal *Upgrade path:* * `upgradeAuthority() view returns (address)`, `setUpgradeAuthority(address newAuthority)` - same split as RakebackV2. Not live yet, arrives with a contract upgrade * `renounceUpgradeability()` - freeze implementation; one-way; gated on the upgrade authority *Staking:* * `stake(uint256 amount, uint8 durationOption) nonReentrant` - open new position (one active per user); reverts if position exists * `appendStake(uint256 amount) nonReentrant` - add to existing position (no duration arg - the tier is fixed at open; lock re-weighted: average remaining + full new lock) * `requestUnstake(uint256 amount) nonReentrant` - queue withdrawal (settles rewards, stops earning) * `claimUnstaked() nonReentrant returns (uint256 amount)` - collect whatever has dripped from all release tickets * `claimUnstakedFrom(uint256 maxTickets) nonReentrant returns (uint256 amount)` - bounded variant for wallets with many tickets * `claim() nonReentrant` - claim all reward tokens * `claim(address token) nonReentrant returns (uint256 amount)` - claim one token's rewards * `claimTokenSelf(address account, address token)` - settle a single token for an account * `claimAsMoto(address token, uint256 minMotoOut, address[] calldata path, uint256 deadline) nonReentrant returns (uint256 motoOut)` - not live yet, arrives with a contract upgrade (the deployed `feeRouter()` is a placeholder). Claim and swap to MOTO through the FeeRouter (path must run token→MOTO); `minMotoOut` is checked against the MOTO that actually reached the caller (BelowMinMotoOut) * `bulkImport(address[] calldata accounts, uint256[] calldata amounts, uint8[] calldata durationOptions) onlyOwner nonReentrant` - import staker positions; pulls `sum(amounts)` MOTO from the owner; each target must have no active position. Three arrays: there is no `starts` argument *Reward Notifications (onlyNotifier):* * `notifyReward(address token, uint256 amount) nonReentrant` - add token rewards (spread per effective stake); `token` must be on the owner-approved list, else RewardTokenNotApproved; with no effective stake the amount is forwarded to `treasury`; emits RewardNotified. The deployed implementation lists an unknown token on first notify instead, until the contract upgrade *Views:* * `pendingReward(address account, address token) view returns (uint256)` - user's accrued reward (settled + accruing) * `vested(address account) view returns (uint256)` / `available(address account)` - vested principal and what requestUnstake can move now * `effectiveStakeOf(address account) view returns (uint256)` - boosted stake weight * `ticketCount(address account) view returns (uint256)` / `ticketsLength(address account) view returns (uint256)` / `claimableUnstaked(address account) view returns (uint256 amount)` - unstake-ticket surface * `rewardTokensLength() view returns (uint256)` **Events** * `event Staked(indexed address account, uint256 amount, uint8 durationOption)` * `event Imported(indexed address account, uint256 amount, uint8 durationOption)` * `event RewardNotified(indexed address token, uint256 amount, uint256 accRewardPerEffShare)` * `event RewardForwardedToTreasury(indexed address token, uint256 amount)` - notifyReward arrived with no effective stake, so it went to treasury * `event Claimed(indexed address account, indexed address token, uint256 amount)` * `event ClaimSkipped(indexed address account, indexed address token)` - one token's payout was skipped inside a multi-token claim * `event ClaimedAsMoto(indexed address account, indexed address token, uint256 rewardIn, uint256 motoOut)` * `event UnstakeRequested(indexed address account, uint256 amount, uint64 start)` * `event Unstaked(indexed address account, uint256 amount)` * `event Appended(indexed address account, uint256 added, uint256 total, uint8 durationOption, uint64 start)` * `event BoostSet(uint8 durationOption, uint256 bps)` * `event RewardNotifierSet(address rewardNotifier)` * `event FeeRouterSet(address feeRouter)` * `event VestAnchorSet(uint64 anchor)` * `event RewardTokenAdded(indexed address token)` * `event RewardTokenRemoved(indexed address token)` * `event RewardTokenPurged(indexed address token)` **Custom Errors** * `error ZeroAmount()` * `error MaxRewardTokensReached()` * `error InvalidDuration()` * `error PositionExists()` - stake when position already open * `error NoPosition()` - appendStake without active position * `error ExceedsAvailable()` - withdraw > unlocked principal * `error TooManyOpenTickets()` - requestUnstake would exceed MAX\_OPEN\_TICKETS * `error LengthMismatch()` * `error ZeroAddress()` * `error NotNotifier()` * `error BadPath()` - claimAsMoto path invalid * `error BelowMinMotoOut(uint256 received, uint256 floor)` - claimAsMoto delivered less MOTO than the caller's floor * `error OnlySelf()` * `error InvalidAnchor()` * `error NotRewardToken()` - removeRewardToken on non-reward token * `error TokenPermanentlyDelisted()` - try to re-list * `error RewardTokenNotApproved(address token)` - notifyReward for a token the owner has not approved * `error RewardTokenAlreadyApproved(address token)` - addRewardToken for a listed token * `error ResidualCeilingTooHigh(uint256 requested, uint256 cap)` - purgeRewardToken with `maxResidual` above `MAX_PURGE_RESIDUAL` * `error TokenNotDelisted()` - purgeRewardToken on non-delisted * `error TokenBalanceNotZero()` - purgeRewardToken while the free balance exceeds maxResidual * `error BoostTooHigh()` - setBoost above MAX\_BOOST\_BPS * `error CannotPurgeMoto()` - purgeRewardToken on MOTO, whose balance is staked principal **Notes** * One active position per user; appendStake re-weights remaining lock with full new lock (average by principal) * Principal vests linearly from (anchor if set and valid, else start) over lock duration * Unstaking streams principal over 30 days (separate Ticket per request); requestUnstake settles rewards but queued principal stops earning * Reward accounting is index-based (never balanceOf); per-reward-token accumulators prevent principal ever being paid as reward * claimAsMoto goes through FeeRouter (pays protocol fee like any other trade) * MOTO is BOTH principal AND valid reward token; internal accounting keeps them separate * removedRewardToken stays in settle loop until fully drained and purged (prevents re-list accumulator-reuse bug) *** ### PveVault (packages/contracts/src/launcher/PveVault.sol) [#pvevault-packagescontractssrclauncherpvevaultsol] Escrow for PvE-bought tokens (bought with the creator's own ETH at launch, reserved for Motocat stakers). Read its address from `/config.addresses.pveVault`. PvE is claimed on Motoswap's staking page (`/stake/motocats`). The claim surface is `claimable(address token, address wallet)` (note the token-first argument order), `claim(address token)` and `claimMany(address[] tokens)`. Eligibility is snapshotted via `stakedBalanceOfAt(wallet, creditBlock - 1)` on MotocatStaking, so it is the wallet's cat balance when the credit landed that counts. `pending(address token)` is a per-TOKEN amount awaiting credit, not a per-wallet claimable balance. UUPS proxy. **Constructor / Init** * `initialize(address initialOwner, address upgradeAuthority_)` - set owner and the upgrade authority (zero leaves upgrades with the owner) **State Variables** * `launcher: address` - the primary launch contract allowed to record PvE receipts * `extraLaunchers[address => bool]` - additional authorized launchers (moto.fun's LaunchpadCurve), additive to `launcher` * `motocatStaking: address` - the staking contract whose historical stake sizes a split * `totalReceived[token => uint256]` - cumulative PvE tokens received per token (indexer stats) * `credits[token => Credit]` - `` `{ uint128 amount, uint64 totalStaked, uint64 creditBlock }` ``; ONE credit per token, ever, packed into a single slot * `claimed[token => mapping(wallet => bool)]` - whether a wallet has taken its share * `pending[token => uint256]` - escrow that landed while nobody was staked, awaiting `recreditPending`. A per-TOKEN amount, not a per-wallet claimable balance * `stakingWiringFrozen: bool` - true once the first credit is frozen, after which `setMotocatStaking` is shut * `expectingArrival[token => bool]` - tokens a launcher has announced escrow for that are not credited yet. The moto.fun curve escrows at fill time and the receipt only follows at graduation; the marker keeps `forward` and `rescueExcess` off that balance in between * `escrowFloor[token => uint256]` - the balance the vault held when a marked token's escrow was credited; the rescue ceiling reads this rather than the receipt **External / Public Functions** *Config (onlyOwner):* * `setLauncher(address launcher_)` - wire the primary launcher * `setExtraLauncher(address launcher_, bool allowed)` - authorize or revoke an additional launcher * `setMotocatStaking(address staking)` - wire the staking contract a split reads history from; rejects a zero address and an address with no code, and reverts StakingWiringFrozen once any credit exists * `recreditPending(address token)` - turn a held escrow into a claimable credit at today's stake. Owner-only because the caller picks the block, which is the whole basis of the preceding-block anti-JIT rule * `forward(address token, address to, uint256 amount) nonReentrant` - move a token the vault owes nobody. Refuses outright (TokenIsCredited) for any token with a credit or a pending hold * `clearPendingArrival(address token)` - clear an arrival marker for a token this vault holds nothing for (a coin that never graduates); emits PendingArrivalClearedByOwner * `rescueExcess(address token, address to) nonReentrant returns (uint256 amount)` - recover a stray transfer of a token the vault is ALREADY paying out. Moves only the balance above `credit.amount + pending`; NoExcessToRescue when there is none *Upgrade path:* * `upgradeAuthority() view returns (address)`, `setUpgradeAuthority(address newAuthority)` - same split as RakebackV2 * `renounceUpgradeability()` - freeze implementation; one-way; gated on the upgrade authority *Core (launcher-only):* * `notePendingArrival(address token)` - mark a token whose escrow is about to arrive by plain transfer. One-shot per token: AlreadyExpecting if marked, AlreadyCredited if credited or pending * `recordPve(address token, address creator, uint256 amount)` - launcher-only receipt record, which also credits the escrow with no owner action. Checks `amount` against the vault's actual balance rather than trusting it, then sizes the split against `totalStakedAt(block.number - 1)`. Emits PveReceived, then PveCredited or PveHeld *Views and claims:* * `rescuableExcess(address token) view returns (uint256)` - what `rescueExcess` could move right now; zero for a token with no credit * `claimable(address token, address wallet) view returns (uint256)` - the wallet's share, `credit.amount * stakedBalanceOfAt(wallet, creditBlock - 1) / credit.totalStaked`. Zero once claimed or if the wallet was not staked at the credit block. Note the token-first argument order * `claim(address token) nonReentrant returns (uint256 amount)` - claim one token's share; reverts NothingToClaim on zero * `claimMany(address[] tokens) nonReentrant returns (uint256 total)` - claim across several tokens. Bounded by the caller's array, never by global state, which is what keeps the PvE token count unlimited. Entries with nothing to claim are skipped, but the call still reverts if the whole batch yields nothing **Events** * `event LauncherSet(address launcher)` * `event ExtraLauncherSet(indexed address launcher, bool allowed)` * `event MotocatStakingSet(address staking)` * `event PveReceived(indexed address token, indexed address creator, uint256 amount)` * `event PveCredited(indexed address token, uint256 amount, uint256 totalStaked, uint256 creditBlock)` * `event PveHeld(indexed address token, uint256 amount)` * `event PveClaimed(indexed address token, indexed address wallet, uint256 amount)` * `event Forwarded(indexed address token, indexed address to, uint256 amount)` * `event ExcessRescued(indexed address token, indexed address to, uint256 amount)` * `event PendingArrivalNoted(indexed address token)` * `event PendingArrivalCleared(indexed address token)` - cleared by the receipt * `event PendingArrivalClearedByOwner(indexed address token, indexed address by)` **Custom Errors** * `error ZeroAddress()` - a setter or forward received zero * `error NotLauncher()` - recordPve called by an address that is neither `launcher` nor an extra launcher * `error StakingNotSet()` - recreditPending with no staking contract wired * `error AlreadyCredited()` - recordPve for a token that already has a credit or a pending hold * `error AmountNotReceived()` - the vault holds less than the recorded `amount` * `error NoCredit()` - recreditPending on a token with nothing held * `error AlreadyClaimed()` - the wallet already took its share * `error NothingToClaim()` - claim or claimMany yielded zero * `error PoolStillEmpty()` - recreditPending while nobody is staked * `error SeedingNotClosed()` - the staking contract's seed is still in flight, so its supply is not the allocation table's * `error NotAContract(address target)` - setMotocatStaking pointed at an address with no code * `error StakingWiringFrozen()` - setMotocatStaking after a credit exists * `error TokenIsCredited(address token)` - forward on a token owed to stakers * `error ValueTooLarge()` - amount above uint128 or staked count above uint64 * `error TableMovedThisBlock()` - a stake or unstake landed in the same block as the credit, so the snapshot is not final. Retry in the next block * `error NoExcessToRescue(address token)` * `error AlreadyExpecting(address token)` / `error NotExpecting(address token)` - arrival-marker gates **Notes** * Payouts are pull-based and lazy, and PvE credits never expire. There is no sweep and no treasury reclaim of a credited amount * A credit landing while nobody is staked, while staking is unwired, or mid-seed is HELD rather than reverted: a creator's paid launch must never fail over an operational condition they cannot see * `claimable` truncates toward the pool (the standard MasterChef rule), so a small permanent remainder always survives. That residue is correct, not drift, and it is deliberately stranded * `depositToStaking` and its `DepositedToStaking` event were REMOVED with the staking contract's streamed-rewards track. `MotocatStakingV3` has no `depositReward` to push into *** ### MotocatStakingV3 (packages/contracts/src/nft/MotocatStakingV3.sol) [#motocatstakingv3-packagescontractssrcnftmotocatstakingv3sol] Deployed on Ethereum at `/config.addresses.motocatStaking`, with the seed closed. Escrow staking for the Motocat collection: a stake ledger, historical checkpoints, a one-time seeding phase for distribution day, and an L1 to L2 broadcast hook for future networks (no outbox is registered on Ethereum today). It distributes nothing. 1 staked cat = 1 share, no rarity weighting. UUPS proxy behind an owner-gated upgrade path with a one-way `renounceUpgradeability()` switch back to immutable. Earlier revisions of this page documented a reward-streaming contract - an owner-curated allowlist, `depositReward`, per-token `accRewardPerShare`, `rewardDebt`, `pending`, `strandedRewards`, a JIT warmup, `claim`, and an owner pause. None of that exists in the deployed V3. PvE airdrops are the only reward surface and `PveVault` pays them against a frozen per-token snapshot, reading this contract's `stakedBalanceOfAt` and `totalStakedAt`. There is no `depositReward`, no `claim`, no `pause`, and no `rewardWarmupBlocks` here. If you are porting an integration off the old docs, that is the change to plan around. The deletion is structural, not tidy-up. A MasterChef-shaped accumulator has to re-baseline every staker's `rewardDebt` for every token ever issued on every balance change, so `stake` and `unstake` grew with the token count. Measured on V2, a seeded holder's partial exit cost 2,259,022 gas at 32 reward tokens and 8,781,454 at 128, against EIP-7825's per-transaction ceiling of 16,777,216. With an unlimited number of PvE tokens that pattern is not mitigable. The invariant V3 holds instead: no operation's gas depends on how many PvE tokens have ever been airdropped. `stake` and `unstake` scale only with the cats in the call plus two checkpoint slots, and every remaining loop is bounded by an array the caller passes, so hitting a gas ceiling can only ever mean "send a smaller batch". Anti-JIT moved with it. `PveVault` sizes a credit against `totalStakedAt(creditBlock - 1)`, so staking in the credit's own block is worthless, which is why the warmup window has no successor. **Constructor / Init** * `constructor()` - disables initializers on the implementation * `initialize(IERC721 collection_, address initialOwner)` - one-shot. Rejects a zero collection AND one with no code (`collection` has no setter, so a proxy initialised against an EOA is bricked for good), and a zero owner. Must run in the same transaction as the proxy deployment **State Variables** * `collection: IERC721` - the escrowed collection, written once at initialize. Deliberately storage rather than `immutable`, so an upgrade cannot silently re-point the escrow * `totalStaked: uint256` - cats currently staked across all wallets * `stakerOf[tokenId => address]` - who staked each cat; zero when not staked here * `seedingClosed: bool` - one-way latch. False during setup, true forever after `closeSeeding` * `seeder: address` - the address permitted to call `seedStakes` and `closeSeeding` (the MotocatSeeder). Deliberately not the owner, though the owner can call both paths directly * `seedSource: address` - the wallet `seedStakes` pulls cats from when the contract does not already hold them * `outboxes: address[]` - registered L1 to L2 outbox adapters, one per destination chain. May be empty * `isOutbox[address => bool]` - guards against registering the same adapter twice * `_stakeCheckpoints[wallet], _totalStakeCheckpoints` - private append-only `` `Checkpoint { uint32 fromBlock, uint224 value }` `` arrays, the OZ ERC20Votes shape: one entry per block that changed the value, packed into a single slot. Several stakes in one block collapse to one entry * `_staked[wallet => uint256[]]`, `_stakedIndex[tokenId => uint256]` - private enumeration and its swap-and-pop index The reentrancy guard and the upgrade-renounced flag live in ERC-7201 namespaced slots rather than sequential storage, deliberately: the contract is compiled against two different OpenZeppelin versions across two repos, and OZ's own `ReentrancyGuard` moved its storage between them, which behind a proxy would shift every field above by one slot. **External / Public Functions** *Staking (permissionless, never pausable):* * `stake(uint256[] calldata tokenIds) nonReentrant` - pull each cat from the caller into escrow and record it staked to them. Uses `transferFrom`, not `safeTransferFrom`, so there is no hook back into the caller. Blocked until `seedingClosed`. Batch size is uncapped by design: the only bound is the per-transaction gas ceiling, so an oversized call fails and the caller sends a smaller batch. Emits Staked, then broadcasts * `unstake(uint256[] calldata tokenIds) nonReentrant` - return cats to the caller. Instant, no lockup, no fee, no slashing. Only the wallet that staked a cat may unstake it. Never blocked, not even during seeding. A PvE entitlement already credited is unaffected: it was frozen against a past block. Emits Unstaked, then broadcasts *Seeding (owner or seeder, setup phase only):* * `seedStakes(address[] wallets, uint256[] counts, uint256[] tokenIds) nonReentrant onlySeeder` - initialise staked positions from the allocation table. `counts[i]` consumes the next `counts[i]` ids from the flat `tokenIds`. Writes checkpoints per wallet, without which every seeded holder reads zero from `stakedBalanceOfAt` and receives nothing from every PvE credit. Pulls each cat from `seedSource` unless the contract already holds it. Not idempotent: a repeated id reverts AlreadySeeded. Emits both Staked and Seeded per wallet, so the points indexer can tell a seeded position from a wallet's own action. Does NOT broadcast * `closeSeeding() onlySeeder` - end the setup phase permanently: the seed path dies, public `stake` opens. One-way, no reopen * `setSeeder(address seeder_) onlyOwner` - settable only while seeding is open * `setSeedSource(address seedSource_) onlyOwner` - settable only while seeding is open *Upgrades and admin (onlyOwner):* * `renounceUpgradeability()` - permanently freeze the implementation. One-way. Ownership, the seeder wiring and the outbox registry keep working * `upgradesRenounced() view returns (bool)` - whether the implementation can still be replaced * `renounceOwnership() view` - disabled, always reverts RenounceDisabled. Ownership can still be transferred and accepted *Rescue (onlyOwner, never touches an escrowed cat):* * `rescueERC20(address token, address to, uint256 amount) nonReentrant` - unconditional, and only because the rewards track is gone: this contract has no path that accepts, accounts for or pays out an ERC20, so every ERC20 balance here is a stray * `rescueERC721(address nft, address to, uint256 tokenId) nonReentrant` - a cat from the escrowed collection is recoverable only if it has no recorded staker (a stray that arrived via raw `transferFrom`); otherwise reverts CannotRescueStakedCat. Any other collection's NFT is fully recoverable * `rescueETH(address to, uint256 amount) nonReentrant` - the contract has no payable entrypoint, so any ETH here was force-sent and is owed to nobody *Cross-chain broadcast:* * `addOutbox(address outbox) onlyOwner` - register an L1 to L2 adapter * `removeOutbox(address outbox) onlyOwner` - deregister. Swap-and-pop, so index ordering is not stable across a removal * `outboxCount() view returns (uint256)` - zero is a valid, expected state * `rebroadcast(address wallet) nonReentrant` - permissionless. Re-sends the wallet's CURRENT staked count to every adapter, re-reading live state rather than replaying a stored message, so the worst an attacker achieves is paying gas to tell every mirror the truth. This is the recovery path for a BroadcastFailed * `rebroadcastMany(address[] wallets) nonReentrant` - permissionless batch form. Exists for the seed: `seedStakes` broadcasts nothing, so after distribution day essentially the entire holder base is unknown to every mirror, and a sweep over the allocation table is a precondition of the first L2 PvE launch, not a cleanup. No filtering and no deduplication - a zero-stake wallet still broadcasts `0`, which is a legitimate correction An adapter that reverts never blocks a stake: the call is wrapped in try/catch, surfaced as BroadcastFailed, and recovered via `rebroadcast`. The payload is the wallet's COUNT, not token ids, so cost does not scale with how many cats a whale holds. An empty registry costs one SLOAD. *Views:* * `stakedBalanceOf(address wallet) view returns (uint256)` - live count of cats the wallet has staked * `stakedTokensOf(address wallet) view returns (uint256[] memory)` - the tokenIds it currently has staked. Cost grows with the wallet * `stakedTokensOfSlice(address wallet, uint256 offset, uint256 limit) view returns (uint256[] memory ids)` - a bounded page: up to `limit` ids starting at `offset`, clamped (not reverting) past the end. Not live yet, arrives with a contract upgrade * `stakedBalanceOfAt(address wallet, uint256 blockNumber) view returns (uint256)` - how many cats the wallet had staked as of the END of `blockNumber`. Reverts FutureLookup for the current or a future block * `totalStakedAt(uint256 blockNumber) view returns (uint256)` - total staked as of the end of `blockNumber`; the denominator for a PvE split. Same FutureLookup rule * `isStaked(uint256 tokenId) view returns (bool)` * `onERC721Received(...) pure` - always reverts DirectTransferNotAllowed. Staking must go through `stake`, so every escrowed cat maps to a staker who can retrieve it **Events** * `event Staked(indexed address wallet, uint256[] tokenIds)` * `event Unstaked(indexed address wallet, uint256[] tokenIds)` * `event Seeded(indexed address wallet, uint256[] tokenIds)` - one wallet's slice of a seed batch. Emitted alongside Staked so existing consumers keep working while a consumer that cares can tell a seeded position from a wallet's own action * `event SeedingClosed()` * `event SeederSet(indexed address seeder)` * `event SeedSourceSet(indexed address seedSource)` * `event ERC20Rescued(indexed address token, indexed address to, uint256 amount)` * `event ERC721Rescued(indexed address token, indexed address to, uint256 tokenId)` * `event ETHRescued(indexed address to, uint256 amount)` * `event OutboxAdded(indexed address outbox)` * `event OutboxRemoved(indexed address outbox)` * `event Broadcast(indexed address wallet, uint256 stakedCount)` * `event BroadcastFailed(indexed address outbox, indexed address wallet)` - the adapter reverted. The stake or unstake it accompanied SUCCEEDED anyway; that chain's mirror is stale for the wallet until someone calls rebroadcast * `event UpgradesRenounced()` **Custom Errors** * `error ZeroCollection()` - initialize received a zero collection, or one with no code * `error ZeroOutbox()` - addOutbox received zero * `error OutboxAlreadyRegistered()` * `error OutboxNotRegistered()` * `error EmptyArray()` - stake, unstake, seedStakes or rebroadcastMany called with an empty array * `error NotStaker(uint256 tokenId)` - unstake for a cat not staked by the caller * `error UnstakeExceedsStaked()` - unstake more than staked * `error DirectTransferNotAllowed()` - safeTransferFrom into this contract rejected * `error FutureLookup()` - a historical stake lookup for the current or a future block, where the value is not final yet * `error CheckpointOverflow()` - block number or stake count outside the packed checkpoint range. Unreachable in practice * `error ZeroToken()` - rescue called with a zero token or NFT address * `error RenounceDisabled()` - renounceOwnership is blocked * `error CannotRescueStakedCat(uint256 tokenId)` - rescueERC721 on a cat with a recorded staker * `error ETHTransferFailed()` * `error ZeroRecipient()` - initialize or a rescue received a zero recipient * `error SeedingNotClosed()` - public stake attempted before the latch flipped. Unstake is never shut * `error SeedingAlreadyClosed()` - a seed-phase call after closeSeeding * `error NotSeeder()` - caller is neither `seeder` nor owner * `error SeedLengthMismatch()` - `counts` and `wallets` disagree, a count is zero, or `counts` does not sum to `tokenIds.length` * `error AlreadySeeded(uint256 tokenId)` - the id already has a staker: a repeated id inside one batch, or a re-run batch * `error ZeroSeedSource()` - a cat must be pulled but no seedSource is wired * `error InvalidSeedWallet(address wallet)` - a seed row naming address(0) or this contract, both of which permanently strand the cats * `error ReentrantCall()` * `error UpgradesAreRenounced()` - an upgrade attempted after renounceUpgradeability **Notes** * No lockup, no minimum staking time, no unstake fee, no slashing, in any code path * No pause: stake and unstake are always free actions on V3 * UUPS-upgradeable, owner-gated, with a one-way `renounceUpgradeability()` that converts the contract to immutable without disturbing any position * `seedStakes` is sized by GAS, not by cat count: per-wallet cost is fixed at two checkpoint pushes and does not amortise, so 200 cats across 200 wallets does not fit in one transaction where 200 across 10 wallets fits comfortably * `unstake` is never gated by the seeding latch. If a seeded holder exits before reconciliation, `MotocatSeeder.reconcile` reverts on the total mismatch, which is the correct outcome and not a lock-out: the owner can call `closeSeeding` directly once the discrepancy is understood *** ### MotoToken (packages/contracts/src/token/MotoToken.sol) [#mototoken-packagescontractssrctokenmototokensol] Fixed-supply (10B) ERC-20 + EIP-2612 permit. No mint, no burn function, no owner, no pause, no upgrade path. Entire supply minted to `recipient` at construction. Deployed on Ethereum at `/config.addresses.motoToken`. The token carries a launch gate that governed transfers and approvals before trading opened. It is over: `tradingOpen()` returns `true` on the deployed token and can never return `false` again, so for an integrator MOTO is a plain ERC-20. The gate surface is listed because it is in the ABI. **Constructor** * `constructor(address recipient)` - mint INITIAL\_SUPPLY to `recipient`, who is also recorded as `DEPLOYER`; BadDeploy on zero **Constants / Immutables** * `INITIAL_SUPPLY = 10_000_000_000 * 1e18` * `DEPLOYER: address` - immutable * `WINDOW: uint256` - launch-gate parameter. Read it from the contract **State Variables** * `PAIR: address` - the Uniswap V2 MOTO/WETH pair (`/config.addresses.motoUniswapPair`) * `WETH: address`, `UNWRAPPER: address`, `GATE_SIGNER: address` - bound when the gate was armed * `HARD_OPEN: uint256`, `openAt: uint256` - gate timestamps * `gateArmed: bool`, `armPowerRenounced: bool` * `authorizationRevoked[address => bool]` **External / Public Functions** *ERC-20 and permit:* `name()` = "Motoswap", `symbol()` = "MOTO", `decimals()` = 18, `totalSupply`, `balanceOf`, `allowance`, `approve`, `transfer`, `transferFrom`, `permit`, `nonces`, `DOMAIN_SEPARATOR`, `eip712Domain`. Standard OpenZeppelin behaviour while `tradingOpen()` is true. *Views:* * `tradingOpen() view returns (bool)` - true once the gate is open. One-way * `owner() pure returns (address)` - always the zero address * `quoteBuy(uint256 ethIn) view returns (uint256)` / `quoteSell(uint256 tokenIn) view returns (uint256)` - Uniswap V2 output math (0.30%) against `PAIR`'s live reserves * `reserves() view returns (uint256 rToken, uint256 rWeth)` - `PAIR` reserves, token side first * `isAuthorized(address account) view returns (bool)`, `gateDigest(address account, uint256 authDeadline) view returns (bytes32)` - gate reads *Direct pair access (works after the gate opened; signatures are ignored once `tradingOpen()`):* * `buy(bytes calldata sig, uint256 authDeadline, uint256 amountOutMin, uint256 deadline) payable returns (uint256 amountOut)` - wrap `msg.value` and swap it for MOTO on `PAIR`, delivered to the caller * `sell(bytes calldata sig, uint256 authDeadline, uint256 amountIn, uint256 amountOutMin, uint256 deadline) returns (uint256 amountOut)` - swap the caller's MOTO for ETH on `PAIR`, unwrapped through `UNWRAPPER` *Gate (historical):* * `authorize(address account, uint256 authDeadline, bytes calldata sig)`, `authorizeAndTransfer(address to, uint256 amount, uint256 toDeadline, bytes calldata toSig, uint256 fromDeadline, bytes calldata fromSig) returns (bool)` - no-ops on the gate once trading is open; `authorizeAndTransfer` then behaves as `transfer` *Deployer-only:* * `armGate(address gateSigner, address factory, address weth, bytes32 pairInitCodeHash)`, `renounceArmPower()`, `openNow()`, `setAuthorizationRevoked(address account, bool revoked_)`, `addLiquidity(bytes, uint256, uint256 tokenAmount, uint256 expectedTokensInPair, uint256 minLiquidity, address lpRecipient, uint256 deadline) payable returns (uint256 liquidity)` - launch-day operations. `openNow` and `setAuthorizationRevoked` revert AlreadyOpen now that trading is open **Events** * `event Transfer(indexed address from, indexed address to, uint256 value)`, `event Approval(indexed address owner, indexed address spender, uint256 value)` * `event Authorized(indexed address account, indexed address by, uint256 authDeadline)` * `event AuthorizationRevoked(indexed address account, bool revoked)` * `event TradingScheduled(uint256 openAt, indexed address by)` * `event GateArmed(indexed address pair, indexed address gateSigner, address weth, uint256 hardOpen, address unwrapper)` * `event ArmPowerGivenUp(indexed address by)` **Custom Errors** * `error GateClosed()`, `error ApprovalsClosed()` - a transfer or approval before trading opened. Unreachable now * `error BadSignature()`, `error AuthorizationExpired()`, `error AuthorizationRevoked_()` - gate authorisation failures * `error Expired()` - `buy`/`sell`/`addLiquidity` past `deadline` * `error Slippage()` - output below the caller's minimum, or zero * `error Reentrancy()` * `error WethTransferFailed()`, `error DirectEthRejected()` - the token rejects plain ETH transfers * `error NotDeployer()`, `error BadDeploy()`, `error GateAlreadyArmed()`, `error GateNotArmed()`, `error ArmPowerGone()`, `error AlreadyOpen()`, `error DonationExceedsSeed()`, `error TransientStorageUnavailable()` - deployer-path gates **Notes** * EIP-2612 permit domain: `name = "Motoswap"`, `version = "1"` * No owner, no upgrade path, no pause, no burn function. Burns elsewhere in the system are transfers to `0x…dEaD` *** ### MotoTokenUnwrapper (packages/contracts/src/token/MotoTokenUnwrapper.sol) [#mototokenunwrapper-packagescontractssrctokenmototokenunwrappersol] Deployed by `MotoToken` when its gate was armed. Turns the WETH leg of `MotoToken.sell` into native ETH for the seller. Holds nothing between calls. Address: `MotoToken.UNWRAPPER()`. **State Variables (Immutable)** * `TOKEN: address` - the MotoToken that deployed it, and the only caller allowed * `WETH: address` **External / Public Functions** * `unwrapTo(address to, uint256 amount)` - withdraw `amount` WETH and send it to `to` as ETH; NotToken unless the caller is `TOKEN` **Custom Errors** * `error NotToken()`, `error DirectEthRejected()` (ETH from anything but WETH), `error EthTransferFailed()` *** ### TreasuryVesting (packages/contracts/src/token/TreasuryVesting.sol) [#treasuryvesting-packagescontractssrctokentreasuryvestingsol] Step-vester for treasury allocation: 10 equal tranches released every 180 days over 5 years to fixed beneficiary. Lazy funding (balance + released = total). UUPS proxy. Deployed on Ethereum at `/config.addresses.treasuryVesting`. **Constructor / Init** * `initialize(IERC20 _token, address _beneficiary, uint64 _start, address _owner)` - token, beneficiary and schedule anchor are set once and have no setter; owner authorizes upgrades only **Constants** * `TRANCHES = 10` - equal 10% tranches * `TRANCHE_INTERVAL = 180 days` **State Variables** * `token: IERC20` - MOTO; no setter * `beneficiary: address` - the treasury; no setter * `start: uint64` - schedule anchor; no setter * `released: uint256` - total paid to beneficiary **External / Public Functions** *Core (permissionless release):* * `release() nonReentrant returns (uint256 amount)` - pay all unlocked-but-unpaid tranches to beneficiary; reverts if nothing to release; emits Released *Config (onlyOwner):* * `renounceUpgradeability()` - freeze implementation; one-way *Views:* * `unlockedTranches() view returns (uint256)` - tranches unlocked so far (0..TRANCHES) * `vested() view returns (uint256)` - amount vested so far (of everything ever funded) * `releasable() view returns (uint256)` - amount available to release now * `nextUnlock() view returns (uint256)` - unix time next tranche unlocks (0 when fully unlocked) **Events** * `event Released(uint256 amount, uint256 totalReleased)` **Custom Errors** * `error ZeroAddress()` - init received zero address * `error NothingToRelease()` - release called when releasable() == 0 **Notes** * Lazy funding: total = balance + released (top-ups join the schedule; already-elapsed tranches of new amount become releasable immediately) * No rescue, no pause: only the beneficiary can receive funds *** ### MotoSwapZap (packages/contracts/src/zap/MotoSwapZap.sol) [#motoswapzap-packagescontractssrczapmotoswapzapsol] One-sided liquidity provisioning: swap optimal half, add liquidity with remainder + swap output. Rejects fee-on-transfer tokens. Zap-and-stake also supported. Non-upgradeable. **Constructor** * `constructor(address _router, address _feeRouter, address _masterChef)` - immutable router, feeRouter, factory, masterChef, WETH **State Variables** (none; stateless) **External / Public Functions** *Views:* * `getSwapAmount(uint256 amountIn, uint256 reserveIn, address tokenIn, address otherToken) view returns (uint256)` - optimal half-swap, accounting for live pair fee + protocol fee * `creatorSkimBps(address tokenIn, address otherToken) view returns (uint256 bps)` - total creator skim on the leg: `feeRouter.creatorFeeBps()` counted once per endpoint the registry recognises, so 0x, 1x or 2x. Zero when no registry is wired or the rate is zero; quote assets are never registered *Zap Token→LP:* * `zapInToken(address tokenIn, uint256 amountIn, address otherToken, uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline) nonReentrant ensure returns (uint256 lpAmount)` - deposit tokenIn, mint LP to caller; pulls via safeTransferFrom * `zapInETH(address otherToken, uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline) payable nonReentrant ensure returns (uint256 lpAmount)` - deposit ETH (wrapped to WETH), mint LP to caller *Zap Token→LP→Stake:* * `zapInTokenAndStake(address tokenIn, uint256 amountIn, address otherToken, uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 pid, uint256 deadline) nonReentrant ensure returns (uint256 lpAmount)` - zap + stake into MasterChef pool pid for caller (trusted zapper exemption on MasterChef) * `zapInETHAndStake(address otherToken, uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 pid, uint256 deadline) payable nonReentrant ensure returns (uint256 lpAmount)` **Events** (none) **Custom Errors** * `error PairNotFound()` - no pair for (tokenIn, otherToken) * `error IdenticalTokens()` - tokenIn == otherToken * `error Expired()` - deadline \< block.timestamp * `error EthTransferFailed()` - ETH refund failed * `error InsufficientLpStaked()` - LP minted \< amountLpMin (zap-and-stake only) * `error InsufficientLp()` - LP minted \< amountLpMin (zap only) * `error FeeOnTransferNotSupported()` - token taxes its own transfers * `error PoolPairMismatch()` - pid's pool LP token != expected pair **Notes** * Swap leg goes through FeeRouter (pays protocol fee) * Fee-on-transfer tokens are explicitly rejected (the zap measures real balance changes and reverts `FeeOnTransferNotSupported` if they differ) * Optimal formula reads live factory.swapFeeBps() + feeRouter.protocolFeeBps() * Residual input/other dust is refunded to caller after add-liquidity (first leg) or after stake (second leg) * No MEV protection on add-liquidity leg (amountTokenMin=0, amountOtherMin=0 opt-out individual floors) * `amountLpMin` floors final LP out (protects against sandwich on add-liquidity) *** ### UniswapZap (packages/contracts/src/zap/UniswapZap.sol) [#uniswapzap-packagescontractssrczapuniswapzapsol] One-sided liquidity into Uniswap V2 pairs for launch farming: swap the optimal portion on Uniswap V2, add liquidity, return the LP or stake it into the launch farm for the caller. No owner, no roles, no state. Rejects fee-on-transfer tokens. Non-upgradeable. Deployed on Ethereum at `/config.addresses.uniswapZap`. **Constructor** * `constructor(address router_, address chef_)` - immutable Uniswap V2 router (factory and WETH are read from it) and the `VampChef` **State Variables (Immutable)** * `router: IUniswapV2Router02`, `factory: IUniswapV2Factory`, `WETH: address`, `chef: IVampChefDeposit` **External / Public Functions** * `getSwapAmount(uint256 amountIn, uint256 reserveIn) pure returns (uint256)` - optimal swap portion at Uniswap V2's fixed 0.30% * `zapInToken(address tokenIn, uint256 amountIn, address otherToken, uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline) nonReentrant ensure returns (uint256 lpAmount)` - LP to the caller * `zapInETH(address otherToken, uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline) payable nonReentrant ensure returns (uint256 lpAmount)` - LP to the caller * `zapInTokenAndStake(address tokenIn, uint256 amountIn, address otherToken, uint256 pid, uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline) nonReentrant ensure returns (uint256 lpAmount)` - LP staked into `chef` pool `pid` for the caller * `zapInETHAndStake(address otherToken, uint256 pid, uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline) payable nonReentrant ensure returns (uint256 lpAmount)` **Events** * `event Zapped(indexed address sender, indexed address pair, address tokenIn, uint256 amountIn, uint256 swapAmount, uint256 lpAmount)` **Custom Errors** * `error PairNotFound()`, `error EmptyPair()` (pair with zero reserves), `error IdenticalTokens()`, `error ZeroAmount()`, `error Expired()`, `error EthTransferFailed()`, `error InsufficientLp()`, `error FeeOnTransferNotSupported()`, `error ChefNotSet()`, `error WrongPool()` (`pid`'s LP token is not this pair) **Notes** * `pid` comes right after `otherToken` here. On `MotoSwapZap` it is second to last. Both positions are `uint256`, so a swapped order still encodes * The beneficiary is always `msg.sender`; there is no recipient argument on any path * Unused input and output dust is refunded to the caller; router approvals are zeroed on the way out *** ### UUPSRenounceable (packages/contracts/src/upgrade/UUPSRenounceable.sol) [#uupsrenounceable-packagescontractssrcupgradeuupsrenounceablesol] Abstract base of every UUPS contract above. Not deployed on its own. * `upgradesRenounced() view returns (bool)` - true once the implementation can never be upgraded again * `upgradeSplitArmed() view returns (bool)` - whether a non-zero upgrade authority has ever been installed, after which zero is refused * `event UpgradesRenounced()`, `event UpgradeAuthoritySet(indexed address previousAuthority, indexed address newAuthority)` * `error UpgradesAreRenounced()`, `error NotUpgradeAuthority(address caller)`, `error UpgradeSplitArmed()`, `error UpgradeAuthorityNotAContract(address authority)` Seven contracts expose the split externally (`upgradeAuthority()` and `setUpgradeAuthority(address)`): RakebackV2, Collector, BuybackBurner, MasterChef, RewardVestingEscrow, MotoStaking and PveVault. FeeRouter, VampChef and TreasuryVesting keep upgrades on the owner; the factory keeps them on `feeToSetter`. *** ### Snapshotter (packages/contracts/src/periphery/Snapshotter.sol) [#snapshotter-packagescontractssrcperipherysnapshottersol] One-shot snapshot marker (block + timestamp) for off-chain processes. Owner-only, non-upgradeable Ownable2Step. **State Variables** * `taken: bool` - whether snapshot has been taken * `snapshotBlock: uint256` - snapshot block number * `snapshotTimestamp: uint256` - snapshot timestamp **External / Public Functions** * `snapshot() onlyOwner` - record snapshot marker; one-shot; reverts if already taken; emits SnapshotTaken **Events** * `event SnapshotTaken(indexed uint256 blockNumber, uint256 timestamp)` **Custom Errors** * `error AlreadyTaken()` **Notes** * Purely a marker contract; off-chain indexer/governance reads snapshotBlock and snapshotTimestamp to know which block/time to use for a census *** ### Not covered on this page [#not-covered-on-this-page] * The moto.fun contracts: see [moto.fun contracts](/dev/fun/contracts). * The retired launch contracts under `src/launcher` other than `PveVault`. They are not deployed on Ethereum and nothing integrates against them. * The cross-chain set under `src/crosschain` and `MotocatSeeder`: bridge adapters, outboxes and distribution tooling with no integrator entrypoint on Ethereum. * Interfaces under `src/interfaces`. # moto.fun Addresses, Config and Events (/dev/reference/fun-addresses-config-events) ## 1. Deployed Contract Addresses [#1-deployed-contract-addresses] ### Mainnet (chain 1) [#mainnet-chain-1] moto.fun is deployed with the DEX. Read every address from `/config`; a key that reads the zero address is unset for the current phase. This table names the keys, and `/config` holds the values. | Contract | `/config` key | Address | | -------------------- | ------------------------------ | ---------------------------------------------------------------------------------------- | | `LaunchpadCurve` | `addresses.launchpadCurve` | Deployed. Read the value from `/config` | | `LaunchpadLens` | `addresses.launchpadLens` | Deployed. Read the value from `/config` | | `CreatorFeeRegistry` | `addresses.creatorFeeRegistry` | Deployed. Read the value from `/config` | | `CreatorFeeVault` | `addresses.creatorFeeVault` | Deployed. Read the value from `/config` | | `PveVault` | `addresses.pveVault` | Deployed. Read the value from `/config` | | `MotoToken` (MOTO) | `addresses.motoToken` | Deployed. Read it from `/config`; the [addresses](/dev/addresses) status table prints it | | `MotoSwapFactory` | `addresses.factory` | Deployed. Read the value from `/config` | | `FeeRouter` | `addresses.feeRouter` | Deployed. Read the value from `/config` | | `Collector` | `addresses.collector` | Deployed. Read the value from `/config` | | WETH | `addresses.weth` | `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` (canonical) | | LP burn sink | none | `0x000000000000000000000000000000000000dEaD` | `LaunchpadToken`'s implementation address has no key on the Motoswap `/config`. Read it from the moto.fun backend's `/config.addresses.tokenImplementation` if you need it for CREATE2 derivation, or skip derivation and call `LaunchpadLens.predictToken`. ### Discovering live addresses at runtime [#discovering-live-addresses-at-runtime] Two endpoints, both worth reading once at startup. Full field lists on [addresses and discovery](/dev/fun/addresses). ```ts // 1. The Motoswap API: is moto.fun live, and where is the app? const cfg = await fetch("https://api.motoswap.org/config").then((r) => r.json()); if (!cfg.motofunEnabled) return; // === cfg.addresses.launchpadCurve !== ZeroAddress const { launchpadCurve, launchpadLens } = cfg.addresses; // 2. The app backend: the rest of the deployment, per chain. const funCfg = await fetch(`${cfg.motofunUrl}/backend-fun/${slug}/config`).then((r) => r.json()); const { curve, lens, tokenImplementation, pveVault, weth, moto } = funCfg.addresses; ``` Rules that fall out of it: * **A zero address is a declared value, never an omitted key.** Branch on the value. * The **curve and the lens gate independently**. A non-zero curve with a zero lens means the read surface is unavailable, so do not quote. * `pveVault` zero means PvE is off on that chain. * `motofunEnabled: false` on the **app backend** means the service is up but no curve is set for that chain. Its indexer and keeper idle, and `POST /intents` answers `503`. * If a network's upstream is not configured, the config path answers with the app's HTML rather than JSON. That is the signal that moto.fun is not live there, not an outage. ### Deriving a coin address offline [#deriving-a-coin-address-offline] ``` deployer = the curve initCode = EIP-1167 minimal proxy over tokenImplementation create2Salt = keccak256(abi.encode(creator, salt)) ``` The CREATE2 salt is **not** the `salt` field from the intent. It is bound to the creator, so a salt replayed from another wallet lands at a different address and a signed intent cannot be hijacked into someone else's advertised address. ### Local development [#local-development] Local addresses are not listed and pinning them would be wrong: a local stack mints fresh addresses every run. Start the stack and read its `/config`. *** ## 2. Chain / Network Config [#2-chain--network-config] moto.fun launches on Ethereum with the DEX. Other networks are planned for later. When they arrive, each will get its own curve deployment, coin set and app backend behind a per-network path segment, so read the network from `/config` rather than hardcoding it. | Concept | Where it comes from | | ---------------------------- | ------------------------------------------------------------------------------- | | Whether moto.fun is live | `motofunEnabled` on `/config` (Ethereum only at launch) | | The app origin | `motofunUrl` on the Motoswap `/config` | | The per-network path segment | The chain slug, the same one that appears in share links as `?chain=` | | The chain id | `chainId` on the app backend's `/config` | | The EIP-712 domain | `eip712` on the app backend's `/config`, plus the chain id and the served curve | ### Public endpoints [#public-endpoints] | Surface | Base | | --------------------- | --------------------------------------- | | Motoswap API | `https://api.motoswap.org` | | OpenAPI spec | `https://api.motoswap.org/openapi.json` | | Interactive reference | `https://api.motoswap.org/docs` | | The app backend | `/backend-fun/` | ### WebSockets [#websockets] None. Realtime on the app backend is Server-Sent Events at `/stream`; the Motoswap API has no realtime surface for moto.fun. See [realtime](/dev/fun/realtime). *** ## 3. Domain / Wire Types [#3-domain--wire-types] The Motoswap API uses the standard Motoswap wire types; see the [cheat sheet](/dev/api#wire-types-cheat-sheet). The app backend is looser and needs its own rules: | Domain | Wire | Notes | | ------------------------------------ | -------------------- | -------------------------------------------------------- | | wei, uint256 | decimal string | Exact. Never a number | | `priceX18` | decimal string | 1e18 fixed point. ETH per whole coin | | Scaled columns (`*_gwei`, `*_milli`) | number | Lossy, for sorting and display only | | Candle `low` / `high` | **number** | Floats. The one place prices are numbers | | Candle `open` / `close` | **string, optional** | Treat as possibly absent | | Addresses | lowercase string | Not always regex-validated on input | | Timestamps | number | Unix seconds, except `curveReadAt` which is milliseconds | | Status | number | `0` None, `1` Trading, `2` Frozen, `3` Graduated | ### `null` policy [#null-policy] `ethUsd` is **nullable on `/stats` and `/pve/value`** and **non-nullable on `/tokens`, `/tokens/{address}` and `/creators/top`**. This is intentional: the first pair uses a real-price-only read so a null means "no real price", never a fabricated one; the second falls back to a configured rate. Do not assume one rule across the service. `metadata` is `null` rather than wrong when its hash does not match the on-chain commitment. `curve` is `null` when the chain read failed past its stale window, with `curveLive: false` marking both that and a stale-but-served state. `pairWeth` and `pairMoto` are `null` until graduation. *** ## 4. Fee Schedule and Curve Constants [#4-fee-schedule-and-curve-constants] ### Curve fee [#curve-fee] | Component | Value at ship | Cap | Where | | ------------------- | --------------------- | ---------------------------- | ------------------------------- | | Protocol fee | `70` bps (0.70%) | `MAX_PROTOCOL_FEE_BPS = 100` | `LaunchpadCurve.protocolFeeBps` | | Creator fee | `50` bps (0.50%) | `MAX_CREATOR_FEE_BPS = 50` | `LaunchpadCurve.creatorFeeBps` | | **Total per trade** | **`120` bps (1.20%)** | `150` bps | Both sides, buys and sells | Both are owner-tunable state, not constants. The creator fee ships **at its cap**, so it can only be lowered. Read them at trade time. This curve rate differs from the DEX, where a swap pays 1% (1.3% on a graduated moto.fun coin). The fee comes off the ETH leg: off the input on a buy, off the output on a sell. Only the net, post-fee ETH enters the curve's reserve. **Split**: the protocol share transfers to the `Collector` in WETH, at the same rate and to the same address as the DEX `FeeRouter`. The creator share transfers to `CreatorFeeVault` in WETH and is credited per coin. If the registry cannot be read, or the curve is no longer an authorised depositor, the **entire** fee goes to the Collector and `CreatorFeeRoutedToCollector` fires. The trader-facing rate never changes. ### Points and Rakeback parity [#points-and-rakeback-parity] Because the curve's protocol fee is the same rate, in the same asset, to the same Collector, and the `Trade` event carries the literal transfer amount, a curve trade credits **exactly** what a DEX swap of the same size credits. Same Points, same Rakeback, same balances, same weekly epochs, same 0.20% Rakeback share. There is no separate curve pot. The curve's **creator** fee is a different leg entirely: it reaches the `CreatorFeeVault`, never the Rakeback pool. ### Curve constants [#curve-constants] | Constant | Value | Meaning | | ------------------------------------- | ---------------------- | ------------------------------------------------------------------------------ | | `SUPPLY` | `1e27` (1,000,000,000) | Fixed supply per coin | | `FLOOR` | `2e26` (200,000,000) | **Parameter set `0`.** The graduation floor for a coin whose `paramsId` is `0` | | `VIRTUAL_TOKENS` | `66_666_667e18` | **Parameter set `0`.** Virtual token reserve | | `VIRTUAL_ETH` | `4 ether / 3` | **Parameter set `0`.** Virtual ETH reserve | | `SELL_REOPEN_DELAY` | `15 minutes` | Frozen coin sell reopen | | `MIN_TWAP_WINDOW` / `MAX_TWAP_WINDOW` | *not published* | MOTO reference window. Read them from the contract | | `MAX_BLOCKED_QUOTES` | *not published* | Pre-bond blocklist quote assets. Read it from the contract | The three set `0` rows are in force for every coin stamped `0`, which is what a fresh deployment stamps until the owner appends a set. They are not a property of the contract. Each coin is priced by the parameter set it was stamped with at launch: read it with `paramsOf(token)`, which returns `(virtualEth, floorSupply, virtualTokens)`. See [each coin has its own curve parameters](/dev/fun/contracts#each-coin-has-its-own-curve-parameters). Derived: about **4 ETH** of net buying sits in a curve at graduation, at a market cap of about **20 ETH**. ### Graduation constants [#graduation-constants] | Constant | Value at ship | Cap | | --------------------- | --------------- | ------------------------------------------------------------------------------------- | | `graduationBounty` | `0.015 ether` | `MAX_BOUNTY = 0.08 ether`, and clamped to 2% of the pot at graduation | | `maxTwapDeviationBps` | *not published* | The MOTO buyback tolerance band. Read it from the contract. There is no "off" setting | The pot after the bounty splits **50/50** between the TOKEN/WETH pool leg and the MOTO buyback. Both pools seed at the final curve price. Both LP positions mint to `0x…dEaD`. *** ## 5. Events - Integrator Reference [#5-events---integrator-reference] ### LaunchpadCurve [#launchpadcurve] The five an indexer needs: ```solidity event TokenCreated( address indexed token, address indexed creator, string name, string symbol, bytes32 metadataHash, address indexed quoteToken ); ``` `topic0` is `0x6223a619ba904a9914dac4461d25f8f27e372c0a030791c3a5c012768765ef45`, and the event has four topics: the hash, `token`, `creator`, `quoteToken`. `quoteToken` is the curve's WETH address; see [which asset a coin is quoted in](/dev/fun/contracts#which-asset-a-coin-is-quoted-in). The discovery event. Fires at materialization on every launch, whichever path. `creator` is the signer, which is also `creatorOf[token]` and the PvE beneficiary. It is **not** necessarily the fee recipient. ```solidity event Trade( address indexed trader, address indexed token, bool isBuy, uint256 ethAmount, uint256 tokenAmount, uint256 priceX18, uint256 protocolFee, uint256 creatorFee ); ``` Every buy and every sell. Field notes: * `ethAmount` is the curve-leg gross: **fee-inclusive on buys, pre-fee on sells**. Do not sum it as one consistent notion of volume without deciding which you mean * `priceX18` is the spot price **after** the trade, 1e18 fixed point, the same number `LaunchpadLens.currentPrice` returns * `protocolFee` is the literal WETH transfer to the Collector; `creatorFee` the literal deposit into the vault. Both are amounts, not rates, so a fee-schedule change needs no special handling * **`isBuy` is not indexed.** Buys cannot be topic-filtered from sells; decode and branch ```solidity event FloorReached(address indexed token, uint256 realEth); ``` The coin froze. `tokenReserve` is exactly the coin's own `floorSupply` from this point, and all trading is paused until graduation or the sell-reopen delay expires. ```solidity event Graduated( address indexed token, address pairWeth, address pairMoto, uint256 wethLp, uint256 motoLp, uint256 tokensLpWeth, uint256 tokensLpMoto, uint256 dustBurned, address indexed keeper ); ``` **The pair addresses are not indexed.** Pair discovery requires decoding the data, not a topic filter. `keeper` is whoever sent the transaction and collected the bounty. `dustBurned` is the coin-side remainder sent to `0x…dEaD`. ```solidity event PveFill(address indexed token, address indexed contributor, uint256 tokens); ``` Every escrow fill, from a launch slice or a public `buyPve`. `contributor` is who paid. Everything else on the curve: ```solidity event FeeRecipientSet(address indexed token, address indexed recipient); event CreatorFeeRoutedToCollector(address indexed token, uint256 amount); event CreatorFeeUnregistered(address indexed token, address indexed creator); event LaunchParamsSet(uint16 indexed id, uint128 virtualEth, uint128 floorSupply, uint256 virtualTokens, uint256 impliedRaise); event RaiseBandSet(uint128 minRaise, uint128 maxRaise); event UpgradeAuthoritySet(address indexed previous, address indexed next); event PveRecorded(address indexed token, uint256 tokens); event PveRecordFailed(address indexed token, uint256 tokens); event BountyOwed(address indexed keeper, uint256 amount); event BountyClaimed(address indexed keeper, uint256 amount); event PriceCheckpointed(uint256 cumulative, uint32 timestamp); event FeesSet(uint256 protocolFeeBps, uint256 creatorFeeBps); event GraduationBountySet(uint256 bounty); event CollectorSet(address collector); event MaxTwapDeviationBpsSet(uint256 bps); event BlockedQuoteAssetsSet(address[] quotes); event LaunchesPausedSet(bool paused); event AllBuysPausedSet(bool paused); event TokenBuysPausedSet(address indexed token, bool paused); event PveVaultSet(address vault); event PveKeeperSet(address keeper); ``` Semantics worth catching: * **The curve is a proxy, so it also emits the standard proxy and ownership events** from its own address: `Upgraded(address indexed implementation)`, `Initialized(uint64 version)`, `OwnershipTransferStarted` and `OwnershipTransferred`, and `UpgradesRenounced()`. The deploy block carries `Upgraded`, `OwnershipTransferred`, `UpgradeAuthoritySet` and `Initialized`. Do not let an unknown-topic handler treat them as errors * `LaunchParamsSet` means coins launched from that block on are priced by a new parameter set. Coins already launched are unaffected * **`FeeRecipientSet` fires only when the recipient differs from the creator.** Its absence means the creator is the recipient. Do not treat a missing event as "no fee recipient" * `CreatorFeeRoutedToCollector` is the creator leg failing over. A merely *disabled* creator fee is silent, so this event means a read failure or a revoked depositor, not a policy change * **`CreatorFeeUnregistered` is declared and no longer emitted.** A registry refusal with the coin's slot still empty used to launch the coin without a creator fee and fire this event. It now reverts `RegistryRefused`, so no coin launches unregistered. Keep the event in your ABI only to decode logs from before that change. `CreatorFeeRoutedToCollector` is a different thing and still live: it fires per trade, on a coin that does have a creator, when the creator leg could not be paid * A launch does **not** fail open in general. A slot held by another address reverts `CreatorAlreadyClaimed`, and an unreadable registry reverts `RegistryUnreadable`. The empty-slot refusal above is the only outcome that lets a coin launch unregistered * `PveRecordFailed` means the escrow is still held and awaiting a `recordPveLate` retry * `LaunchesPausedSet(false)` and `AllBuysPausedSet(false)` also fire from `renounceOwnership`, which clears both before renouncing so a pause cannot latch forever ### LaunchpadToken [#launchpadtoken] ```solidity event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); ``` Standard ERC-20. The launch mint appears as `Transfer(0x0, curve, 1e27)`. There is no bonded event: the `bonded` flag is set inside the graduation transaction and read as state. ### LaunchpadLens [#launchpadlens] No events. It is a pure read surface. ### CreatorFeeRegistry [#creatorfeeregistry] ```solidity event TokenRegistered(address indexed token, address indexed creator, address indexed registrar); event CreatorSet(address indexed token, address indexed creator); event CreatorSetBySelf(address indexed token, address indexed previousCreator, address indexed creator); event TokenDisabled(address indexed token, bool disabled); event RegistrarSet(address indexed registrar, bool allowed); ``` `registrar` distinguishes which surface registered a coin. `CreatorSet` fires on an owner takeover and on a creator's own redirect (`CreatorSetBySelf` fires only on the second). Either one re-attributes the coin's **full** accrued history, so a creator dashboard folded on the latest `CreatorSet` never splits earnings by ownership period. ### CreatorFeeVault [#creatorfeevault] ```solidity event Accrued(address indexed token, address indexed quoteAsset, uint256 amount); event Claimed(address indexed token, address indexed creator, address indexed quoteAsset, uint256 amount); event DepositorSet(address indexed depositor, bool allowed); ``` `Accrued` is the only accrual writer, and `notifyDeposit` is the vault's single deposit chokepoint, so this event covers **every** depositor: the DEX `FeeRouter`, the moto.fun curve, and anything wired next. `Claimed.creator` is who was actually paid. Do not conflate the two creator-fee representations. `Accrued` and `Claimed` are the vault ledger and cover both launch surfaces. `Trade.creatorFee` is the curve's own leg and is what venue-level curve revenue is measured from. ### PveVault [#pvevault] External to this repo. Its events are documented with the [DEX contracts](/dev/reference/addresses-config-events#pvevault). *** ## 6. Indexing Rules [#6-indexing-rules] * **Discovery is one address.** Watch `TokenCreated` on the curve. There is no factory to enumerate and no `PairCreated` sweep until a coin graduates * **Key on `(blockNumber, txHash, logIndex)`** and keep raw events append-only. Every official insert is idempotent on `(txHash, logIndex)`, so a re-scan is a no-op and over-rewinding a reorg is free * **Do not store a derived status column.** Fold it from the latest surviving lifecycle event, so a rollback is a range delete with nothing to rebuild. This is what both official indexers do * **Confirmation depth** on the app backend defaults to 3 blocks, with hash-based reorg detection and a 200-block wedge limit * **Freeze is reversible.** A `Frozen` coin that nobody graduates reopens for sells after 15 minutes, and a sell returns it to `Trading`. Model the lifecycle as a state machine with that back edge, not as a one-way ratchet * **Graduation market cap** should be snapshotted once and never re-priced. A graduation lost to a reorg must clear it * **The curve's `Trade` event is the trade-level truth.** After graduation, trades are ordinary pair and `FeeRouter.Swap` events; see the [DEX event catalog](/dev/reference/addresses-config-events#5-events---integrator-reference) # moto.fun API - Full Inventory (/dev/reference/fun-api-full-inventory) ## moto.fun API inventory [#motofun-api-inventory] ### Complete Endpoint Reference for External Integrators [#complete-endpoint-reference-for-external-integrators] Two services carry moto.fun data. They are not the same backend and they do not share conventions. | | Motoswap API | The moto.fun app backend | | ------------------ | -------------------------------------------------------------------- | --------------------------------------- | | Base | `https://api.motoswap.org` | `/backend-fun/` | | Framework | Hono, OpenAPI-documented | Hono on Bun | | In `/openapi.json` | Yes | No | | ETags | On cached GETs (not `/config`, `/health/live`, `/version` or writes) | Yes, on JSON `200` responses | | Pagination | Cursor | None. Hardcoded caps | | Auth | None on reads | None on reads | | Realtime | No | SSE | `motofunUrl` comes from `GET https://api.motoswap.org/config`. See [addresses](/dev/fun/addresses). *** ## PART 1 - MOTOSWAP API (`api.motoswap.org`) [#part-1---motoswap-api-apimotoswaporg] ### Conventions [#conventions] Wire types are the standard Motoswap ones: addresses lowercase strings, wei and uint256 as decimal strings, USD as decimal-dollar strings (`"1234.56"`), seconds as numbers. Error body is `{ "error": "" }`. Non-2xx responses are never cached. Cache tiers on this group: `BLOCK` is `public, max-age=3, s-maxage=3, stale-while-revalidate=9`. `STATIC` is `max-age=300, s-maxage=300, stale-while-revalidate=900`. **ETag versioning**: every `/motofun/*` route versions on the freshest block across the three moto.fun raw tables, not on the chain head. A curve with no activity keeps returning `304` through every DEX block. Send `If-None-Match` and poll freely. **Rate limit**: `/motofun/*` is per-IP burst 600, 150 per second. `/launchpad/*` is burst 120, 30 per second, tighter because the logo route serves bytes. **The `404` that is not a `404`**: every `/motofun/*` route answers `404 { "error": "moto.fun is not enabled on this deploy" }` when the configured curve address is zero. That is a deployment state, not a missing resource. Check `/config.motofunEnabled` first. **USD is frozen at write time.** Every USD field except `tvlUsd` is a snapshot taken when the row was written and is never re-priced. `tvlUsd` is a live gauge. **"Now" is the indexer head block timestamp**, not wall clock, in every 24h window on this group. *** ### GET /motofun/stats [#get-motofunstats] Venue-wide counters. No parameters. Tier `BLOCK`. ```json { "coins": 0, "bonding": 0, "frozen": 0, "graduated": 0, "tvlUsd": "0", "volume24hUsd": "0", "volumeAllUsd": "0", "fees24hUsd": "0", "feesAllUsd": "0", "protocolFees24hUsd": "0", "creatorFees24hUsd": "0", "trades24h": 0, "launches24h": 0 } ``` * `coins`, `bonding`, `frozen`, `graduated`, `trades24h`, `launches24h`: numbers * Every `*Usd` field: decimal-dollar string * `tvlUsd` is the sum of real ETH in **non-graduated** curves at the live WETH price. Graduated coins' liquidity lives in pools and is counted by the DEX side, not here * `bonding = max(0, coins - graduated - frozen)` Statuses: `200`, `404` (moto.fun off). *** ### GET /motofun/coins [#get-motofuncoins] Newest-first coin list. Tier `BLOCK`, query allow-list `["cursor"]`. | Param | In | Type | Required | Default | | -------- | ----- | ------ | -------- | ------- | | `cursor` | query | string | no | none | Page size is 25. The cursor is an opaque signed keyset cursor over `(timestamp, token_address)`. ```json { "items": [ { "address": "0x…", "name": "string", "symbol": "string", "creator": "0x…", "timestamp": 0, "status": "bonding", "pairWeth": "0x…", "pairMoto": "0x…", "volume24hUsd": "0", "volumeAllUsd": "0", "trades": 0, "lastPriceX18": "0" } ], "nextCursor": null, "hasMore": false } ``` * `status`: `"bonding" | "frozen" | "graduated"`, folded on read from the latest surviving lifecycle row. A coin with no lifecycle row is `"bonding"` * `pairWeth`, `pairMoto`: address or `null`. Non-null only after graduation * `lastPriceX18`: decimal string of the latest trade's `price_x18`, or `null` if the coin never traded * `trades`: number There is no cursor error hook on this route. An undecodable cursor produces an empty page with status `200`. Check `hasMore` rather than treating an empty `items` as the end of the list. Statuses: `200`, `404` (moto.fun off). *** ### `GET /motofun/coins/{address}` [#get-motofuncoinsaddress] Point lookup: is this a curve coin, and has it graduated? | Param | In | Type | Required | | --------- | ---- | ------- | -------- | | `address` | path | address | yes | ```json { "address": "0x…", "name": "string", "symbol": "string", "status": "bonding", "pairWeth": "0x…", "pairMoto": "0x…" } ``` No volume, price, or creator fields; that is the list shape only. Statuses: `200`; `404 { "error": "moto.fun is not enabled on this deploy" }`; `404 { "error": "this token never launched on the moto.fun curve" }`. Tier `BLOCK`. The version is venue-wide but the ETag folds the request path, so the tag is unique per address. *** ### GET /motofun/revenue/series [#get-motofunrevenueseries] Per-bucket venue revenue, Collector leg and creator leg separately. | Param | In | Type | Required | Default | Values | | -------- | ----- | ---- | -------- | ------- | ------------------------ | | `range` | query | enum | no | `"30d"` | `7d`, `30d`, `90d`, `1y` | | `bucket` | query | enum | no | `"day"` | `day`, `epoch` | ```json { "bucket": "day", "buckets": [ { "t": 0, "total": "0", "protocol": "0", "creator": "0" } ] } ``` * `t`: bucket start, unix seconds * `total`, `protocol`, `creator`: decimal-dollar strings * The series is dense and zero-filled from the range start to the head bucket inclusive * `protocol` and `creator` are measured from the `Trade` event's exact fee wei, never derived from a bps constant Tier `BLOCK`, query allow-list `["range", "bucket"]`. Statuses: `200`, `404`. *** ### moto.fun on shared Motoswap routes [#motofun-on-shared-motoswap-routes] **`GET /config`** (tier `STATIC`) carries: | Key | Type | Notes | | -------------------------- | ------- | --------------------------------------------- | | `addresses.launchpadCurve` | address | Zero when moto.fun display is off | | `addresses.launchpadLens` | address | Gated independently of the curve | | `motofunEnabled` | boolean | Exactly `launchpadCurve != 0x0` | | `motofunUrl` | string | The moto.fun app origin. `""` means fall back | | `launchPhase` | enum | `"prelaunch" \| "vamp" \| "dex"` | **`GET /activity`** accepts `venue`: `"motoswap" | "motofun" | "all"`, default `"all"`. moto.fun rows carry `kind` of `motofun_launch`, `motofun_trade`, or `motofun_graduation`. With moto.fun off, the venue is forced to `"motoswap"` regardless of the request, because rows may exist in the database on a display-off deploy. **`GET /analytics/overview`** carries an **optional** top-level key, absent entirely when moto.fun is off: ```json { "motofun": { "volumeUsd": "0", "feesUsd": "0" } } ``` moto.fun is an additive component here. The DEX revenue series (`/analytics/revenue/series`, `/stats/series`) do **not** include curve volume; use `/motofun/revenue/series` for that. **`GET /creator/{address}`** tags every coin with the surface that registered it: ```json { "address": "0x…", "tokens": [ { "token": "0x…", "disabled": false, "venue": "motofun", "lastClaimAt": 0, "usdEarned": "0", "quotes": [ { "quoteAsset": "0x…", "earned": "0", "usdEarned": "0", "swaps": 0, "claimed": "0", "unclaimed": "0" } ] } ], "series": [ { "day": 0, "usdEarned": "0", "swaps": 0 } ], "totalUsdEarned": "0" } ``` `venue` is `"motofun" | "deployer"`. `deployer` marks coins from the retired launch contract; new coins are always `motofun`. Both surfaces deposit into the **same** `CreatorFeeVault`, so this is a label for display, never a branch in the claim path. A coin is `"motofun"` if and only if it has a curve row on this chain. An unknown address returns `200` with empty arrays, never `404`. `GET /creator/token/{token}` does **not** carry `venue`. **`GET /launchpad/token/{address}/logo`** serves a coin's logo as bytes. * `200`: image bytes, `Content-Type: image/png` or `image/webp`, tier `STATIC`, `ETag`, `X-Content-Type-Options: nosniff`. `304` on a matching `If-None-Match` * `400 { "error": "invalid token address" }`, `404 { "error": "no logo" }`, both `private, no-store` * On a local miss the API proxies once from the moto.fun backend, persists the result, and serves it locally from then on. Accepted payloads are PNG or WebP, non-empty, at most 256 KB and 512x512. A bounded negative cache stops address cycling turning this into an unmetered amplifier * The `POST` on the same path is a separate signed-upload route that a moto.fun coin cannot use, which is why the proxy exists Both logo routes are excluded from `/openapi.json` by design: one serves bytes, the other takes a multipart body. Every `/motofun/*` route **is** in the spec, on a flag-off deploy as well, because the flag decides whether a route answers, not whether it exists. *** ## PART 2 - The moto.fun app backend [#part-2---the-motofun-app-backend] Base `/backend-fun/`. Every path below is relative to that. ### Conventions [#conventions-1] * **No auth on any read.** `POST /intents` is gated by an EIP-712 signature, not by a key. The `/admin/*` group is operator-gated and answers `401` without a credential * **ETags on JSON `200` responses**, weak (`W/""`), over the serialized body. `If-None-Match` gets a `304` with no body, carrying the same `ETag` and `Cache-Control`. No `Last-Modified`. No ETag on a redirect, on the PNG routes, or on any non-`200` * **Pagination on two lists only.** `GET /tokens` and `GET /graduated/feed` page with a signed, opaque cursor and a `limit`. A forged, edited or stale cursor answers `400 { "error": "bad cursor" }` and an out-of-range limit `400 { "error": "bad limit" }`. Every other list has a fixed cap and no paging * **Error body** is `{ "error": "" }`, except `GET /share/{address}`, which answers `text/plain` on error * **Status codes in use**: 200, 201, 302, 304, 400, 401, 403, 404, 409, 413, 429, 500, 503. `304` answers a matching `If-None-Match`. `302` is the image route's redirect to the CDN. `403` is a banned party or a taken-down coin, both on `POST /intents` * **Body limit** 256 KB; over it returns `413 { "error": "request body too large" }` * Unhandled errors become `500 { "error": "internal error" }`; the stack is never sent to the client **Rate limits.** Per-IP token buckets, each over a 10-minute window with continuous refill. Each cap is env-configurable; the values below are the shipping defaults, not a contract. | Limiter | Env var | Default | Applies to | | --------------- | ------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Intent writes | `INTENTS_PER_IP_PER_10MIN` | 5 | `POST /intents` only | | Board | `BOARD_READS_PER_IP_PER_10MIN` | 6000 | `GET /tokens` | | Expensive reads | `READS_PER_IP_PER_10MIN` | 3000 | `GET /pve/value`, `GET /status`, `GET /graduated/feed`, `GET /wallet/{address}/trades-today`, `GET /fees/history/{wallet}`, `GET /wallet/{address}/positions` | | Creators | `CREATORS_TOP_READS_PER_IP_PER_10MIN` | 3000 | `GET /creators/top` | | Coin detail | `TOKEN_DETAIL_READS_PER_IP_PER_10MIN` | 30000 | `GET /tokens/{address}`, and one token per `GET /tokens/{address}/snapshot` | | Quotes | `QUOTE_READS_PER_IP_PER_10MIN` | 28800 | `GET /tokens/{address}/quote`. A second, smaller budget covers quotes that miss the cache, refused with `429 { "error": "too many new quotes" }` | Every other public read is unlimited, `/health` included. Read-limit body is `{ "error": "rate limited" }`. SSE is capped separately at 2000 global (`503` over it) and 40 per IP (`429` over it) by default, both env-configurable, and both refusals carry `Retry-After`. `GET /tokens`, `GET /pve/value` and `GET /creators/top` answer a refusal with `Retry-After` in seconds and `Cache-Control: no-store`. The remaining throttled routes send a bare `429` with no `Retry-After`, so do not depend on the header being present. Under a continuous-refill bucket `Retry-After` is the refill time for one token, about a second at these caps, and it is floored at 1 so it is never `0`. **Cache headers** are set per route from a table covering every route, and the coverage is enforced by a test that fails the build when a route has no entry. | Tier | Wire value | Routes | | --------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `no-store` | `no-store` | `/health`, `/health/live`, `/version`, `/intents/precheck`, `POST /intents`, `/stream`, every `/admin/*`, `/wallet/{address}/fee-reroutes`, `/wallet/{address}/trades-today`, `/wallet/{address}/positions`, `/fees/history/{wallet}` | | Live revalidate | `no-cache` | `/config`, `/intents/{address}`, `/tokens`, `/tokens/{address}`, `/tokens/{address}/candles`, `/tokens/{address}/holders`, `/trades/recent`, `/stats` | | Short window | `public, max-age=10, stale-while-revalidate=20` | `/creators/top` | | Short window | `public, max-age=30, stale-while-revalidate=30` | `/pve/value` | | Unfurl | `public, max-age=300, stale-while-revalidate=600` | `/share/{address}` | | Deprecated | `public, max-age=300` | `/tickers/{symbol}/available` | | Handler-owned | `public, max-age=86400` (CDN redirect), `public, max-age=300` (inline) | `/tokens/{address}/image` | | Handler-owned | `public, max-age=30` | `/tokens/{address}/card.png` | `no-cache` stores the body and revalidates before every reuse; it is not `no-store`. The wallet money routes are `no-store` rather than `no-cache` deliberately, so a claimable balance or a day's trades cannot be served out of a shared cache to the wrong screen. Two rules decide what a non-2xx gets, in this order. A `no-store` route stays `no-store` whatever it answers, because that tier is applied before the status is looked at and nothing softens it. On every other route, a handler that set its own `Cache-Control` keeps it; only when the handler set none does an error get downgraded to `no-cache`, so a `404` or a `429` cannot inherit that route's positive `max-age`. **Server-side caches**: `/tokens` 5s, `/stats` 15s, `/creators/top` 20s, per-coin curve state 3s live with a 60s stale-serve, PvE value 45s, share card 30s, the ETH/USD rate 60s, a creator's launch nonce 30s. **Response caps**: | Route | Cap | | ----------------------------------- | ------------------------------- | | `/tokens` coins / pending | 500 / 100 | | `/tokens/{address}` recentTrades | 50 | | `/tokens/{address}/candles` | 2000 buckets, plus one seed row | | `/tokens/{address}/holders` | 10 | | `/trades/recent` trades / creations | 20 / 6 (last 24h) | | `/creators/top` | 25 | | `/wallet/{address}/trades-today` | 50 | | `/fees/history/{wallet}` | 200 | | `/wallet/{address}/positions` | uncapped, dust-filtered | `/tokens`, `/tokens/{address}` and `/intents/{address}` select whole database rows, so a schema migration adds fields automatically. Treat every field list below as "at least these fields". Do not write a decoder that rejects unknown keys. *** ### GET /config [#get-config] Static per deploy. No database read, no RPC. No parameters. Always `200`. ```json { "chainId": 1, "addresses": { "curve": "0x…", "lens": "0x…", "tokenImplementation": "0x…", "pveVault": "0x…", "weth": "0x…", "moto": "0x…", "creatorFeeRegistry": "0x…", "creatorFeeVault": "0x…" }, "eip712": { "name": "MotoswapLaunchpad", "version": "1" }, "motofunEnabled": true, "publicOpen": true, "launchOpensAt": null } ``` `addresses.curve` is the curve's proxy. `creatorFeeRegistry` and `creatorFeeVault` are read off the curve after boot and serve `null` until that read lands: `null` means "not known yet", not the zero address. `publicOpen` is an operator switch that gates the app's launch-pending screen and nothing else; every route answers normally while it is `false`. `launchOpensAt` is an ISO 8601 UTC string or `null`, and `null` means no opening time was given to this deploy. `addresses.pveVault` is the zero address when PvE is not live on this chain, a **declared value, never an omitted key**. `motofunEnabled: false` means the service is up but no curve is deployed here. *** ### GET /health [#get-health] Operational. Unauthenticated by design. Do not point an integration at it as a data source. ```json { "ok": true, "chainId": 1, "curve": "0x…", "indexer": { "lastIndexedBlock": "8123456", "headBlock": "8123459", "lagBlocks": 3, "lastSuccessfulTickAgoMs": 1200, "reorgCount": 0, "lastReorgAgoMs": null, "wedged": null }, "frozenCoins": { "count": 0, "hiddenCount": 0, "oldestSeconds": null, "stuck": false, "maxSeconds": 300, "tokens": ["0x…"] }, "keeper": { "enabled": true, "address": "0x…", "balanceWei": "12000000000000000", "lastPassAt": 1756000000000, "lastPassAgeMs": 4000, "lastError": null, "stalled": false, "missing": false }, "pveRecordFailures": { "count": 0, "tokens": [] }, "indexerErrors": { "count": 0, "recentAgoMs": null, "recent": [{ "txHash": "0x…", "logIndex": 3, "eventName": "Trade", "blockNumber": 8123400 }], "maxAgeSeconds": 3600 } } ``` `lastIndexedBlock` and `headBlock` are decimal strings; `lagBlocks` is a number. On a chain with no curve deployed, each section is replaced by a `disabled` string and the response carries `"mode": "pre-curve"`, still with `200`. The HTTP code is `200` or `503` and covers only wedge, tick age and lag. The `ok` field is stricter: it also requires no stuck graduations, a live keeper, and no recent indexer errors. `200` with `"ok": false` is a real, reachable state. *** ### GET /health/live [#get-healthlive] The liveness probe, and the path the platform healthcheck points at. One trivial check, no database fold, no RPC, exempt from load shedding. No parameters. Always `200`. Tier `no-store`, because a liveness answer served from a cache reports a dead process as alive to the one reader that can restart it. ```json { "ok": true, "commit": "a1b2c3d" } ``` Poll this rather than `/health` if all you need is up-or-down. `/health` is a deep status fold over the database and is far more expensive to answer, but it is not rate-limited: an uptime probe of it is never charged against any bucket. *** ### GET /version [#get-version] Build provenance: which build is serving right now. No parameters. Always `200`. Tier `no-store`, for the same reason as above inverted, a cached copy would report the previous commit after a deploy and confirm a roll that did not take. ```json { "commit": "a1b2c3d", "builtAt": "2026-01-01T00:00:00.000Z", "dirty": false, "branch": "master", "chainId": 1 } ``` Use it to confirm a deploy actually landed, and to pin which build a bug report came from. *** ### GET /tokens [#get-tokens] The board. With no parameters it answers the whole board in one body, up to 500 coins and 100 pending, cached 5s, and that is the form to poll. | Parameter | Type | Default | Notes | | --------- | ------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `limit` | integer | `50` once `limit` or `cursor` is sent | `1` to `100`, else `400 { "error": "bad limit" }` | | `cursor` | string | none | The previous body's `nextCursor`, verbatim. Signed and bound to the `q` it was issued for, else `400 { "error": "bad cursor" }`. Treat that as "restart the scan": a cursor can stop verifying across a backend restart | | `q` | string | none | Case-insensitive search, trimmed, at most 64 characters, else `400 { "error": "bad q" }` | Every body carries `nextCursor`: a string when the page came back full, `null` at the end. Pages run newest coin first. `429` with `Retry-After` when the board bucket is empty. ```json { "tokens": [ { "address": "0x…", "creator": "0x…", "name": "…", "symbol": "…", "metadata_hash": "0x…", "status": 1, "pair_weth": null, "pair_moto": null, "created_block": 123, "created_at": 1755990000, "hidden": 0, "pve": 0, "frozen_at": null, "announced": 0, "pve_credited_at": null, "fee_recipient": null, "frozen_block": null, "graduated_block": null, "pve_credited_block": null, "pair_moto_block": null, "unfrozen_block": null, "grad_mcap_usd": null, "prev_frozen_at": null, "prev_frozen_block": null, "prev_unfrozen_block": null, "trades24h": 4, "last_price_x18": "1350000000", "last_trade_ts": 1755999000, "holder_count": 12, "has_image": 1, "volume24h": "410000000000000000" } ], "pending": [ { "address": "0x…", "creator": "0x…", "submitter": "0x…", "fee_recipient": "0x…", "name": "…", "symbol": "…", "metadata_hash": "0x…", "nonce": "0", "deadline": 1756000000, "has_image": 0 } ], "ethUsd": 3421.55 } ``` * `status`: `0` None, `1` Trading, `2` Frozen, `3` Graduated * `volume24h`: exact decimal wei string. `last_price_x18`: decimal string * `has_image` is **advisory**. It is a cheap pattern probe that skips hash verification, so an occasional 404 from the image route is expected * `holder_count` is the true net-positive holder count, netted of PvE fills * `pending[]` is filtered against a live on-chain nonce so dead intents drop out, and includes intents up to one hour past their deadline so a card can show an expired end state * `ethUsd` here is never null; it falls back to a configured rate * Hidden coins are excluded throughout *** ### `GET /tokens/{address}` [#get-tokensaddress] One coin. Fires a live curve read on every request, cached 3s. **Pending branch** (an intent exists, no coin yet): ```json { "pending": true, "intent": { "…every intents column…", "metadata_json": "{…}" }, "ethUsd": 3421.55 } ``` **Materialized branch**: every column from `/tokens` above, plus: ```json { "metadata": { "description": "…", "image": "data:image/png;base64,…" }, "curve": { "realEth": "1200000000000000000", "tokenReserve": "800000000000000000000000000", "status": 1, "buysPaused": false, "paramsId": 0, "priceX18": "1350000000", "bondingProgress": "420000000000000000", "frozenAtOnChain": null }, "curveLive": true, "curveReadAt": 1756000000123, "recentTrades": [ { "tx_hash": "0x…", "log_index": 3, "token": "0x…", "trader": "0x…", "is_buy": 1, "eth_amount": "100000000000000000", "token_amount": "1000000000000000000000", "price_x18": "1350000000", "protocol_fee": "1000000000000000", "creator_fee": "500000000000000", "block_number": 8123400, "timestamp": 1755999000, "eth_amount_gwei": 100000000, "creator_fee_gwei": 500000, "token_amount_milli": 1000000, "fed": 0 } ], "stats": { "trades": 42, "volume_wei": "4100000000000000000", "creator_fees_wei": "20500000000000000", "volume24h_wei": "410000000000000000" }, "pveTokensWei": "0", "pveEthWei": "0", "pveLaunchTokensWei": "0", "pveLaunchEthWei": "0", "pveFedTokensWei": "0", "pveFedEthWei": "0", "devHoldsWei": "0", "firstBuyer": "0x…", "frozenAt": null, "sellsOpenAt": null, "pveRecordFailed": null, "creatorFeeRoutedWei": null, "ethUsd": 3421.55 } ``` * `metadata` is a parsed object or **`null`**. Null means the stored blob's hash disagrees with the on-chain commitment. It is never served unverified * `curve.priceX18` is served for every coin, graduated ones included, with no status gate: `status` sits beside it. For `status` `3` it is the graduation price, frozen, equal to the coin's price when its reserve reached the floor. The live price of a graduated coin is its pair's. `curve.paramsId` is the launch parameter set the coin was stamped with * `curve` is `null` when the chain read failed and no recent good state exists. `curveLive: false` marks both that case and a stale-but-served state. **A throttled RPC renders as `curveLive: false`, never as a `404`** * `recentTrades[]` selects whole rows. `fed` is a 0/1 flag marking a buy whose whole fill escrowed to the PvE vault and which is not the coin's first buy * `frozenAt` and `sellsOpenAt` are non-null **only while the coin is actually frozen**, and both come from the same chain read so they cannot disagree. `sellsOpenAt = frozenAt + SELL_REOPEN_DELAY` * `pveRecordFailed` is `null` or `{ "tokensWei": "…", "at": 1755999000 }` * `creatorFeeRoutedWei` is `null` (never happened) or a decimal wei string * `devHoldsWei` is always present as `"0"` rather than omitted Statuses: `200`, `404 { "error": "not found" }`. *** ### `GET /tokens/{address}/candles` [#get-tokensaddresscandles] | Param | In | Type | Default | Bounds | | ------------ | ----- | --------------- | ------------- | --------------------------------------------- | | `address` | path | address | - | lowercased, not regex-validated | | `resolution` | query | integer seconds | `300` | clamped to `[60, 86400]` | | `from` | query | unix seconds | `now - 86400` | raised to the window floor, then aligned down | Response is a **bare array**, up to 2001 rows: ```json [ { "bucket": 1755993600, "low": 1300000000, "high": 1400000000, "trades": 6, "volume": "410000000000000000", "open": "1310000000", "close": "1395000000" } ] ``` The exact rules, including that empty buckets produce no row and fill-forward is the client's job, are on [realtime and candles](/dev/fun/realtime#candles). In short: UTC epoch-aligned buckets, a 2000-slot window anchored to the coin's **last trade** rather than to wall clock, one seed row prepended, `low`/`high` as numbers and `open`/`close` as optional strings, volume as an exact wei string summing both sides, and `[]` with `200` for a coin that never traded. *** ### `GET /tokens/{address}/holders` [#get-tokensaddressholders] ```json { "holders": [ { "trader": "0x…", "net_wei": "1000000000000000000000" } ], "holderCount": 40, "pveVaultWei": "500000000000000000000" } ``` `holders` is the top 10 by net position. `holderCount` is the **true total**, not the page size. `pveVaultWei` is a decimal wei string or `null` when nothing was banked. Positions are netted of same-transaction PvE fills, so the escrow does not read as a whale. Always `200`. *** ### `GET /tokens/{address}/image` [#get-tokensaddressimage] The creator's picture as real bytes, so `og:image` works. `200`: raw image body. `Content-Type` is one of `image/png`, `image/jpeg`, `image/gif`, `image/webp`; plus `cache-control: public, max-age=300`, `x-content-type-options: nosniff`, and `content-security-policy: default-src 'none'`. **SVG is deliberately refused**, as is any non-data URL. Missing or unverified metadata gives `404 { "error": "no image" }`. ### `GET /tokens/{address}/card.png` [#get-tokensaddresscardpng] A 1200x630 share card. `address` **is** regex-validated here, `400 { "error": "bad address" }`. `200` is PNG bytes with `cache-control: public, max-age=30`. `404` when no coin exists. Images are re-checked against the on-chain hash before embedding; anything that fails degrades to a ticker-letters placeholder. ### `GET /share/{address}` [#get-shareaddress] An HTML unfurl page for crawlers, with a meta-refresh bounce to the app for humans. | Param | In | Notes | | --------- | ----- | ---------------------------------------------------------- | | `address` | path | On failure: **`text/plain` 400 `bad address`** | | `ref` | query | Optional referral, forwarded only if it is a valid address | `200` is `text/html` with `og:title`, `og:description`, `og:image`, `twitter:card`, and a refresh to the app at `/coin/
?chain=`. `404` is `text/plain`. *** ### GET /status [#get-status] The public health answer, for telling an outage from your own network. No parameters. `/health` is the operator document; this is the short one. ```json { "state": "ok", "checks": [ { "name": "…", "state": "ok", "detail": "…" } ], "notice": null } ``` `notice` is `null` or an object an operator has set, never a bare string: ```json { "notice": { "text": "…", "severity": "warning", "setAt": 1755990000 } } ``` `severity` is `"info"`, `"warning"` or `"critical"`, and `setAt` is unix seconds. Render `notice.text`; printing the object gives `[object Object]`. Treat `state` and each check's `state` as opaque labels to display, and do not build logic on the `checks` names, which are not a stable list. Spends the expensive-reads bucket. *** ### `GET /tokens/{address}/quote` [#get-tokensaddressquote] A lens quote served by the backend, so a client does not need its own RPC. `side` is `buy` or `sell`. `amount` is a decimal wei string: ETH in for a buy, coin in for a sell. ```json { "side": "buy", "tokensOut": "…", "grossUsed": "…", "refund": "…", "quotedAmount": "…" } ``` A sell answers `ethOut` in place of the three buy fields. `400` on a malformed address, side or amount. `502 { "error": "quote unavailable" }` when the chain read failed. `429` on either of its two budgets, with `Retry-After`. It is a convenience over `LaunchpadLens.quoteBuy` and `quoteSell`, which stay the canonical source and are what to use before sending a transaction. *** ### `GET /tokens/{address}/snapshot` [#get-tokensaddresssnapshot] The coin page in one body: `{ token, holders }`, plus `candles` and `quote` when their parameters are sent. `token` is exactly the `GET /tokens/{address}` body and `holders` exactly the `GET /tokens/{address}/holders` body, built by the same code behind the same caches, so the field lists for those routes apply unchanged. It spends one token on each bucket whose data it carries. Use it when you would otherwise make those requests together; keep the separate routes when you need one of them. *** ### GET /graduated/feed [#get-graduatedfeed] Graduated coins, newest graduation first. Built for listing sites. | Parameter | Type | Default | Notes | | --------- | ------- | ------- | ------------------------------------------------------------------------------------------- | | `limit` | integer | `100` | At least `1`, clamped to `200`. A non-number answers `400 { "error": "bad limit" }` | | `cursor` | string | none | The previous body's `nextCursor`. Signed; a bad one answers `400 { "error": "bad cursor" }` | ```json { "coins": [ { "chainId": 1, "token": "0x…", "name": "…", "symbol": "…", "creator": "0x…", "pair": "0x…", "pairMoto": "0x…", "graduatedAt": 1755990000, "graduatedBlock": 123, "logoUrl": "…", "url": "…" } ], "nextCursor": null } ``` `pair` is the TOKEN/WETH pool and `pairMoto` the TOKEN/MOTO pool, the same two addresses the `Graduated` event carries. Hidden coins are left out. `429` with `Retry-After` when rate limited. *** ### GET /trades/recent [#get-tradesrecent] The live ticker strip. No parameters, no rate limit. Always `200`. ```json { "trades": [ { "tx_hash": "0x…", "log_index": 3, "token": "0x…", "trader": "0x…", "is_buy": 1, "eth_amount": "100000000000000000", "timestamp": 1755999000, "symbol": "DOGE", "block_number": 8123400, "fed": 0 } ], "creations": [ { "token": "0x…", "creator": "0x…", "symbol": "DOGE", "timestamp": 1755990000, "block_number": 8123000 } ] } ``` 20 newest trades by `(block_number DESC, log_index DESC)`. 6 newest creations, **only from the last 24 hours**. Hidden coins excluded. *** ### GET /stats [#get-stats] No parameters. Cached 15s. Always `200`. ```json { "coins": 128, "graduated": 9, "volumeWei": "412000000000000000000", "creatorFeesWei": "2060000000000000000", "pveTokensWei": "0", "pveEthWei": "0", "pveUnvaluedCount": 0, "ethUsd": 3421.55 } ``` The three `*Wei` fields are exact decimal strings. **`ethUsd` is nullable here**: a null means no real price was available, never a fabricated one. ### GET /creators/top [#get-creatorstop] No parameters. Cached 20s. Always `200`. ```json { "rows": [ { "creator": "0x…", "earned_wei": "1200000000000000000", "trades": 84, "coins": 3, "top": { "address": "0x…", "name": "Doge", "symbol": "DOGE", "status": 3 } } ], "totalWei": "2060000000000000000", "ethUsd": 3421.55 } ``` Folded per creator, not per coin, capped at 25. `top` is that creator's highest-earning coin. `ethUsd` here is **not** nullable. ### GET /pve/value [#get-pvevalue] What the PvE escrow is currently worth on this chain. Memoised 45s and coalesced across callers. ```json { "chainId": 1, "ethWei": "3200000000000000000", "curvePriced": 12, "poolPriced": 3, "unpriced": 1, "asOf": 1756000000, "ethUsd": 3421.55 } ``` A graduated coin with a WETH pair is priced off pool reserves; everything else off its last indexed trade price. A coin that cannot be priced is counted in `unpriced` rather than silently zeroed. **`ethUsd` is nullable.** *** ### `GET /wallet/{address}/positions` [#get-walletaddresspositions] Read-rate-limited. `address` is regex-validated. ```json { "positions": [ { "token": "0x…", "symbol": "DOGE", "name": "Doge", "status": 1, "last_price_x18": "1350000000", "net_wei": "1000000000000000000000", "eth_in_wei": "100000000000000000", "bought_wei": "1200000000000000000000" } ] } ``` Uncapped, dust-filtered, ordered by position value descending. Cost basis is apportioned by the PvE net ratio. Hidden coins excluded. ### `GET /wallet/{address}/trades-today` [#get-walletaddresstrades-today] Read-rate-limited. The wallet's trades since the last 00:00 UTC Points drop. **Deliberately carries no point values and no per-trade deltas.** ```json { "count": 7, "dayStart": 1755993600, "trades": [ { "txHash": "0x…", "token": "0x…", "name": "Doge", "symbol": "DOGE", "isBuy": true, "fed": 0, "ethAmount": "100000000000000000", "timestamp": 1755999000 } ] } ``` `count` is the true day total; `trades` is capped at 50. `dayStart` is computed from the **server clock**, not chain time. ### `GET /wallet/{address}/fee-reroutes` [#get-walletaddressfee-reroutes] Not rate-limited. Every coin this creator launched whose creator fee was rerouted to the Collector, in one request. ```json { "reroutes": { "0xtoken…": "1500000000000000" } } ``` A map of coin address to cumulative rerouted wei. `{}` is the normal case. ### `GET /fees/history/{wallet}` [#get-feeshistorywallet] Read-rate-limited. Lifetime creator-fee **claims**. The `recipient` matched is whoever the vault actually paid, which is not necessarily the launch signer. ```json { "lifetime": { "0xquoteasset…": "4200000000000000000" }, "claims": [ { "txHash": "0x…", "token": "0x…", "name": "Doge", "symbol": "DOGE", "quoteAsset": "0x…", "amountWei": "1200000000000000000", "timestamp": 1755999000 } ] } ``` `lifetime` is keyed by quote asset, summed in BigInt rather than in SQL to keep wei precision. `claims` is capped at 200, newest first. **Carries no live accrued balances**: read those from the vault on chain. *** ### GET /intents/precheck [#get-intentsprecheck] A read-only advisory answered before the wallet signature prompt. It peeks the rate-limit window without consuming a slot. | Param | In | Required | Validation | | --------- | ----- | -------- | --------------------------------------------------------- | | `creator` | query | yes | address, else `400 { "error": "bad creator address" }` | | `symbol` | query | no | trimmed and lowercased, checked against the reserved list | ```json { "throttled": false, "reserved": false, "slotsFree": 3 } ``` `slotsFree` **fails open** to the full cap on an RPC failure. Statuses `200`, `400`. ### POST /intents [#post-intents] Creates a gasless launch intent. Signature-gated and IP-rate-limited at 5 per 10 minutes. The chain re-verifies at materialization, so the backend never holds launch authority. | Field | Type | Required | Default | Validation | | -------------- | -------------- | -------- | ------- | ------------------------------------------------- | | `creator` | address | yes | - | address regex | | `submitter` | address | no | zero | Zero means anyone may submit | | `feeRecipient` | address | no | zero | Refused if it is the curve or the PvE vault | | `name` | string | yes | - | 1 to 48 characters | | `symbol` | string | yes | - | `^[A-Za-z0-9]{1,16}$`, and not reserved | | `salt` | integer string | yes | - | must parse as a BigInt | | `nonce` | integer string | yes | - | must equal the on-chain `launchNonce`, else `409` | | `deadline` | integer string | yes | - | a safe integer, in the future, at most 7 days out | | `signature` | hex string | yes | - | EIP-712 recovery must equal `creator` | | `metadata` | any JSON | no | `{}` | serialized at most 20,000 characters | The EIP-712 type, field order load-bearing: ``` LaunchIntent(address creator, address submitter, string name, string symbol, bytes32 metadataHash, uint256 salt, uint256 nonce, uint256 deadline, address feeRecipient) ``` Domain: `{ name, version, chainId, verifyingContract }`, with `name` and `version` from the served `/config.eip712` and `verifyingContract` the served curve. `metadataHash` is `keccak256` of the exact serialized metadata JSON. `201`: ```json { "predictedAddress": "0x…", "metadataHash": "0x…" } ``` Other statuses: `400` on any validation failure, including `bad signature`. `409` on a nonce mismatch (the message names the value to use) or a salt that already launched a coin. `429` on the IP throttle or the per-creator live-intent cap. `503` when moto.fun is not live on this chain, or when the chain cannot be reached to read the nonce. Re-posting the same `(creator, salt)` upserts, preserving the original `created_at` and any moderation state. ### `GET /intents/{address}` [#get-intentsaddress] A pending launch's signed payload, verbatim, because it is what a submitter must replay on chain. ```json { "predicted_address": "0x…", "creator": "0x…", "submitter": "0x…", "fee_recipient": "0x…", "name": "…", "symbol": "…", "symbol_lower": "…", "metadata_hash": "0x…", "metadata_json": "{…}", "salt": "123", "nonce": "0", "deadline": 1756000000, "signature": "0x…", "created_at": 1755990000, "materialized": 0 } ``` Every field except `metadata_json` travels verbatim. `metadata_json` is a **string or null**, nulled when its hash does not match the on-chain commitment. Hidden intents `404`. ### `GET /tickers/{symbol}/available` - DEPRECATED [#get-tickerssymbolavailable---deprecated] Ticker exclusivity was removed from the contract. This always answers `{ "available": true }` for a syntactically valid symbol, and `{ "available": false, "reason": "1-16 letters or numbers" }` otherwise. Always `200`. Do not integrate against it. *** ### GET /stream [#get-stream] Server-Sent Events. Documented in full on [realtime](/dev/fun/realtime#the-stream). Summary: a firehose with no subscription protocol, payloads of exactly `{ type, token }`, event types `created`, `trade`, `frozen`, `unfrozen`, `graduated` (plus two retired ones), a `ping` every 25 seconds, the SSE connection caps described above (2000 global and 40 per IP by default), and **no replay**: no `id:`, no `Last-Event-ID`, no buffer. *** ### Not for integration [#not-for-integration] * The `/admin/*` group: moderation, an operator identity read, the audit log, metadata override read and write and delete, ban and unban, and the ban list. Nine routes. All operator-gated, all `no-store`, `401` without a credential. All nine share the same operator prologue, so all nine answer `503` when no operator token is configured, which means the whole group is off by default. Shapes are deliberately not documented here. Never document the credential, never integrate against any of it *** ## Indexing model, both services [#indexing-model-both-services] Both indexers are polling log indexers over the curve address. **Events consumed.** The app backend indexes `TokenCreated`, `Trade`, `FloorReached`, `Graduated`, `PveFill`, `PveRecorded`, `PveRecordFailed`, `FeeRecipientSet`, `CreatorFeeRoutedToCollector`, plus `Claimed` from the `CreatorFeeVault`. The Motoswap API indexes `TokenCreated`, `Trade`, `FloorReached`, `PveFill`, `Graduated`, and recognises and skips everything else on the curve. **Reorg discipline.** The app backend scans to `head - confirmations` (default 3), detects reorgs by comparing the stored canonical block hash rather than by height, rewinds `max(4 x confirmations, 64)` blocks inside one transaction, and purges every block-scoped table above the fork point before re-deriving. Over-rewinding is free because every insert is idempotent on `(tx_hash, log_index)`. Past a 200-block cumulative rewind it wedges deliberately and stops, reporting it on `/health`, until a human clears it. The Motoswap API keeps no derived accumulator for moto.fun: status folds on read and volume sums surviving rows, so a rollback is a range delete with nothing to rebuild. **Graduation market cap** is snapshotted once per graduation and never re-priced. A graduation reorged away clears it. **Poison logs** are caught per log, recorded, and skipped rather than wedging the cursor, which is why `/health` alerts on the recency of those rows rather than only on their count. # moto.fun Contracts - Full Inventory (/dev/reference/fun-contracts-full-inventory) Every signature on this page was read from the contract source, not paraphrased from a design document, and where the source and an older description of it disagree, the source wins. It describes what the deployed contracts do. The moto.fun contracts are `LaunchpadCurve` with its three linked libraries (`GraduationSeeder`, `MotoFloorMath`, `LaunchpadTokenDeployer`), `LaunchpadToken`, `LaunchpadLens` and `GasTank`. The canonical Motoswap contracts the curve depends on follow in Part 2: `CreatorFeeRegistry`, `CreatorFeeVault`, `PveVault`, `MirrorRegistry`, the three L1 outboxes, the two L2 receivers and the broadcast surface of `MotocatStakingV3`. The moto.fun repo carries its own copy of the creator-fee pair, identical to the canonical pair apart from import paths and a header comment, so one description serves both. Two rules that apply to the whole page: * **Resolve every address at runtime.** `GET /config` on the Motoswap API and `/config` on the moto.fun backend are the sources. See [addresses](/dev/fun/addresses). Nothing here is a value to hardcode. * **Some parameter values are deliberately withheld.** The MOTO reference window bounds, the buyback tolerance band and its bounds, and the blocked-quote cap are named but never printed on this site. Read them from the deployed contract. Every place they would appear says so. Each contract section follows the same shape: purpose, constructor, constants, state, every external and public function with its signature, meaning and access rule, every event with its fields, every custom error with the condition that raises it, and the invariants the contract keeps. Access is stated per function. Where a function carries no access note it is callable by anyone. *** ## Part 1: the moto.fun repo [#part-1-the-motofun-repo] ### LaunchpadCurve (`contracts/src/LaunchpadCurve.sol`) [#launchpadcurve-contractssrclaunchpadcurvesol] The singleton. Every coin's bonding curve lives in this one contract: launch, buy, sell, graduate, PvE escrow, fee routing, and the MOTO price checkpoint. Constant product against virtual reserves on both sides. Launching is an EIP-712 signature or a direct call; the coin materializes as an EIP-1167 clone inside the first transaction that names it. When a coin's reserve reaches its floor supply the curve freezes it and a permissionless, bounty-paid `graduate` buys MOTO and seeds two Motoswap pools with the LP burned. A UUPS proxy. Inherits `Initializable`, `Ownable2StepUpgradeable`, `UUPSRenounceable`, `ReentrancyGuardTransient` and `EIP712`, with the domain `("MotoswapLaunchpad", "1")`. Solidity `0.8.26`. `EIP712` is deliberately the non-upgradeable base: under `delegatecall` it rebuilds the domain separator against the proxy address, which is the address signatures must verify against. The implementation's constructor only disables initializers. Everything a deploy configures is set once by `initialize`, in the proxy's context, in the same transaction that deploys the proxy. Three linked libraries carry code that would not fit under the EIP-170 size limit otherwise: `GraduationSeeder` (the committed half of graduation), `MotoFloorMath` (the graduation split and the launch-parameter derivation) and `LaunchpadTokenDeployer` (deploys the token implementation). The curve runs them by `delegatecall`, so they act on the curve's balances and emit nothing of their own. Their errors are declared on the curve as well, so the curve's ABI decodes them. There is no `receive()` and no `fallback()`. ETH enters only through the payable functions. There is deliberately no withdraw and no sweep: curve ETH is never sweepable, by anyone. **Initializer** ```solidity struct Config { address owner; address registry; // CreatorFeeRegistry address vault; // CreatorFeeVault address weth; address moto; address motoFactory; address feeRouter; address uniFactory; // zero disables the venue bytes32 uniPairInitCodeHash; address sushiFactory; // zero disables the venue bytes32 sushiPairInitCodeHash; address collector; address pveVault; // zero disables PvE fills address upgradeAuthority; // the timelock. Not the owner. Zero is refused address[] blockedQuoteAssets; // extra quotes blocked pre-bond alongside WETH and MOTO } constructor() EIP712("MotoswapLaunchpad", "1") // disables initializers, nothing else function initialize(Config calldata c) external initializer ``` `initialize` refuses, in order: a zero `registry`, `vault`, `weth`, `moto`, `motoFactory`, `feeRouter` or `collector` (`ZeroAddress`); a codeless `motoFactory`, `feeRouter`, `weth` or `moto` (`NotAContract`); a venue with a factory but no init-code hash or the reverse (`VenueHalfConfigured`); a non-zero codeless `pveVault` (`NotAContract`); `moto == weth` (`ZeroAddress`); a codeless `registry` or `vault` (`NotAContract`); a zero `upgradeAuthority` (`ZeroAddress`); and a `blockedQuoteAssets` list above `MAX_BLOCKED_QUOTES` (`TooManyQuotes`). It emits `UpgradeAuthoritySet(0, authority)` and then deploys `LaunchpadToken` through `LaunchpadTokenDeployer`, from the proxy's context, so `tokenImplementation` is always the curve's own and every clone's `curve` is the proxy. The code-length checks matter more than they look. Solidity's extcodesize check before a high-level call runs in the caller's frame, outside any `try/catch`, so a codeless dependency would revert every graduation forever with no lever on the curve's side. Failing at deploy is the only cheap place. **Constants** * `SUPPLY = 1_000_000_000 ether` (1e27) - total supply per coin. Must equal `LaunchpadToken.LAUNCH_SUPPLY` * `FLOOR = 200_000_000 ether` (2e26) - **parameter set `0`**: the graduation floor of a coin whose `paramsId` is `0`, which is 80% sold. A coin's own floor is the second member of `paramsOf(token)` * `VIRTUAL_TOKENS = 66_666_667 ether` - **parameter set `0`**: virtual token reserve, the zero-burn condition `FLOOR^2 / (SUPPLY - 2 * FLOOR)` rounded up to a whole token. The sub-token remainder becomes graduation dust * `VIRTUAL_ETH = uint256(4 ether) / 3` - **parameter set `0`**: virtual ETH reserve. Under these three values the raise to graduation is 3x this, about 4 ETH, and the graduation market cap is 15x this, about 20 ETH. A coin's own is the first member of `paramsOf(token)`. Set `0` is what a fresh deployment launches coins under, since a launch is stamped with `nextParamsId` and that is zero until the first `setLaunchParams`. The source calls these three the legacy values because a later set can supersede them for new coins, not because they are unused; they keep their names so the ABI did not move. Building on them is correct only for coins stamped `0` * `BPS_DENOM = 10_000` * `MAX_PROTOCOL_FEE_BPS = 100` - cap on `protocolFeeBps` (1.00%) * `MAX_CREATOR_FEE_BPS = 50` - cap on `creatorFeeBps` (0.50%) * `MAX_BOUNTY = 0.08 ether` - cap on `graduationBounty`. A constant, not a gas-scaled formula: a bounty that read `tx.gasprice` or `block.basefee` would be a lever an attacker sets, carved from the coin's own raise * `SELL_REOPEN_DELAY = 15 minutes` - how long a coin sits `Frozen` before `sell` reopens. A dead-man's switch for a graduation that is broken upstream, not a wait anyone sees in ordinary operation. Not zero, so a 1-wei sell cannot knock a coin out of `Frozen` in the block before its graduation lands; not unbounded, so a competitor cannot hold every frozen coin hostage by keeping graduation broken * `MIN_TWAP_WINDOW: uint32` - the shortest window the MOTO reference is judged over. Below it the reference is too fresh to price against. Not published here. Read it from the contract * `MAX_TWAP_WINDOW: uint32` - the longest window the reference is judged over. Past it the average is too old to stand for the market. A fixed multiple of `MIN_TWAP_WINDOW`, and the multiple is load-bearing: the two-slot ring floats between one and two minimums in ordinary operation, so a ceiling at twice the minimum would be struck routinely. Not published here. Read it from the contract * `TAIL_ANCHOR_MAX: uint32` - how recently the MOTO/WETH pair must have written its own cumulative for a checkpoint to anchor on that write rather than on an extrapolation from current spot. Capped so the anchor can never be older than the shortest window the floor will measure over. Not published here * `MAX_TWAP_DEVIATION_CAP_BPS` / `MIN_TWAP_DEVIATION_BPS` - the ceiling and floor on `maxTwapDeviationBps`. There is no "off" setting: with an atomic buyback, zero would not defer the MOTO leg, it would stop every graduation. Deliberately not published here * `MAX_BLOCKED_QUOTES` - cap on `blockedQuoteAssets`. Every entry rides all three venues, so each costs three pair derivations and three cold stores per launch. Not published here. Read it from the contract * `LAUNCH_INTENT_TYPEHASH = keccak256("LaunchIntent(address creator,address submitter,string name,string symbol,bytes32 metadataHash,uint256 salt,uint256 nonce,uint256 deadline,address feeRecipient)")` **Types** ```solidity enum Status { None, Trading, Frozen, Graduated } /// One slot: 112 + 112 + 8 + 8 + 16 = 256 bits. struct Curve { uint112 realEth; uint112 tokenReserve; Status status; bool buysPaused; uint16 paramsId; } /// One launch parameter set. Written once when appended, never edited. struct LaunchParams { uint128 virtualEth; uint128 floorSupply; uint256 virtualTokens; } struct RaiseBand { uint128 minRaise; uint128 maxRaise; } struct LaunchIntent { address creator; address submitter; // zero means anyone may submit, at zero value only string name; string symbol; bytes32 metadataHash; uint256 salt; uint256 nonce; uint256 deadline; address feeRecipient; // zero means the creator } ``` `Status.Frozen` means the floor was reached and graduation is pending; all trading is paused in that window, and `graduate` is the only way out of it besides the `SELL_REOPEN_DELAY` timer. A `Frozen` curve always has `tokenReserve == FLOOR`, because the partial-fill clamp is the only way in. `LaunchIntent.submitter == address(0)` means anyone may materialize the intent, but only with zero value. A creator who intends to send their own first buy through an intent must set `submitter` to themselves; see `ValueNeedsSubmitter`. `feeRecipient` is covered by the signature, so it cannot be swapped in transit; the registry owner is the only lever afterwards. **State variables (public getters)** * `curves(address token) -> (uint112 realEth, uint112 tokenReserve, Status status, bool buysPaused, uint16 paramsId)` - five values. `paramsId` is stamped at launch from `nextParamsId` and never written again * `launchNonce(address creator) -> uint256` - the per-creator EIP-712 nonce. Consumed by `buyWithIntent` only; `launch` never reads it * `frozenAt(address token) -> uint256` - when the coin froze. Starts the sell-reopen clock * `pveAccrued(address token) -> uint256` - running PvE escrow. Every fill adds here; cleared only by a successful credit, so a failed one stays retryable * `pveVaultOf(address token) -> address` - the vault a coin's PvE fills were paid into, pinned at the first fill and never changed * `creatorOf(address token) -> address` - the wallet that signed the launch. The coin's identity and the PvE credit's beneficiary. Distinct from the fee recipient, which lives in the registry * `bountyOwed(address keeper) -> uint256` - graduation bounty that could not be pushed to its keeper * `motoPriceCumulativeLast: uint256` / `motoPriceTimestampLast: uint32` - the newer checkpoint slot * `motoPriceCumulativePrev: uint256` / `motoPriceTimestampPrev: uint32` - the older checkpoint slot, kept so the measured window never collapses on a refresh * `collector: address` - protocol fee destination * `maxTwapDeviationBps: uint256` - the buyback's tolerance band. How far below the reconstructed floor the graduation swap may fill, and equally how far above it seeded MOTO may run before the surplus goes to the Collector. Its value is deliberately not published here; read it from the contract * `blockedQuoteAssets(uint256 i) -> address` - the extra quote assets whose pairs are blocked pre-bond. Owner-maintained; a coin keeps whatever list existed at its own launch * `pveVault: address` - the PvE escrow. Zero disables fills. Only steers coins that have not escrowed yet * `pveKeeper: address` - the second address, besides the owner, allowed to call `recordPveLate`. Zero means owner-only * `protocolFeeBps: uint256` - ships at `70` * `creatorFeeBps: uint256` - ships at `50`, which is also its cap * `graduationBounty: uint256` - ships at `0.015 ether` * `launchesPaused: bool`, `allBuysPaused: bool` * `upgradeAuthority: address` - the only account that may upgrade the implementation. A timelock, separate from `owner`, on a storage slot of its own * `nextParamsId: uint16` - the id the next launch is stamped with, which is also the newest entry in the parameter table. Zero until the first `setLaunchParams`, and zero means parameter set `0`, the three constants above * `raiseBand() -> (uint128 minRaise, uint128 maxRaise)` - the band a new parameter set's implied raise must sit inside. Unset (`minRaise == 0`) until the owner sets it, and unset refuses every `setLaunchParams`. Its bounds are not published here **Set once by `initialize` (public getters)** These were immutables before the proxy and are storage now, because an immutable lives in the implementation's code and would carry the wrong value under `delegatecall`. None has a setter. * `tokenImplementation: LaunchpadToken` - deployed inside `initialize` * `registry: CreatorFeeRegistry`, `vault: CreatorFeeVault` * `WETH: IWETH`, `MOTO: address` * `motoFactory: IMotoSwapFactory`, `feeRouter: IFeeRouter` * `uniFactory: address` / `uniPairInitCodeHash: bytes32` * `sushiFactory: address` / `sushiPairInitCodeHash: bytes32` **External and public functions: the user path** * `launch(string name, string symbol, bytes32 metadataHash, uint256 salt, uint256 pveEth, address feeRecipient) payable nonReentrant returns (address token)` - anyone. The direct launch, with an optional atomic dev buy of `msg.value`. `pveEth` is the slice of that value donated to the PvE escrow: `0` is a plain dev buy, `msg.value` is an all-PvE launch, anything between is both at once from one curve fill at one price. Reverts `PveExceedsValue` if `pveEth > msg.value`, and `PveUnavailable` if `pveEth > 0` while `pveVault` is zero. The internal buy runs with `minTokensOut = 0`, which is safe here and only here: the curve was created in the same call and nobody else can be party to it. The split is measured against `grossUsed`, not `msg.value`, so a floor-hit partial fill fills the donation first and the creator's own slice absorbs the shortfall. PvE tokens go straight to the vault, never through the creator's balance * `buyWithIntent(LaunchIntent intent, bytes signature, uint256 minTokensOut) payable nonReentrant returns (address token)` - anyone, subject to the intent. The offline-deploy path. Checks in order: `IntentExpired` past `intent.deadline`; `NotTheSubmitter` if `intent.submitter` is set and is not `msg.sender`; `ValueNeedsSubmitter` if `intent.submitter` is zero and `msg.value > 0`; `BadNonce` if `intent.nonce != launchNonce[intent.creator]`; `BadIntentSignature` if ECDSA recovery of `launchIntentDigest(intent)` is not `intent.creator`. Then bumps the nonce, materializes the coin bound to the signer, and if value was sent runs an ordinary buy credited to `msg.sender`. Zero value materializes without buying; that is the relayer path and it stays open to anyone * `buy(address token, uint256 minTokensOut) payable nonReentrant returns (uint256 tokensOut)` - anyone. Ordinary buy; tokens land with `msg.sender` * `sell(address token, uint256 tokensIn, uint256 minEthOut) nonReentrant returns (uint256 ethOut)` - anyone. Pulls `tokensIn` with `safeTransferFrom` (the token's narrow curve exemption means no approval is needed while unbonded), pays the net ETH. Accepts a `Frozen` coin once `SELL_REOPEN_DELAY` has passed since `frozenAt`, and such a sell first flips the coin back to `Trading`, because it is no longer at the floor and no longer a graduation candidate * `buyPve(address token, uint256 minTokensOut) payable nonReentrant returns (uint256 tokensOut)` - anyone. Buy the curve at the going rate and donate every coin to the PvE escrow the coin is pinned to. The buyer pays normal fees, gets a `Trade` under their address, and keeps nothing. Only while the coin is `Trading`: a `Frozen` coin is before graduation but reverts `NotTrading`, like any other buy **External and public functions: the keeper path** * `graduate(address token) nonReentrant` - anyone. Permissionless, signerless, bounty-paid, atomic, and not pausable by anyone including the owner. Requires `Status.Frozen`, else `NotFrozen`. One transaction buys the coin's MOTO and opens both pools, or the whole thing reverts at a few staticcalls' worth of gas and the next block retries. The sequence is in the graduation section below * `pokePriceCheckpoint()` - anyone. Deliberately **not** `nonReentrant`, because every buy re-enters it through a gas-capped external self-call. Seeds or advances the MOTO/WETH price checkpoint. Reverts `NoMotoPool` if the pair cannot be read, and `CheckpointFresh` if the last roll is younger than `MIN_TWAP_WINDOW`. The rate limit is not conditioned on whether a usable reference exists, on purpose: rolling is what advances the older slot, so an unthrottled poke would pin the measured window one block wide forever * `recordPveLate(address token) nonReentrant` - **owner or `pveKeeper` only**, else `NotAuthorized`. Retries a PvE credit that failed at graduation. Requires `Status.Graduated` (`NotGraduated`) and a non-zero `pveAccrued` (`PveNothingToRecord`). Gated because the vault sizes the credit at the caller's block, so a permissionless caller could stake just in time and take a share the graduation-time stakers earned * `claimBounty(address to) nonReentrant returns (uint256 amount)` - anyone with a balance in `bountyOwed`. `to` is explicit because a credit exists precisely when the keeper could not receive ETH. Reverts `ZeroAddress` on a zero `to` and `PveNothingToRecord` on a zero balance **External and public functions: views** * `paramsOf(address token) view returns (uint256 virtualEth, uint256 floorSupply, uint256 virtualTokens)` - the curve parameters in force for one coin. **This is the read**, not the three set `0` constants. An address with no curve answers the set `0` triple rather than reverting, which is not what a fresh launch would get once a parameter set exists: a preview of an unlaunched coin must resolve `nextParamsId`, as `LaunchpadLens.quoteLaunch` does * `launchParams(uint16 id) view returns (uint128 virtualEth, uint128 floorSupply, uint256 virtualTokens)` - one entry of the parameter table. Id `0` is not an entry and reads as zeros, not as the set `0` constants. Use `paramsOf` * `twapRef() view returns (bool ok, uint256 refCum, uint32 elapsed)` and `twapRefAt(uint32 nowTs) view returns (bool ok, uint256 refCum, uint32 elapsed)` - which checkpoint slot the MOTO reference would be measured from, at the current block or at a given timestamp. `ok` describes the checkpoint ring alone. It is not a prediction that `graduate` will succeed; `LaunchpadLens.graduationReadiness` answers that * `upgradesRenounced() view returns (bool)` - inherited from `UUPSRenounceable` * `launchIntentDigest(LaunchIntent intent) view returns (bytes32)` - the exact EIP-712 digest a creator signs. Hashes `name` and `symbol` as `keccak256(bytes(...))` per the EIP-712 string rule. Public so the backend, tests and frontend derive it from one implementation * Every public state variable listed above * Inherited: `owner()`, `pendingOwner()`, `eip712Domain()`, `proxiableUUID()` **External functions: admin (all `onlyOwner`)** * `setFees(uint256 protocolBps, uint256 creatorBps)` - reverts `FeeAboveCap` above either cap. Emits `FeesSet` * `setGraduationBounty(uint256 bounty)` - reverts `BountyAboveCap` above `MAX_BOUNTY`. Emits `GraduationBountySet` * `setCollector(address collector_)` - zero reverts `ZeroAddress`. Emits `CollectorSet` * `setPveVault(address escrow)` - zero disables PvE fills for coins that have not escrowed yet. `address(this)` reverts `ZeroAddress` (every fill would be a no-op transfer that still credits `pveAccrued`, stranding the coins with no sweep). A non-zero codeless address reverts `NotAContract`. Emits `PveVaultSet`. Never moves a coin that already has `pveVaultOf` set * `setMaxTwapDeviationBps(uint256 bps)` - bounded in both directions by `MIN_TWAP_DEVIATION_BPS` and `MAX_TWAP_DEVIATION_CAP_BPS`, else `DeviationOutOfRange`. The only owner lever over the buyback. Emits `MaxTwapDeviationBpsSet` * `setRaiseBand(uint128 minRaise, uint128 maxRaise)` - a zero floor or a floor above the ceiling reverts `RaiseBandOutOfRange`. Gates what the next `setLaunchParams` may append and never touches a coin. Emits `RaiseBandSet` * `setLaunchParams(uint128 virtualEth, uint128 floorSupply) returns (uint16 id)` - appends a parameter set. Takes no id, so there is no path that edits an existing entry. Reverts `RaiseBandUnset` before a band exists, `FloorSupplyOutOfRange` for a `floorSupply` of zero or too close to half of `SUPPLY`, and `RaiseOutOfBand` when the implied raise falls outside `raiseBand`. `virtualTokens` is derived from `floorSupply` by the zero-burn relation and never supplied. Every coin launched after the call is priced by the new entry; every coin already launched keeps its own. Emits `LaunchParamsSet` * `setPveKeeper(address keeper)` - any address including zero. No code check, because it authorises a caller rather than being escrowed into. Emits `PveKeeperSet` * `setBlockedQuoteAssets(address[] quotes)` - replaces the list. Above `MAX_BLOCKED_QUOTES` reverts `TooManyQuotes`; a zero entry reverts `ZeroAddress`. Affects only coins launched after the call. Emits `BlockedQuoteAssetsSet` * `setLaunchesPaused(bool paused)`, `setAllBuysPaused(bool paused)`, `setTokenBuysPaused(address token, bool paused)` - the three pause levers. Sells are never pausable by the owner; they pause only in the `Frozen` window, and that window is bounded by `SELL_REOPEN_DELAY` * `renounceOwnership()` - overridden. Clears `launchesPaused` and `allBuysPaused` first, emitting the matching events, so a renounce cannot latch a global pause forever. Per-coin `buysPaused` is not cleared because the set is not enumerable * Inherited: `transferOwnership(address)` (owner only), `acceptOwnership()` (pending owner only) **External functions: upgrade authority only** All three revert `NotUpgradeAuthority` for any other caller, the owner included. * `upgradeToAndCall(address newImplementation, bytes data) payable` - inherited UUPS. Also reverts `UpgradesAreRenounced` once renounced * `setUpgradeAuthority(address next)` - zero reverts `ZeroAddress`. Gated on the current authority, not on `owner`, so a change of authority inherits the timelock's delay. There is no owner override: lose the authority and upgrades are gone. Emits `UpgradeAuthoritySet` * `renounceUpgradeability()` - one-way. Freezes the implementation for good while ownership and every operational setter keep working. Emits `UpgradesRenounced` **Events** ```solidity event TokenCreated(address indexed token, address indexed creator, string name, string symbol, bytes32 metadataHash, address indexed quoteToken); event FeeRecipientSet(address indexed token, address indexed recipient); event Trade( address indexed trader, address indexed token, bool isBuy, uint256 ethAmount, // curve-leg gross ETH: fee-inclusive on buys, pre-fee on sells uint256 tokenAmount, uint256 priceX18, // spot ETH per whole coin after the trade, 1e18 fixed point uint256 protocolFee, // the literal WETH transfer to the Collector uint256 creatorFee // the literal WETH deposit into the vault ); event FloorReached(address indexed token, uint256 realEth); event Graduated( address indexed token, address pairWeth, address pairMoto, uint256 wethLp, uint256 motoLp, uint256 tokensLpWeth, uint256 tokensLpMoto, uint256 dustBurned, address indexed keeper ); event FeesSet(uint256 protocolFeeBps, uint256 creatorFeeBps); event GraduationBountySet(uint256 bounty); event CollectorSet(address collector); event MaxTwapDeviationBpsSet(uint256 bps); event RaiseBandSet(uint128 minRaise, uint128 maxRaise); event LaunchParamsSet(uint16 indexed id, uint128 virtualEth, uint128 floorSupply, uint256 virtualTokens, uint256 impliedRaise); event PriceCheckpointed(uint256 cumulative, uint32 timestamp); event BountyOwed(address indexed keeper, uint256 amount); event BountyClaimed(address indexed keeper, uint256 amount); event CreatorFeeRoutedToCollector(address indexed token, uint256 amount); event CreatorFeeUnregistered(address indexed token, address indexed creator); event BlockedQuoteAssetsSet(address[] quotes); event LaunchesPausedSet(bool paused); event AllBuysPausedSet(bool paused); event TokenBuysPausedSet(address indexed token, bool paused); event PveVaultSet(address vault); event PveKeeperSet(address keeper); event PveFill(address indexed token, address indexed contributor, uint256 tokens); event PveRecorded(address indexed token, uint256 tokens); event PveRecordFailed(address indexed token, uint256 tokens); event UpgradeAuthoritySet(address indexed previous, address indexed next); // inherited event Upgraded(address indexed implementation); // ERC-1967, on every implementation change event UpgradesRenounced(); // UUPSRenounceable event Initialized(uint64 version); ``` When each fires: * `TokenCreated` fires at materialization, on every launch, whichever path * `FeeRecipientSet` fires **only when the recipient differs from the creator**. Its absence means the creator is the recipient. Additive, so every existing `TokenCreated` decoder keeps working * `Trade` fires on every buy (`isBuy = true`) and every sell. `ethAmount` is `grossUsed` on a buy and `grossOut` on a sell. `isBuy` is **not indexed**, so buys cannot be topic-filtered from sells. `trader` is always `msg.sender`, including on `buyPve` and on the launch fill * `FloorReached` fires inside the buy that clamps at `FLOOR` and freezes the coin * `Graduated` indexes `token` and `keeper` but **not the pair addresses**; pair discovery means decoding the data. `motoLp` and `tokensLpMoto` are the amounts actually seeded after price matching, which may be less than the leg's allotment * `PriceCheckpointed` fires on every successful roll, whether from a poke, a buy's self-call or a deploy script. `timestamp` is the anchor timestamp, which is the pair's own last write when that write was recent * `BountyOwed` fires when the bounty push failed; `BountyClaimed` when it was later pulled * `CreatorFeeRoutedToCollector` fires when the registry or vault could not be read, or the curve is no longer an authorised depositor, and the creator cut was non-zero. Operators should treat it as an outage. A merely disabled creator fee is silent, because that is a configured state * `CreatorFeeUnregistered` is declared and nothing on this implementation emits it. The branch it belonged to, a registry refusal with the coin's slot still empty, now reverts `RegistryRefused`. The declaration stays so that logs written before that change still decode against the deployed ABI * `LaunchParamsSet` fires when the owner appends a parameter set. `id` is what every launch after that block is stamped with. `impliedRaise` is emitted although it is derivable, because it is the number a reviewer of a timelocked call can check by eye, and `virtualTokens` because it is derived by the contract rather than supplied * `RaiseBandSet` fires with both halves of the band, since one without the other tells an indexer nothing * `UpgradeAuthoritySet` fires once in `initialize` with a zero `previous`, and on every `setUpgradeAuthority` * `PveFill` fires on every escrow fill. `contributor` is whoever paid the ETH: the creator on a launch slice, anyone on `buyPve` * `PveRecorded` / `PveRecordFailed` fire from the graduation-time credit and from `recordPveLate`. A failure keeps the accrual **Custom errors** * `ZeroAddress()` - constructor zero dependency, `moto == weth`, `setCollector(0)`, `setPveVault(address(this))`, a zero entry in `setBlockedQuoteAssets`, `claimBounty(address(0))` * `BadFeeRecipient()` - the launch names the curve, the registry, the vault, the PvE vault, the coin itself, WETH, MOTO or the Collector as fee recipient. Fees registered to a protocol contract could never be claimed * `LaunchesArePaused()` - any launch while `launchesPaused` * `BuysArePaused()` - a buy while `allBuysPaused` or the coin's `buysPaused` * `NotTrading()` - a buy on a non-`Trading` curve, or a sell on a curve that is neither `Trading` nor a `Frozen` coin past `SELL_REOPEN_DELAY` * `NotFrozen()` - `graduate` on a non-`Frozen` curve * `BadSymbol()` - symbol empty or over 16 bytes. `BadName()` - name empty or over 48 bytes * `BadIntentSignature()` - ECDSA recovery is not `intent.creator` * `NotTheSubmitter()` - `intent.submitter` is set and is not `msg.sender` * `ValueNeedsSubmitter()` - `buyWithIntent` called with value on an intent whose `submitter` is zero. An open intent's signature is a bearer token the backend publishes; funding it from the creator's own wallet would let a mempool copier take the whole dev buy at the launch price and leave the creator with a spent nonce and a taken address. The zero-value relayer path stays open * `BadNonce()` - `intent.nonce != launchNonce[creator]` * `IntentExpired()` - `block.timestamp > intent.deadline` * `ZeroAmount()` - a buy with no value, or a sell of zero * `Slippage()` - `tokensOut == 0`, the price-equivalent buy guard, or `ethOut < minEthOut` * `FeeAboveCap()`, `BountyAboveCap()`, `DeviationOutOfRange()` - the admin setter bounds * `NoMotoPool()` - `pokePriceCheckpoint` could not read the MOTO/WETH cumulative * `CheckpointFresh()` - `pokePriceCheckpoint` inside `MIN_TWAP_WINDOW` of the last roll. Costs a buy about 2k gas inside its self-call and nothing else * `NotAContract()` - a codeless dependency in the constructor, or a codeless `setPveVault` * `NotAuthorized()` - `recordPveLate` from neither owner nor `pveKeeper` * `VenueHalfConfigured()` - a venue factory set without its init-code hash, or the reverse * `TooManyQuotes()` - `blockedQuoteAssets` above `MAX_BLOCKED_QUOTES`, in the constructor or the setter * `EthTransferFailed()` - a buy refund, a sell payout or a bounty claim transfer failed * `PveUnavailable()` - a PvE fill while `pveVault` is zero, from either entry point * `PveExceedsValue()` - `launch` with `pveEth > msg.value` * `PveNothingToRecord()` - `recordPveLate` with zero accrual, **and also `claimBounty` with zero owed**. The name does not match the second condition; decode by selector * `NotGraduated()` - `recordPveLate` before graduation * `CurveInsolvent()` - a sell whose curve-implied gross output exceeds the real ETH on hand. A named invariant break rather than a `Panic(0x01)`, so monitoring can tell this invariant broke * `CreatorAlreadyClaimed()` - the registry already holds a **different** creator for this address. A launch that would land a stranger on the fee stream is a hijack and fails closed * `RegistryUnreadable()` - the registry could neither register nor be read back, so the launch's creator-fee claim cannot be verified either way * `RegistryRefused()` - the registry refused to register the coin and its creator slot is still empty. Reachable from one state only: the curve is not a registrar on `CreatorFeeRegistry`. Fixed by `registry.setRegistrar(curve, true)` from the registry owner, then re-sending the launch * `RaiseBandOutOfRange()` - `setRaiseBand` with a zero floor or a floor above the ceiling * `RaiseBandUnset()` - `setLaunchParams` before any `setRaiseBand` * `FloorSupplyOutOfRange()` - `setLaunchParams` with a `floorSupply` of zero, at or above half of `SUPPLY`, or so close to it that the derived virtual token reserve no longer fits. Raised inside `MotoFloorMath` * `RaiseOutOfBand()` - `setLaunchParams` whose implied raise falls outside `raiseBand`. Raised inside `MotoFloorMath` * `NotUpgradeAuthority()` - an upgrade, `setUpgradeAuthority` or `renounceUpgradeability` from any account other than `upgradeAuthority` * `UpgradesAreRenounced()` - an upgrade after `renounceUpgradeability`. Inherited from `UUPSRenounceable` * `MotoRefMissing()` - no checkpoint has ever been taken. Anyone may fix it with `pokePriceCheckpoint` * `MotoRefTooFresh(uint64 readyAt)` - a checkpoint exists but no slot has served `MIN_TWAP_WINDOW` yet. **Carries the maturity timestamp** of the soonest slot: sleep until `readyAt` rather than retrying blind * `MotoRefStale()` - both slots are past `MAX_TWAP_WINDOW`. A permissionless poke re-anchors, and one window later this clears * `MotoPoolUnreadable()` - the pool, the factory's live swap fee or the FeeRouter's protocol fee could not be read, or answered something no floor can be built from: a zero reserve, a reserve wider than `uint112`, a zero average, a fee at or above 100%, an overflow in the reconstruction, or a zero-sized pool leg. It also covers the post-match WETH leg check. Several unrelated conditions share this selector * `MotoOutBelowFloor(uint256 got, uint256 need)` - the MOTO buyback filled below its floor, measured on the received balance delta. The whole graduation reverts; nothing is spent, nothing is owed, the next block retries * `MotoLegTooSmall()` - the TOKEN/MOTO leg would be dust (`useMoto * useTokens <= 1e6`, or either side zero). Raised inside `GraduationSeeder.seed`. Unreachable at real parameters; it exists so the two-pool guarantee is enforced by a revert-and-retry rather than by a one-pool graduation The six `MotoRef*`, `MotoPool*`, `MotoOut*` and `MotoLeg*` errors are expected traffic, not alerts. Graduation is permissionless and retried every block, so a keeper decodes them and waits. **Access control** | Function | Who | Guard | | :---------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | :----------------------------------------------- | | `launch`, `buy`, `sell`, `buyPve` | anyone | `nonReentrant`, pause flags on launches and buys | | `buyWithIntent` | anyone for a zero-value open intent; `intent.submitter` when set; a funded call must name a submitter | `nonReentrant`, nonce, deadline, signature | | `graduate` | anyone | `nonReentrant`. Not pausable | | `pokePriceCheckpoint` | anyone | rate limit only. Not `nonReentrant` | | `claimBounty` | anyone with a `bountyOwed` balance | `nonReentrant` | | `recordPveLate` | owner or `pveKeeper` | `nonReentrant` | | `set*` except `setUpgradeAuthority`, `renounceOwnership`, `transferOwnership` | owner | `onlyOwner` | | `upgradeToAndCall`, `setUpgradeAuthority`, `renounceUpgradeability` | the upgrade authority (a timelock), never the owner | `NotUpgradeAuthority` | | `acceptOwnership` | pending owner | `Ownable2Step` | No roles and no `AccessControl`. `graduate` is explicitly outside the owner's reach, and so is every sell outside the bounded `Frozen` window. **Invariants** * Only the net, post-fee ETH enters `realEth`; the fee is wrapped and routed out in the same call. So `realEth` is always covered by the contract's own ETH balance, and a sell can never draw more than the curve-implied gross output. Buy rounding favours the curve, which is what makes `CurveInsolvent` unreachable in ordinary operation * A `Frozen` curve has `tokenReserve == FLOOR` exactly. A sell after `SELL_REOPEN_DELAY` takes it back to `Trading`, and a re-buy re-freezes it at exactly `FLOOR` through the same clamp, so `graduate` never sees any other reserve * A coin's PvE fills and its PvE credit always address the same vault (`pveVaultOf`), so a vault repoint can never strand a coin's escrow * `pveAccrued` is cleared only by a successful `recordPve`, never by a failure and never by the owner * The curve is never a fee recipient, never an LP holder, and never holds MOTO or WETH between transactions on the normal path: LP mints to `0xdead`, leftovers go to the Collector, the coin-side remainder burns * The graduation MOTO swap routes through the public `FeeRouter`, so the protocol fee applies and the volume is visible to Points and Rakeback. The curve never asks for a `swapAllowed` grant; the deployment's verification invariant is that only the router and the FeeRouter sit on the swap perimeter * Every cross-domain read on a path that must not revert (`_routeFees`, the price reference, the launch-time hijack check) goes through a guarded staticcall that treats short returndata as a failure, because `try/catch` does not cover the ABI decode. The PvE credit is a state-changing call, so it is wrapped in `try/catch` instead, behind an explicit code-length check on the vault for the same reason *** ### LaunchpadToken (`contracts/src/LaunchpadToken.sol`) [#launchpadtoken-contractssrclaunchpadtokensol] The ERC-20 template, deployed once by the curve's constructor and cloned per coin as an EIP-1167 minimal proxy. 1e27 fixed supply, 18 decimals, no tax, no mint, no owner, no permit. Hand-written rather than OpenZeppelin because the OZ base takes name and symbol in a constructor, which clones cannot use. The implementation bricks itself in its constructor by setting `initialized = true`, so only clones can ever be initialized. Two behaviours are active only while unbonded, and both are permanently cleared when the curve calls `setBonded` at graduation. After that the coin is indistinguishable from a vanilla ERC-20. **Constructor** * `constructor(address curve_)` - reverts `ZeroAddress` on a zero curve. Sets the `curve` immutable, which every clone reads through delegatecall semantics **Constants and immutables** * `LAUNCH_SUPPLY = 1_000_000_000 ether` * `curve: address` - the curve singleton, shared by every clone **State variables (public getters)** * `name: string`, `symbol: string`, `totalSupply: uint256` * `initialized: bool` - one-shot init flag. `true` on the implementation from birth * `bonded: bool` - set `true` exactly once, by the curve, at graduation * `balanceOf(address) -> uint256`, `allowance(address owner, address spender) -> uint256` * `blockedWhileUnbonded(address) -> bool` - the pre-bond transfer blocklist: the precomputed pair addresses **External and public functions** * `decimals() pure returns (uint8)` - returns `18` * `initialize(string name_, string symbol_, address[] blocked)` - **curve only** (`OnlyCurve`), one-shot (`AlreadyInitialized`). Sets name and symbol, writes every entry of `blocked` into the blocklist, mints `LAUNCH_SUPPLY` to the curve, emits `Transfer(address(0), curve, LAUNCH_SUPPLY)` * `setBonded()` - **curve only** (`OnlyCurve`). Sets `bonded = true`. Permanently lifts both unbonded behaviours. No way back, and no event of its own; the curve's `Graduated` is the signal * `approve(address spender, uint256 value) returns (bool)` - standard. Emits `Approval` * `transfer(address to, uint256 value) returns (bool)` - standard, through the blocklist check * `transferFrom(address from, address to, uint256 value) returns (bool)` - standard, with one narrow exemption: the allowance check is skipped only when `msg.sender == curve && !bonded && to == curve`. That is the gasless sell pull, and nothing else. An allowance of `type(uint256).max` is not decremented. Reverts `InsufficientAllowance` otherwise **Events** ```solidity event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); ``` **Custom errors** * `OnlyCurve()` - `initialize` or `setBonded` from anyone but the curve * `AlreadyInitialized()` - a second `initialize`, or any `initialize` on the implementation * `BlockedUntilBonded()` - a transfer to a blocklisted address while unbonded * `InsufficientBalance()`, `InsufficientAllowance()` * `ZeroAddress()` - a zero curve in the constructor, or a transfer to `address(0)` **Access control**: `initialize` and `setBonded` are curve-only. Everything else is the ERC-20 surface, callable by anyone. **Invariants** * `totalSupply` is `LAUNCH_SUPPLY` from `initialize` onward and never changes. There is no mint and no burn function; the graduation "burn" is a transfer to `0xdead` * The blocklist is written once, at `initialize`, and never edited. A coin keeps the list derived at its own launch even if the curve's `blockedQuoteAssets` changes later * The curve's allowance exemption can only ever move tokens **into** the curve. The old blanket exemption, which let the curve move any holder's tokens anywhere pre-bond, is gone **Known gap, stated in the source**: the blocklist enumerates pool addresses on three venues against a known quote set, but the set is not enumerable. A `TOKEN/` pair on Uniswap or Sushi, a V3 or V4 pool, or an unknown V2 fork sits outside it. Treat pre-bond pool liquidity as possible but unsupported. Closing the class needs a rule rather than a longer list, and every candidate rule costs some pre-bond transfer freedom. *** ### LaunchpadLens (`contracts/src/LaunchpadLens.sol`) [#launchpadlens-contractssrclaunchpadlenssol] The external signatures below are stable. Inside, each quote, price and progress read now resolves the coin's own parameter set from its `paramsId` instead of assuming set `0`, `quoteLaunch` resolves `nextParamsId` because the coin it previews does not exist yet, and the MOTO reference is read through the curve's `twapRef` and `twapRefAt` rather than recomputed here. `bondingProgress` is measured against the coin's own floor supply. Read-only companion to `LaunchpadCurve`. Every function is a `view` and nothing in the contract can move a wei. It exists because the curve compiles to just under the EIP-170 limit with these views removed; with them inlined the runtime bytecode was over the ceiling, which `forge test` never reports because Foundry disables the size limit for test deployments. Everything here mirrors the curve's own arithmetic line for line, and `test/Lens.t.sol` pins the two against each other. The constructor rejects a zero (`ZeroAddress`) or codeless (`NotAContract`) target: a lens pointed at an EOA would answer every query with zeros and look like a dead market rather than a misconfiguration. **Constructor** * `constructor(address curve_)` **State variables** * `curve: LaunchpadCurve` (immutable) - the only public state The curve constants (`SUPPLY`, `FLOOR`, `VIRTUAL_TOKENS`, `VIRTUAL_ETH`, `BPS_DENOM`) are mirrored as private constants and are not readable through the lens. Read them from the curve. **Types** ```solidity enum Readiness { Ready, TooFresh, Stale, Missing, OutOfBand, Upstream, NotFrozen } ``` The first six match the six regimes the graduation design names; `NotFrozen` is appended rather than inserted, so a consumer written against the six-value list decodes `0..5` identically. It exists so a keeper does not read a finished job as an outage. | Regime | Curve error it predicts | What to do | | :---------- | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ | | `Ready` | none | send `graduate` | | `TooFresh` | `MotoRefTooFresh` | wait until `readyAt`. A poke would make it worse | | `Stale` | `MotoRefStale` | poke, then wait one window. `readyAt` assumes the poke is sent now | | `Missing` | `MotoRefMissing` | poke, then wait one window | | `OutOfBand` | `MotoOutBelowFloor` | wait, and poll. `readyAt` is `0` because no timestamp can be promised: the pool moved or someone is standing on it, and both resolve on their own | | `Upstream` | `MotoPoolUnreadable` | the only regime that means something is broken | | `NotFrozen` | `NotFrozen` | not a candidate: `None`, `Trading` or already `Graduated` | **External functions (all `view`, callable by anyone)** * `quoteBuy(address token, uint256 ethIn) returns (uint256 tokensOut, uint256 grossUsed, uint256 refund)` - what `buy` with `msg.value == ethIn` would deliver, the ETH it would actually charge, and the refund if the buy crosses the floor. Returns zeros if the coin is not `Trading` or `ethIn == 0` * `quoteLaunch(uint256 ethIn, uint256 pveEth) returns (uint256 devTokens, uint256 pveTokens, uint256 grossUsed, uint256 refund)` - what a split `launch` actually delivers. Exact and token-free, because a launch always fills a fresh curve. **A create form must use this, not `quoteBuy`**: `quoteBuy` answers the whole-curve-fill question and overstates the creator's take by the PvE ratio. Returns zeros if `ethIn == 0` or `pveEth > ethIn`. Same clamp and rounding as the split in `launch`: the donation is measured against what is actually spent and floors toward the creator * `quoteSell(address token, uint256 tokensIn) returns (uint256 ethOut)` - net of fees. Returns `0` if not `Trading` or `tokensIn == 0` * `currentPrice(address token) returns (uint256)` - spot in ETH per whole coin, 1e18 fixed point. The same number `Trade.priceX18` carries * **It does not check the coin's status.** Graduation zeroes `realEth` and `tokenReserve`, so for a `Graduated` coin this returns `virtualEth * 1e18 / virtualTokens`. That is the coin's graduation price, frozen: the zero-burn relation that fixes the virtual token reserve is exactly the condition under which the price at the floor equals `virtualEth / virtualTokens`, for any parameter set, up to rounding. It is correct for what it is and stale against the market from the first pool trade on, with no revert and no zero to warn you. For an address the curve never launched (`Status.None`) the same read answers the set `0` ratio, a non-zero price for something that is not a coin. `quoteBuy` and `quoteSell` return zeros outside `Trading`, and `bondingProgress` pins at `1e18`, but `currentPrice` keeps answering. Read `curves(token).status` first, and take a graduated coin's live price from its pools * `bondingProgress(address token) returns (uint256)` - `0` to `1e18`. `None` is `0`; anything past `Trading` is `1e18` * `predictToken(address creator, uint256 salt) returns (address)` - the CREATE2 address `launch` will deploy for this creator and salt: `Clones.predictDeterministicAddress(curve.tokenImplementation(), keccak256(abi.encode(creator, salt)), address(curve))`. Must stay identical to the curve's own derivation, and `Lens.t.sol` asserts it against a real launch * `graduationReadiness(address token) returns (Readiness regime, uint64 readyAt, uint256 minMotoOut, uint256 quotedOut, uint256 deviationBps, uint256 bountyNow, uint256 sellsOpenAt)` - everything a keeper needs to decide whether to send `graduate`, and when to try again if not. **Never reverts**: every cross-domain read is guarded and every failure classified. `readyAt` is the earliest timestamp the coin could become `Ready`: now for `Ready`, the soonest slot's maturity for `TooFresh`, one window from now for `Stale` and `Missing`, and `0` for `OutOfBand`, `Upstream` and `NotFrozen`, where no timestamp can be promised. `minMotoOut` is the floor the swap must clear; `quotedOut` is what the swap would return at the pool's current reserves, and `quotedOut < minMotoOut` is exactly `OutOfBand`. `deviationBps` is how far `quotedOut` sits from the honest expectation the floor was built from, unsigned. All three are zero unless the regime is `Ready` or `OutOfBand`. `bountyNow` is what `graduate` would pay after the `pot / 50` clamp, and `sellsOpenAt` is `frozenAt + SELL_REOPEN_DELAY`; both are filled for every `Frozen` coin regardless of regime. Mirrors the curve's tail preference too: when the pair's own last write is recent, the window ends there rather than at an extrapolated tail * `motoPoolDeviationBps() returns (uint256 deviationBps, bool usable)` - **retired**. Marked stale in the source and kept only so the indexer, keeper and frontend can migrate behind a dual-ABI window. It still measures spot against the average and `usable` still reports whether a reference exists inside the window bounds, but a `true` here no longer implies the buyback will clear its floor, and the deviation itself no longer gates anything. Do not build on it. Use `graduationReadiness` **Events**: none. **Custom errors**: `ZeroAddress()` and `NotAContract()`, both constructor-only. **Access control**: none needed. Every function is a view. **Invariants** * The lens holds no state beyond the `curve` pointer and can move no value * Every quote reads `protocolFeeBps` and `creatorFeeBps` from the curve at call time, so a fee change is reflected immediately. A local reimplementation with the rates baked in is a latent mispricing the day they move * `graduationReadiness` classifies rather than inherits a failure: short returndata from the factory, the pair, the FeeRouter or the curve reads as `Upstream`, never as a revert *** ### GasTank (`contracts/src/GasTank.sol`) [#gastank-contractssrcgastanksol] Holds ETH for the moto.fun keeper and the other gas keys on one chain and tops them up on demand, so nobody has to send gas by hand. One deployment per chain, shared by every payee on it. An operations contract: no user ever calls it, and nothing in the trading path depends on it. The design is one trade. `refill` is permissionless and every other lever is `onlyOwner`. The destination set is fixed by the owner, the amount is fixed by the contract (floor, target, per-call cap, per-window cap), and the precondition is that the payee is already below its floor. The only thing a caller chooses is when a warranted refill happens, so there is nothing for a privileged caller to protect. There is no refiller key to steal, and if every service is down a human can still push the button from a block explorer. The automated refiller is a liveness guarantee, not a permission. Inherits `Ownable2Step`, `Pausable`, `ReentrancyGuardTransient`. It has one owner, and only the owner can change its settings or withdraw. **Constructor** * `constructor(address initialOwner, uint256 lowWatermark, uint32 initialSendGas)` - `initialSendGas` must sit inside `[MIN_SEND_GAS, MAX_SEND_GAS]`, else `BadSendGasLimit`. Emits `LowWatermarkSet(0, lowWatermark)` and `SendGasLimitSet(0, initialSendGas)`. Deploy scripts set themselves as owner, write the payee rows, then hand over through the two-step transfer **Constants** * `WINDOW = 1 days` (`uint64`) - the spend window for `perDayCapWei`. **Tumbling, not rolling, and not a UTC calendar day.** A payee's window opens on its first refill and shuts exactly `WINDOW` seconds later. That bounds spend at `perDayCapWei` per window but at twice `perDayCapWei` across an arbitrary 24 hours: open a window with one small refill, drain the remainder one second before it shuts, drain a fresh allowance one second later. The honest fix is free: set `perDayCapWei` to half the 24-hour number you mean, which is what the deploy script does * `MIN_SEND_GAS = 2_300` (`uint32`) - floor on `sendGasLimit`, the classic transfer stipend * `MAX_SEND_GAS = 100_000` (`uint32`) - ceiling on `sendGasLimit`, so a contract payee cannot do real work inside the tank's call frame **Types** ```solidity struct Payee { uint128 floorWei; // refill only when payee.balance is strictly below this uint128 targetWei; // refill aims at this balance; must exceed floorWei uint128 perCallCapWei; // most one refill can send uint128 perDayCapWei; // most this payee can receive inside one WINDOW uint128 spentInWindow; // sent since windowStart; can sit above perDayCapWei after the owner lowers the cap mid-window uint64 windowStart; // unix time the current window opened; 0 = never refilled bool enabled; // owner switch bool registered; // set once by setPayee; distinguishes "not a payee" from "paused payee" } ``` `targetWei` is the on-chain ceiling on hot exposure: no refill can take a payee above it, so it also bounds what a revoke-first key rotation strands. **State variables (public getters)** * `payees(address) -> Payee` - the allowlist. Only the owner writes it * `lowWatermarkWei: uint256` - tank balance below which `TankLow` is emitted after every outflow. An alarm backstop, not a trigger. Zero disables the event * `sendGasLimit: uint32` - gas forwarded with a refill send. Payees are plain accounts, which need none of it **External and public functions** * `receive() payable` - anyone. Funds the tank and emits `Funded`. Nobody needs a key to add runway * `refill(address payee) nonReentrant whenNotPaused returns (uint256 amount)` - anyone. Checks in order: `UnknownPayee` if not registered, `PayeeDisabled` if disabled, `AboveFloor` if `payee.balance >= floorWei`. Opens a fresh window if `windowStart == 0` or the current one has shut. `DailyCapReached` if nothing remains in the window (saturating, so an owner lowering the cap below what is already spent closes the window rather than panicking). Then `amount = targetWei - balance`, capped by `perCallCapWei`, by the window remainder, and by the tank's own balance; `TankEmpty` if that leaves zero. Writes `windowStart` and `spentInWindow` **before** the send, sends with `gas: sendGasLimit`, reverts `RefillFailed` if the send failed. Emits `Refilled`, then `TankLow` if the balance fell under the watermark. A partial refill is deliberate: when the tank holds less than the shortfall it sends what it has, because some gas beats none * `quoteRefill(address payee) view returns (uint256 amount)` - anyone. The read-only twin of `refill`, for the cron and for `/health`. Returns zero instead of reverting in every case `refill` would revert, including while paused, so a caller never has to decode errors or reimplement the arithmetic * `setPayee(address payee, uint128 floorWei, uint128 targetWei, uint128 perCallCapWei, uint128 perDayCapWei, bool enabled)` - **owner only**. Registers or reconfigures a payee. Reverts `ZeroAddress` on a zero payee and `InvalidConfig` if `floorWei == 0`, `targetWei <= floorWei`, `perCallCapWei == 0` or `perDayCapWei < perCallCapWei`. Sets `registered = true`. **Never resets `windowStart` or `spentInWindow`**: if it did, an owner (or whoever took the owner key) could hand out unlimited daily allowances by rewriting the row between refills. Emits `PayeeSet` * `setPayeeEnabled(address payee, bool enabled)` - **owner only**. Turns a registered payee off or on without losing its sizing. `UnknownPayee` if not registered. Emits `PayeeSet` with the stored sizing. Killing one payee is one transaction, so a suspected key leak needs neither a full reconfiguration nor a pause of the whole tank * `setLowWatermark(uint256 newWatermarkWei)` - **owner only**. Emits `LowWatermarkSet(previous, new)`. Zero disables `TankLow` * `setSendGasLimit(uint32 newLimit)` - **owner only**. Must sit inside `[MIN_SEND_GAS, MAX_SEND_GAS]`, else `BadSendGasLimit`. Emits `SendGasLimitSet(previous, new)` * `pause()` / `unpause()` - **owner only**. `pause` stops every refill in one transaction. `withdraw` stays open while paused on purpose, because pausing is what you do on the way to emptying the tank. Inherited `paused() view returns (bool)` * `withdraw(address to, uint256 amount)` - **owner only**, paused or not. `ZeroAddress` on a zero `to`, `ZeroAmount` on zero, `InsufficientTankBalance(requested, available)` above the balance. Forwards full gas, because the destination is the owner's chosen account, which may be a contract, rather than a keeper account. Reverts `WithdrawFailed` if the send failed. Emits `Withdrawn`, then `TankLow` if applicable. Automation is never a one-way door * `renounceOwnership() pure` - **disabled**. Always reverts `RenounceDisabled`. `Ownable2Step` makes transfer two-step but leaves renounce as a single unguarded owner call, and one mistaken owner call would zero the owner while the tank still holds a runway and `refill` still pays out. Hand it to a new owner with `transferOwnership` plus `acceptOwnership` * Inherited: `owner()`, `pendingOwner()`, `transferOwnership(address)` (owner only), `acceptOwnership()` (pending owner only) **Events** ```solidity event PayeeSet(address indexed payee, uint128 floorWei, uint128 targetWei, uint128 perCallCapWei, uint128 perDayCapWei, bool enabled); event Refilled(address indexed payee, address indexed caller, uint256 amount, uint256 payeeBalance, uint256 tankBalance); event Funded(address indexed from, uint256 amount, uint256 tankBalance); event Withdrawn(address indexed to, uint256 amount, uint256 tankBalance); event TankLow(uint256 tankBalance, uint256 lowWatermarkWei); event LowWatermarkSet(uint256 previousWei, uint256 newWei); event SendGasLimitSet(uint32 previous, uint32 current); ``` Inherited from `Pausable`: `Paused(address account)`, `Unpaused(address account)`. From `Ownable2Step`: `OwnershipTransferStarted`, `OwnershipTransferred`. **Custom errors** * `ZeroAddress()` - `setPayee` or `withdraw` with a zero address * `ZeroAmount()` - `withdraw(_, 0)` * `InvalidConfig()` - a `setPayee` row that breaks `0 < floorWei < targetWei`, `perCallCapWei > 0` or `perDayCapWei >= perCallCapWei` * `UnknownPayee(address payee)` - `refill` or `setPayeeEnabled` on an address never registered * `PayeeDisabled(address payee)` - `refill` on a registered but disabled payee * `AboveFloor(address payee, uint256 balance, uint256 floorWei)` - `refill` when the payee does not need one yet. The normal steady state * `DailyCapReached(address payee, uint256 perDayCapWei)` - the payee has taken its whole per-window allowance * `TankEmpty()` - the computed refill amount is zero because the tank holds nothing * `RefillFailed(address payee, uint256 amount)` - the refill send reverted. Most likely a payee with code that cannot receive inside `sendGasLimit` * `WithdrawFailed(address to, uint256 amount)` - the withdraw send reverted * `InsufficientTankBalance(uint256 requested, uint256 available)` - `withdraw` above the balance * `BadSendGasLimit(uint32 sendGasLimit)` - constructor or `setSendGasLimit` outside the bounds * `RenounceDisabled()` - any `renounceOwnership` * Inherited from `Pausable`: `EnforcedPause()` on `refill` while paused, `ExpectedPause()` on `unpause` while not paused **Access control** | Function | Who | | :----------------------------------------------------------------------------------------------------------------------- | :--------------------- | | `receive`, `refill`, `quoteRefill` | anyone | | `setPayee`, `setPayeeEnabled`, `setLowWatermark`, `setSendGasLimit`, `pause`, `unpause`, `withdraw`, `transferOwnership` | owner | | `acceptOwnership` | pending owner | | `renounceOwnership` | nobody; always reverts | **Invariants** * Only a registered, enabled payee can ever receive ETH through `refill`, and only when its balance is strictly below its floor * One refill never takes a payee above `targetWei`, never sends more than `perCallCapWei`, and never sends more than remains in the window. `spentInWindow` is not bounded by `perDayCapWei` from above: `setPayee` rewrites the cap and never touches the spend, so lowering the cap below what is already spent leaves the spend above it, the remainder saturates to zero, and `refill` reverts `DailyCapReached` until the window rolls * Window accounting is written before the send, so a reentrant caller sees the spend already booked; `nonReentrant` and the send-gas cap close the same door twice * A row rewrite never resets the window, so the per-window cap cannot be reissued by the owner * The owner can always withdraw, paused or not, and can never be removed by renounce * Guarantee, stated plainly: set `perDayCapWei` to X and a payee receives at most X per window and at most 2X in any 24 hours Sizing (floors, targets, caps, watermark) lives in `script/DeployGasTank.s.sol`, not in the contract, because it is a per-chain, per-gas-price judgement that changes. The deploy section below has the shape. *** ### CreatorFeeRegistry (`contracts/src/CreatorFeeRegistry.sol`, a copy of the canonical Motoswap contract) [#creatorfeeregistry-contractssrccreatorfeeregistrysol-a-copy-of-the-canonical-motoswap-contract] Which coins have a creator fee and who receives it. Populated only by authorised registrar contracts, never by end users. Shared with the DEX: the FeeRouter reads `activeCreator` on every swap that touches a registered token, and the curve is a registrar on it. Plain `Ownable2Step`, not a proxy; it holds no funds and its shape is fixed. Implements `ICreatorFeeRegistry`. In production the curve points at the canonical deployment, not at the vendored copy. The copy exists so the moto-fun repo builds and tests standalone. **Constructor** * `constructor(address initialOwner)` **Types** ```solidity struct TokenConfig { address creator; bool disabled; } // creator == 0 means not registered ``` **State variables (public getters)** * `tokens(address token) -> (address creator, bool disabled)` * `isRegistrar(address) -> bool` - who may call `register` **External and public functions** * `register(address token, address creator)` - **registrar only** (`NotRegistrar`). Sets the initial creator. Reverts `ZeroAddress` on a zero token or creator and `AlreadyRegistered` if the slot is taken, whoever holds it. Emits `TokenRegistered` * `setCreator(address token, address creator)` - **the current creator only** (`NotCreator`), gated on `creatorOf`, not `activeCreator`. Redirects the token's fee stream without going through an admin. Reverts `ZeroAddress` on a zero creator and `NotRegistered` on an unregistered token. One step, not an accept-handshake, because a two-step would strand the balance of anyone who set a recipient that cannot call back. Moves the unclaimed balance with the address, because the vault keys accrual per token. Emits both `CreatorSet` and `CreatorSetBySelf` * `adminSetCreator(address token, address creator)` - **owner only**. Hard-sets the creator for a takeover, lost keys or a redirection. Also captures the unclaimed balance, which is intended. Reverts `ZeroAddress`, `NotRegistered`. Emits `CreatorSet` only * `setDisabled(address token, bool disabled)` - **owner only**. Stops **future** accrual: `activeCreator` returns zero so the FeeRouter and the curve skim nothing further. Does not touch money already earned, because the vault gates claims on `creatorOf`. Reverts `NotRegistered`. Emits `TokenDisabled` * `setRegistrar(address registrar, bool allowed)` - **owner only**. Reverts `ZeroAddress`. Emits `RegistrarSet` * `activeCreator(address token) view returns (address)` - the swap hot-path read. The creator if registered and not disabled, else zero. One SLOAD * `creatorOf(address token) view returns (address)` - the creator regardless of the disabled flag, or zero if never registered. The claim-side read * Inherited: `owner()`, `pendingOwner()`, `transferOwnership(address)` (owner only), `acceptOwnership()` (pending owner only), `renounceOwnership()` (owner only, single step) The two reads must not be conflated. `activeCreator` answers "should the router skim?"; `creatorOf` answers "whose money is it?". Those stopped being the same question the moment a disabled token still had a balance in the vault. **Events** ```solidity event TokenRegistered(address indexed token, address indexed creator, address indexed registrar); event CreatorSet(address indexed token, address indexed creator); event CreatorSetBySelf(address indexed token, address indexed previousCreator, address indexed creator); event TokenDisabled(address indexed token, bool disabled); event RegistrarSet(address indexed registrar, bool allowed); ``` `CreatorSet` fires on both the admin path and the self-redirect path, so anything already watching it keeps working. `CreatorSetBySelf` fires only on the self-redirect path, so an indexer can tell a takeover from a routine change. **Custom errors** * `ZeroAddress()` - `register`, `setCreator`, `adminSetCreator` or `setRegistrar` received zero * `NotRegistrar()` - `register` from an address not in `isRegistrar` * `AlreadyRegistered()` - `register` on a token that already has a creator * `NotRegistered()` - `setCreator`, `adminSetCreator` or `setDisabled` on an unregistered token * `NotCreator()` - `setCreator` from anyone but the current creator **Access control** | Function | Who | | :----------------------------------------------------------------------------------------- | :------------------------------- | | `register` | a registrar (the moto.fun curve) | | `setCreator` | the token's current creator | | `adminSetCreator`, `setDisabled`, `setRegistrar`, `transferOwnership`, `renounceOwnership` | owner | | `acceptOwnership` | pending owner | | `activeCreator`, `creatorOf`, `tokens`, `isRegistrar` | anyone (views) | **Invariants** * A token's creator, once set, is never zero again. There is no unregister * `activeCreator(t) != 0` implies `creatorOf(t) == activeCreator(t)`. Disabling changes the first and never the second * Registration is gated on the caller, not on who deployed the token. The curve therefore checks the outcome of its own `register` call rather than trusting a bare `try/catch`; see `CreatorAlreadyClaimed` and `RegistryUnreadable` on the curve *** ### CreatorFeeVault (`contracts/src/CreatorFeeVault.sol`, vendored from evm-moto-contracts; identical to `src/fees/CreatorFeeVault.sol`) [#creatorfeevault-contractssrccreatorfeevaultsol-vendored-from-evm-moto-contracts-identical-to-srcfeescreatorfeevaultsol] Claim-based escrow for creator fees, per token, in the quote asset they were collected in. Creators pull; nothing is ever pushed. Passive: plain ERC-20 transfers of vetted quote assets in, no hooks, so the skim can never introduce a new revert path beyond the token transfer itself. Shared with the DEX. `Ownable2Step` plus the classic `ReentrancyGuard`. Implements `ICreatorFeeVault`. **Constructor** * `constructor(address initialOwner, address registry_, address weth_)` - reverts `ZeroAddress` on a zero registry or WETH. Both become immutables **State variables and immutables (public getters)** * `registry: ICreatorFeeRegistry` (immutable) - where `creatorOf` is read * `WETH: address` (immutable) - the one asset it will unwrap * `accrued(address token, address quoteAsset) -> uint256` - claimable by the token's **current** creator * `isDepositor(address) -> bool` - who may call `notifyDeposit`. The FeeRouter and the curve **External and public functions** * `setDepositor(address depositor, bool allowed)` - **owner only**. Reverts `ZeroAddress`. Emits `DepositorSet`. **Revoking the live depositor is a trading kill switch, not a fee switch**: the FeeRouter calls `notifyDeposit` unconditionally on every swap that touches a registered token, and the curve calls it on every trade with a non-zero creator cut, so a vault that does not admit its depositor makes every registered token untradeable through that depositor. The rule is `setCreatorFeeConfig(registry, vault, 0)` first on the FeeRouter, then `setDepositor(feeRouter, false)`. Wrapping the call in `try/catch` would be worse, because the quote asset is transferred in before the entitlement is recorded, so a swallowed revert strands it uncredited * `notifyDeposit(address token, address quoteAsset, uint256 amount)` - **depositor only** (`NotDepositor`). Records `amount` as accrued. Only records; the caller must already have transferred the asset in, which is what makes the solvency invariant hold by construction. Emits `Accrued` * `claim(address token, address[] quoteAssets, bool unwrapWeth) nonReentrant` - **the token's current creator only**, read as `registry.creatorOf(token)` (`NotCreator`). Claims across the listed assets to the caller. Zero balances are skipped rather than reverting. WETH is unwrapped to ETH when `unwrapWeth` is set, else transferred as WETH. Effects before interaction: `accrued` is zeroed before each transfer. Reverts `EthTransferFailed` if the ETH send failed. Emits `Claimed` per non-zero asset * `claimMany(address[] tokens, address[] quoteAssets, bool unwrapWeth) nonReentrant` - **the current creator of every listed token**. The same body as `claim`, once per token, with one quote-asset list for all. Reverts `NothingToClaim` on an empty token list. **A third party's token anywhere in the list reverts the whole batch** with `NotCreator` rather than being skipped: the app builds this list from tokens it believes the caller owns, so a mismatch means a takeover or a self-redirect happened underneath it, and that is the event a creator most needs to see. Duplicates are harmless; the second pass reads a zeroed balance * `receive() payable` - accepts ETH only from `WETH` during an unwrapping claim. Anything else reverts `EthTransferFailed` * Inherited: `owner()`, `pendingOwner()`, `transferOwnership(address)` (owner only), `acceptOwnership()` (pending owner only), `renounceOwnership()` (owner only, single step) **Events** ```solidity event Accrued(address indexed token, address indexed quoteAsset, uint256 amount); event Claimed(address indexed token, address indexed creator, address indexed quoteAsset, uint256 amount); event DepositorSet(address indexed depositor, bool allowed); ``` **Custom errors** * `ZeroAddress()` - constructor or `setDepositor` received zero * `NotDepositor()` - `notifyDeposit` from an address not in `isDepositor` * `NotCreator()` - `claim` or `claimMany` from anyone but the token's current creator * `NothingToClaim()` - `claimMany` with an empty token list * `EthTransferFailed()` - the ETH send in an unwrapping claim failed, or `receive` from anyone but WETH **Access control** | Function | Who | | :------------------------------------------------------- | :------------------------------------------ | | `notifyDeposit` | a depositor (the FeeRouter, the curve) | | `claim`, `claimMany` | the token's current creator per `creatorOf` | | `setDepositor`, `transferOwnership`, `renounceOwnership` | owner | | `acceptOwnership` | pending owner | | `receive` | WETH only | **Invariants** * Solvency: the vault's balance of each quote asset always equals the sum of `accrued[*][quoteAsset]`. It holds nothing else, and there is no rescue or sweep * A creator takeover or self-redirect captures the unclaimed balance, because accrual is per token rather than per recipient. Intended * The only ETH the vault ever holds is in flight from `WETH.withdraw` to the claimer inside one `claim` *** ### Interfaces (`contracts/src/interfaces/`) [#interfaces-contractssrcinterfaces] **`ICreatorFeeRegistry`** - `activeCreator(address token) view returns (address)`, `creatorOf(address token) view returns (address)`, `register(address token, address creator)`. The curve's `_routeFees` reads `activeCreator`; its `_registryCreator` reads the `tokens(address)` public getter by signature, because a public mapping getter cannot be referenced through `abi.encodeCall`. **`ICreatorFeeVault`** - `notifyDeposit(address token, address quoteAsset, uint256 amount)`, `isDepositor(address depositor) view returns (bool)`. `isDepositor` is declared here so the curve can ask before it deposits, rather than letting one routine decommissioning call on the vault take every trade down. **`IExternal.sol`** holds the minimal surfaces of the deployed Motoswap contracts the curve touches. Kept local so the repo builds standalone; the canonical definitions live in evm-moto-contracts and win on any conflict. See the [Motoswap contracts inventory](/dev/reference/contracts-full-inventory) for the full contracts. * `IWETH` - `deposit() payable`, `withdraw(uint256)`, `transfer(address, uint256) returns (bool)`, `approve(address, uint256) returns (bool)`, `balanceOf(address) view returns (uint256)` * `IMotoSwapFactory` - `getPair(address, address) view returns (address)`, `createPair(address, address) returns (address)`, `pairCodeHash() view returns (bytes32)` (the one CREATE2 init-code hash for the whole deployment, because pairs are beacon proxies; read live per launch), `swapFeeBps() view returns (uint256)` (governance-retunable up to `MAX_SWAP_FEE_BPS`; every pair reads it live on every swap, so the graduation floor reads it live too) * `IMotoSwapPair` - `mint(address to) returns (uint256 liquidity)`, `totalSupply() view returns (uint256)`, `getReserves() view returns (uint112, uint112, uint32)`, `token0() view returns (address)`, `price0CumulativeLast() view returns (uint256)`, `price1CumulativeLast() view returns (uint256)`. The graduation MOTO leg prices itself against the cumulatives instead of a signed voucher * `IFeeRouter` - `swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline) returns (uint256[] amounts)`, `protocolFeeBps() view returns (uint256)`. The public swap entry. The core router is perimeter-gated on `factory.swapAllowed` and the deployment asserts that only the router and the FeeRouter are admitted, so the curve goes through here like every other protocol swap: the protocol fee applies, the volume is visible to the indexer, and no allowlisting is needed. Pulls `path[0]` from the caller via allowance * `IPveVault` - `recordPve(address token, address creator, uint256 amount)` (launcher-gated on the vault; requires the coins to already sit in the vault, so the curve transfers first and records second) and `notePendingArrival(address token)` (one-shot on the vault; a second call reverts `AlreadyExpecting`, which is why the curve calls it on the first fill only, and fail-closed, because a vault that cannot record an arrival is one the curve must not escrow into) *** ## Part 2: canonical contracts moto.fun depends on [#part-2-canonical-contracts-motofun-depends-on] These live in `evm-moto-contracts` and are deployed by the DEX suite, not by the moto-fun repo. They are here because the curve cannot function without them and an integrator reading moto.fun events will meet their events too. The `PveVault`, `CreatorFeeRegistry` and `CreatorFeeVault` entries on the [Motoswap contracts inventory](/dev/reference/contracts-full-inventory) describe the same deployments, and the two pages match. ### PveVault (`src/launcher/PveVault.sol`) [#pvevault-srclauncherpvevaultsol] Escrow for PvE coins on their way to Motocat stakers. The curve's PvE fills land here by plain transfer during a coin's life on the curve, and at graduation the curve records one credit for the whole sum. Payouts are pull-based and lazy: `recordPve` freezes one credit per token against the stake at the preceding block, and each wallet divides against that frozen snapshot whenever it claims. Nothing iterates a global token list, which keeps the number of PvE tokens unlimited. UUPS proxy. Inherits `Initializable`, `UUPSRenounceable`, `Ownable2StepUpgradeable`, `ReentrancyGuardTransient`. Storage is upgrade-safe: new fields are appended and each consumes one slot of `__gap`, which stands at `uint256[43]` at this commit. The vault reads stake history through `IMotocatStakingHistory`, declared in the same file: `stakedBalanceOfAt(address wallet, uint256 blockNumber) view returns (uint256)`, `totalStakedAt(uint256 blockNumber) view returns (uint256)`, `seedingClosed() view returns (bool)`. On Ethereum that is `MotocatStakingV3`. When other networks come later, the vault there will read a `MirrorRegistry`, which exposes exactly that shape so the audited vault deploys unchanged; see the mirror section below. **Constructor and init** * `constructor()` - disables initializers on the implementation * `initialize(address initialOwner, address upgradeAuthority_)` - `initializer`. Sets the owner and the upgrade authority (zero leaves upgrades with the owner) and initialises UUPS **Types** ```solidity /// ONE credit per token, ever. Packed into one slot; both numbers are range-checked at the write. struct Credit { uint128 amount; uint64 totalStaked; uint64 creditBlock; } ``` **State variables (public getters)** * `launcher: address` - the primary launcher allowed to record receipts. Kept as its own slot rather than folded into the mapping because reinterpreting a live storage slot is the one thing a UUPS upgrade cannot do safely. A retired launch contract held it, and nothing new should claim the slot * `totalReceived(address token) -> uint256` - cumulative PvE proceeds recorded per token. Indexer convenience * `motocatStaking: address` - the staking contract (or mirror) whose history sizes a split * `credits(address token) -> (uint128 amount, uint64 totalStaked, uint64 creditBlock)` - the credit, or zeros * `claimed(address token, address wallet) -> bool` - whether a wallet has taken its share * `pending(address token) -> uint256` - escrow that landed while nobody was staked, staking was unwired, or the seed was still in flight. A per-token amount awaiting `recreditPending`, not a per-wallet claimable balance * `extraLaunchers(address) -> bool` - additional launchers allowed to record receipts, alongside `launcher`. The curve is admitted here * `stakingWiringFrozen: bool` - `true` once the first credit has been frozen, after which `setMotocatStaking` is shut. Every outstanding credit is denominated in one staking contract's history, and re-pointing afterwards would silently re-price every unclaimed credit against a ledger that never contained those holders * `expectingArrival(address token) -> bool` - the launcher has announced escrow in flight for this token and the vault has not credited it yet. Until the curve sets this, the vault cannot see its own escrow arrive: a token transfer notifies nobody, and the receipt only follows at graduation, days or weeks later. While set, `forward` refuses the token and `rescuableExcess` prices it at zero * Inherited: `upgradesRenounced() view returns (bool)`, `proxiableUUID() view returns (bytes32)`, `owner()`, `pendingOwner()` **External and public functions** * `renounceUpgradeability()` - **the upgrade authority** (the owner while the authority is zero). One-way. Freezes the implementation forever while ownership and every operational setter keep working. Never on launch day: it kills UUPS upgrades permanently * `setLauncher(address launcher_)` - **owner only**. Reverts `ZeroAddress`. Emits `LauncherSet`. Refuses zero, so the slot cannot be un-set once written * `setExtraLauncher(address launcher_, bool allowed)` - **owner only**. Reverts `ZeroAddress`. Emits `ExtraLauncherSet`. Additive to `launcher`. Revoking is immediate and strands nothing: an unauthorised launcher's coins still arrive by plain transfer, only the receipt fails, and the curve's `recordPveLate` replays it once re-authorised * `setMotocatStaking(address staking)` - **owner only**. Reverts `StakingWiringFrozen` once any credit exists, `ZeroAddress` on zero, `NotAContract(staking)` on an address with no code. The code check matters: a call to a codeless address succeeds with empty returndata, and the decode failure is not caught by `try/catch`, so a vault pointed at an EOA would make every `recordPve` revert. Emits `MotocatStakingSet` * `notePendingArrival(address token)` - **launcher or extra launcher only** (`NotLauncher`). Marks the token as expecting escrow. Reverts `ZeroAddress` on a zero token, `AlreadyExpecting(token)` if already marked, `AlreadyCredited` if a credit or pending hold already exists. **One-shot per token**, and the one-shot is load-bearing: the curve's own state machine uses the revert to detect a double-escrow, and without it a token could be re-marked after `recordPve` cleared the mark and re-lock a credited token's dust against `rescueExcess` forever. Launcher-gated because an open marker would be a free permanent denial of `forward` on any token an attacker named. Emits `PendingArrivalNoted` * `clearPendingArrival(address token)` - **owner only**. Retires a marker that guards nothing. Reverts `NotExpecting(token)` if no marker, `AlreadyCredited` if a credit or pending hold exists, and `TokenIsCredited(token)` if the vault holds **any** balance of the token. That last check is the whole safety: a marked token with a balance is escrow promised to stakers, and this is never a way to release one. Emits `PendingArrivalClearedByOwner`. Exists because a coin that stalls on the curve and never graduates would otherwise leave its marker set forever * `recordPve(address token, address creator, uint256 amount)` - **launcher or extra launcher only** (`NotLauncher`). The receipt record, which also credits. Reverts `AlreadyCredited` if a credit or pending hold exists, `ValueTooLarge` above `uint128`, `AmountNotReceived` if the vault's balance of the token is below `amount`. Adds to `totalReceived`, emits `PveReceived`. Clears `expectingArrival` and emits `PendingArrivalCleared` when the receipt is non-zero, or when it is zero and the vault holds nothing for the token; a zero receipt against a non-zero balance keeps the marker, because that contradiction is the drain the marker exists to stop. A zero `amount` returns after the receipt. Otherwise sizes the split: if `motocatStaking` is zero, or the staking side's seed is not closed, or `totalStakedAt(block.number - 1)` is zero, the amount is **held** in `pending` and `PveHeld` fires; else the credit is frozen at `(amount, staked, block.number)`, `stakingWiringFrozen` is set, and `PveCredited` fires. The preceding-block rule is what makes staking into a split in the same block worthless * `recreditPending(address token)` - **owner only**. Turns a held escrow into a claimable credit at today's stake. Reverts `NoCredit` if nothing is held, `StakingNotSet` if unwired, `SeedingNotClosed` while the staking side's seed is in flight, `PoolStillEmpty` if nobody is staked at the preceding block. Owner-only because the caller chooses the block, which is the whole basis of the anti-JIT rule; it used to be permissionless and that was wrong * `claimable(address token, address wallet) view returns (uint256)` - the wallet's share: `credit.amount * stakedBalanceOfAt(wallet, creditBlock - 1) / credit.totalStaked`. Zero if no credit, if already claimed, or if the wallet held no staked cats at the credit block. Note the token-first argument order. Truncates toward the pool, so a small permanent remainder always survives * `claim(address token) nonReentrant returns (uint256 amount)` - anyone with a share. Reverts `NothingToClaim` on zero. Pays `min(share, balance)`: a rebasing or fee-on-holdings launched token can shrink the pot, and clamping means the tail is paid what exists rather than reverting forever. Marks `claimed`, transfers, emits `PveClaimed` * `claimMany(address[] tokens) nonReentrant returns (uint256 total)` - anyone. Claims across the caller's own list; entries with nothing to claim are skipped, but the call reverts `NothingToClaim` if the whole batch yields nothing. Bounded by the caller's array, never by global state * `forward(address token, address to, uint256 amount) nonReentrant` - **owner only**. Moves a token the vault owes **nobody**: a stray transfer, or a launch whose escrow never became an entitlement. Reverts `ZeroAddress` on a zero `to` and `TokenIsCredited(token)` for any token with a credit, a pending hold, or an `expectingArrival` marker. PvE credits never expire; there is no sweep and no treasury reclaim of credited amounts. Emits `Forwarded` * `rescuableExcess(address token) view returns (uint256)` - the balance the vault provably owes nobody for a token it is already paying out: `balance - (credit.amount + pending)`, or zero if the token has no obligation at all (that case is `forward`'s) or carries an `expectingArrival` marker (an announced arrival is an obligation of unknown size, so the only sound bound is the whole balance) * `rescueExcess(address token, address to) nonReentrant returns (uint256 amount)` - **owner only**. Moves `rescuableExcess`. Reverts `ZeroAddress`, `NoExcessToRescue(token)`. Emits `ExcessRescued`. Computed, not trusted, and it never reaches the rounding residue inside `credit.amount`. No per-claim bookkeeping, deliberately: tracking a paid-out total would cost a cold store on every claim to serve an owner-only recovery * Inherited UUPS: `upgradeToAndCall(address newImplementation, bytes data) payable` - **the upgrade authority** (the owner while the authority is zero), and reverts `UpgradesAreRenounced` once renounced * Inherited ownership: `transferOwnership(address)` (owner only), `acceptOwnership()` (pending owner only), `renounceOwnership()` (owner only, single step) **Events** ```solidity event LauncherSet(address launcher); event ExtraLauncherSet(address indexed launcher, bool allowed); event MotocatStakingSet(address staking); event PveCredited(address indexed token, uint256 amount, uint256 totalStaked, uint256 creditBlock); event PveHeld(address indexed token, uint256 amount); event PveClaimed(address indexed token, address indexed wallet, uint256 amount); event PveReceived(address indexed token, address indexed creator, uint256 amount); event Forwarded(address indexed token, address indexed to, uint256 amount); event ExcessRescued(address indexed token, address indexed to, uint256 amount); event PendingArrivalNoted(address indexed token); event PendingArrivalCleared(address indexed token); event PendingArrivalClearedByOwner(address indexed token, address indexed by); ``` Inherited: `UpgradesRenounced()` from the mix-in; `Upgraded(address indexed implementation)`, `Initialized(uint64 version)`, `OwnershipTransferStarted`, `OwnershipTransferred` from OpenZeppelin. * `PveReceived.creator` is whatever the launcher passed. The curve passes `creatorOf[token]`, the launch signer, never the registry's fee recipient * `PveHeld` is the signal to put on the distribution-day runbook: the escrow sits until the owner calls `recreditPending` * `PendingArrivalCleared` and `PendingArrivalClearedByOwner` are separate on purpose, so an indexer can tell a normal graduation from an owner override **Custom errors** * `ZeroAddress()` - a setter, `forward`, `rescueExcess` or `notePendingArrival` received zero * `NotLauncher()` - `recordPve` or `notePendingArrival` from an address that is neither `launcher` nor an extra launcher * `StakingNotSet()` - `recreditPending` with no staking contract wired * `AlreadyCredited()` - `recordPve`, `notePendingArrival` or `clearPendingArrival` on a token that already has a credit or a pending hold * `AmountNotReceived()` - the vault holds less than the recorded `amount` * `NoCredit()` - `recreditPending` on a token with nothing held * `AlreadyClaimed()` - declared; no path raises it at this commit. `claimable` returns zero for a claimed wallet and the claim then reverts `NothingToClaim` * `NothingToClaim()` - `claim` or `claimMany` yielded zero * `PoolStillEmpty()` - `recreditPending` while nobody is staked at the preceding block * `SeedingNotClosed()` - `recreditPending` while the staking side's seed is still in flight * `NotAContract(address target)` - `setMotocatStaking` pointed at an address with no code * `NoExcessToRescue(address token)` - `rescueExcess` found nothing above the obligation * `StakingWiringFrozen()` - `setMotocatStaking` after a credit exists * `TokenIsCredited(address token)` - `forward` on a token owed to stakers, or `clearPendingArrival` while the vault holds a balance of the token * `ValueTooLarge()` - an amount above `uint128` or a staked count above `uint64` * `AlreadyExpecting(address token)` - a second `notePendingArrival` for the same token * `NotExpecting(address token)` - `clearPendingArrival` on a token with no marker * Inherited: `UpgradesAreRenounced()` on an upgrade after `renounceUpgradeability` **Access control** | Function | Who | | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------ | | `recordPve`, `notePendingArrival` | `launcher` or an extra launcher (the curve) | | `claim`, `claimMany`, `claimable`, `rescuableExcess` | anyone | | `setLauncher`, `setExtraLauncher`, `setMotocatStaking`, `clearPendingArrival`, `recreditPending`, `forward`, `rescueExcess`, `renounceUpgradeability`, `upgradeToAndCall`, `transferOwnership`, `renounceOwnership` | owner | | `acceptOwnership` | pending owner | **Invariants** * One credit per token, ever. A PvE token is CREATE2-deployed inside the transaction that first escrows it, so it cannot be launched twice, and a wallet's share is fixed at credit time and cannot move afterwards * A credit is never sized against the current block, and never against a half-finished seed. Held, not reverted, because a creator's paid launch must never fail over an operational condition they cannot see * Once any credit exists, `motocatStaking` is frozen. Upgrading the staking implementation behind its proxy is unaffected * The vault never moves a balance it owes: a credited token, a pending token and a marked token are all refused by `forward`, and `rescueExcess` is bounded by the computed obligation * `claimable` truncates toward the pool and the residue is deliberately stranded. It is correct, not drift * The vault sizes the split against `totalStakedAt(block.number - 1)` and pays against `stakedBalanceOfAt(wallet, creditBlock - 1)`, so both sides of every share read the same block **Two things the source says about itself that are out of date**: the `extraLaunchers` comment calls the curve's `recordPveLate` permissionless. It is owner-or-keeper gated at the curve pin. And the `launcher` slot is described as belonging to the old launch contract. That contract is retired, and the deploy order below says what to do with the slot. *** ### UUPSRenounceable (`src/upgrade/UUPSRenounceable.sol`) [#uupsrenounceable-srcupgradeuupsrenounceablesol] The UUPS mix-in `PveVault` inherits. Adds an operations-preserving one-way "upgrade-to-immutable" switch, independent of ownership, in ERC-7201 namespaced storage so it costs no sequential slots. * `upgradesRenounced() view returns (bool)` - anyone. `true` once the implementation can never be upgraded again * `_requireUpgradable()` internal - called from `_authorizeUpgrade`; reverts `UpgradesAreRenounced()` once renounced * `_renounceUpgradeability()` internal - one-way; emits `UpgradesRenounced()`. The inheriting contract exposes the access-gated external entrypoint, which on `PveVault` is `renounceUpgradeability()` *** ### The cat-stake mirror: how PvE will work off Ethereum [#the-cat-stake-mirror-how-pve-will-work-off-ethereum] Motoswap and moto.fun open on Ethereum only. No L2 mirror is live, and `outboxCount()` is zero. On Ethereum the vault reads `MotocatStakingV3` directly. On an L2 the cats stay on Ethereum, so PvE has nothing to read unless the stake state is mirrored. The mirror is one-way and count-only: every stake or unstake on Ethereum fans a `(wallet, stakedCount)` payload out to every registered L1 outbox adapter, each adapter carries it over its stack's canonical bridge, a receiver on the L2 authenticates it, and a `MirrorRegistry` on the L2 stores it as checkpoints in the same append-only shape the L1 contract uses. The L2 `PveVault` is pointed at the registry and needs no changes. Two invariants hold across every transport, and they are the reason there is one registry rather than one per stack: 1. **Pay once per answer.** An adapter refuses to resend a count it has already sent for a wallet, returning rather than reverting because the escrow wraps the call in `try/catch`. This closed an amplification where cheap permissionless rebroadcasts could drain a keeper float into duplicate messages 2. **The ordering key, bit for bit:** `(block.number << 64) | counter`, with a per-wallet counter, so the registry can drop anything not strictly newer than what it has applied. Retryable tickets and bridge messages are not ordered, and a stake and unstake in one L1 block produce two messages with the same block number A mirror that is live is not a mirror that is complete. Registering an outbox does not backfill existing stakers; until every wallet has been broadcast once, `totalStakedAt` on the L2 is an undercount, and a PvE credit landing then pays the never-swept holders nothing, permanently. The `rebroadcastMany` sweep and the registry's `closeSeeding` latch exist for exactly that. #### The broadcast surface of MotocatStakingV3 (`src/nft/MotocatStakingV3.sol`) [#the-broadcast-surface-of-motocatstakingv3-srcnftmotocatstakingv3sol] The staking contract itself is documented on the [Motoswap contracts inventory](/dev/reference/contracts-full-inventory). Only the part the mirror uses is here. * `outboxes(uint256 i) -> address`, `isOutbox(address) -> bool` - the registered adapters. `isOutbox` is also what an adapter's paid paths check before trusting the pointer back to the escrow * `outboxCount() view returns (uint256)` - anyone. Zero is a valid, expected state at Ethereum launch * `stakedBalanceOf(address wallet) view returns (uint256)` - anyone. The one read an adapter takes from the escrow * `addOutbox(address outbox)` - **owner only**. Reverts `ZeroOutbox`, `OutboxAlreadyRegistered`. Emits `OutboxAdded` * `removeOutbox(address outbox)` - **owner only**. Swap-and-pop, so ordering is not stable. Reverts `OutboxNotRegistered`. Emits `OutboxRemoved` * `rebroadcast(address wallet) nonReentrant` - anyone. Re-sends the wallet's **live** count to every adapter. Safe to leave open because it re-reads state rather than replaying a message: the worst a caller achieves is paying gas to tell every mirror the truth * `rebroadcastMany(address[] wallets) nonReentrant` - anyone. The same, batched. Reverts `EmptyArray`. No filtering and no deduplication: a wallet with zero staked cats still broadcasts `0`, because a mirror that missed an unstake is stale in exactly that direction Every stake and unstake also broadcasts, through the same private fan-out, with zero value. A failing adapter never blocks a stake: the call is wrapped in `try/catch` and surfaces as `BroadcastFailed`. ```solidity event OutboxAdded(address indexed outbox); event OutboxRemoved(address indexed outbox); event BroadcastFailed(address indexed outbox, address indexed wallet); // the stake succeeded; that chain's mirror is stale for wallet event Broadcast(address indexed wallet, uint256 stakedCount); ``` Errors on this surface: `ZeroOutbox()`, `OutboxAlreadyRegistered()`, `OutboxNotRegistered()`, `EmptyArray()`. #### IL1Outbox and IStakedCount (`src/nft/IL1Outbox.sol`, `src/crosschain/IStakedCount.sol`) [#il1outbox-and-istakedcount-srcnftil1outboxsol-srccrosschainistakedcountsol] `IL1Outbox` is the single seam every chain plugs into, vendored from the motocat-staking repo. One function, deliberately empty of stack vocabulary: * `send(bytes payload) payable` - the payload is `abi.encode(address wallet, uint256 stakedCount)`, opaque to the escrow. Implementations must tolerate zero value and must not assume the caller retries `IStakedCount` is the single view an adapter needs from the escrow: * `stakedBalanceOf(address wallet) view returns (uint256)` * `isOutbox(address outbox) view returns (bool)` - the other half of the wiring. An adapter that reads counts from a staking contract that never registered it is reading a stranger's ledger #### ArbitrumOutbox (`src/crosschain/ArbitrumOutbox.sol`) [#arbitrumoutbox-srccrosschainarbitrumoutboxsol] The first `IL1Outbox`: forwards a broadcast to an Arbitrum-stack L2 as a retryable ticket, paid from the adapter's own float, so staking gas stays flat for users. Every Arbitrum-specific concept (retryables, submission cost, address aliasing) is confined to this file. `Ownable2Step`. **Constructor** * `constructor(IArbitrumInbox inbox_, address mirror_, address owner_)` - reverts `ZeroAddress` on a zero inbox or mirror. `refundTo` defaults to the owner **Constants** * `MIN_GAS_LIMIT = 250_000` - hard floor under `gasLimit`. Too low is the one misconfiguration that fails silently in both directions: the ticket is created, `TicketCreated` fires, L1 reports success, and the auto-redeem runs out of gas on the far side, so the mirror goes stale * `MIN_MAX_FEE_PER_GAS = 0.01 gwei` - hard floor under `maxFeePerGas`. The same silent failure through a different door: a low fee makes the balance check pass more easily and the retryable never executes **State variables (public getters)** * `inbox: IArbitrumInbox` (immutable) - the Arbitrum Inbox on L1 * `mirror: address` (immutable) - the L2 `MirrorRegistry`. Immutable because a re-pointable target would let one owner transaction redirect every future broadcast * `staking: address` - the only address permitted to call `send` * `gasLimit: uint256` - L2 gas for the mirror write. Ships at `500_000`. Unused L2 gas is refunded, so headroom is close to free * `maxFeePerGas: uint256` - L2 gas price ceiling. Ships at `0.1 gwei` * `maxSubmissionCost: uint256` - floor on the submission fee. Ships at `0.001 ether`. The live figure is read from the inbox and this only floors it * `refundTo: address` - where L2 refunds unspent fees on the float-funded path * `lastMirroredPlusOne(address wallet) view returns (uint256)` - the last count this adapter paid to mirror for the wallet, stored `+1` so zero means never. The pay-once guard * `sendCounter(address wallet) view returns (uint256)` - how many messages this adapter has stamped for the wallet. Both numbers share one storage word: the encoded count in the low 128 bits, the counter above **External and public functions** * `setStaking(address staking_)` - **owner only**. Reverts `ZeroAddress`. Settable rather than immutable because the adapter is deployed before it is registered on the escrow, and an escrow redeploy must not strand a funded adapter. Emits `StakingSet` * `setParams(uint256 gasLimit_, uint256 maxFeePerGas_, uint256 maxSubmissionCost_)` - **owner only**. Reverts `GasLimitTooLow(given, floor)` under `MIN_GAS_LIMIT` and `MaxFeePerGasTooLow(given, floor)` under `MIN_MAX_FEE_PER_GAS`. `maxSubmissionCost` is unbounded because it is a floor the contract raises from live pricing. Emits `ParamsSet` * `setRefundTo(address refundTo_)` - **owner only**. Reverts `ZeroAddress`. Emits `RefundToSet` * `requiredSubmissionCost() view returns (uint256)` - anyone. The submission fee this ticket needs right now, read from `inbox.calculateRetryableSubmissionFee` for the message size, plus a 50% margin for base-fee movement between the read and the submission, floored at `maxSubmissionCost`. A failed read falls back to the floor, because this runs inside `send`, which the escrow catches * `ticketCost() view returns (uint256)` - anyone. `requiredSubmissionCost() + gasLimit * maxFeePerGas`. Read this to size a top-up or a `resend` * `send(bytes payload) payable` - **staking only** (`NotStaking`). Emits `Funded` if value arrived. Decodes `(wallet, stakedCount)`. If the adapter already mirrored this exact count, emits `AlreadyMirrored` and returns. Else reverts `Underfunded(required, held)` if the float cannot cover `ticketCost`, stamps the ordering key, submits the retryable addressed to `mirror.setStake(wallet, stakedCount, orderingKey)` with `refundTo` as both refund and beneficiary, and emits `TicketCreated` * `resend(address wallet) payable` - anyone, caller-funded. The escape hatch for a lost ticket: a retryable that expired unredeemed leaves the live count equal to the last count sent, so the pay-once guard correctly suppresses every free rebroadcast. Requires mutual wiring (`StakingNotSet`, `NotRegisteredWithStaking(staking)`), a non-zero wallet, and `msg.value >= ticketCost()` else `ResendUnderfunded(required, paid)`. Reads the count **live** from the escrow, never from the caller. The L2 refund and the ticket's beneficiary are the caller, not `refundTo`. Does **not** touch `lastMirroredPlusOne`, so a stranger cannot consume the one float-funded send a wallet was owed. Overpayment on L1 stays as float and is logged as `Funded`. Emits `Resent` * `resendMany(address[] wallets) payable` - anyone, caller-funded. The batched form, for the case where the L2 base fee rose above `maxFeePerGas` and every ticket in a window expired. Reverts `EmptyArray`. Cost is read once for the batch and the whole batch is refused if underfunded rather than paying for a prefix. Emits `Resent` per wallet * `receive() payable` - anyone. Tops up the float. Emits `Funded` * `sweep(address to, uint256 amount)` - **owner only**. Recovers float. Reverts `ZeroAddress`, `SweepFailed`. Emits `Swept` * Inherited ownership surface **Events** ```solidity event StakingSet(address indexed staking); event ParamsSet(uint256 gasLimit, uint256 maxFeePerGas, uint256 maxSubmissionCost); event RefundToSet(address indexed refundTo); event TicketCreated(address indexed wallet, uint256 stakedCount, uint256 ticketId, uint256 orderingKey, uint256 gasLimit, uint256 maxFeePerGas, uint256 submissionCost); event AlreadyMirrored(address indexed wallet, uint256 stakedCount); event Resent(address indexed wallet, address indexed payer, uint256 stakedCount, uint256 paid); event Funded(address indexed from, uint256 amount); event Swept(address indexed to, uint256 amount); ``` `TicketCreated` carries the parameters it was sent with because the one failure left on this path is invisible from L1: if the far chain's gas price rises above `maxFeePerGas`, the ticket exists and never executes. One subscription answers "was this ticket ever going to execute, and did it". `Resent.paid` is what that ticket cost, not what the caller sent; the excess surfaces as `Funded`. **Custom errors** * `ZeroAddress()` - constructor, `setStaking`, `setRefundTo`, `sweep`, `resend` or `resendMany` received zero * `NotStaking()` - `send` from anyone but `staking` * `Underfunded(uint256 required, uint256 held)` - the float cannot cover a float-funded ticket * `SweepFailed()` - the sweep transfer reverted * `GasLimitTooLow(uint256 given, uint256 floor)`, `MaxFeePerGasTooLow(uint256 given, uint256 floor)` - `setParams` bounds * `EmptyArray()` - `resendMany` with no wallets * `StakingNotSet()` - a paid path before `setStaking` * `NotRegisteredWithStaking(address staking)` - a paid path while the escrow has not registered this adapter * `ResendUnderfunded(uint256 required, uint256 paid)` - the caller did not cover the ticket **Access control**: `send` is staking-only. `resend`, `resendMany`, `receive` and the views are open. `setStaking`, `setParams`, `setRefundTo`, `sweep` and the ownership transfer are owner-only. **Invariants** * The float is spent only on a count the mirror does not already have, and only from `send`. The paid paths check `msg.value`, never the balance, so the float is unreachable from them * Every ticket carries a strictly increasing ordering key per wallet, within and across blocks * The two funding paths encode exactly the same L2 call, through one private `_submit`, so they cannot drift #### OpOutbox (`src/crosschain/OpOutbox.sol`) [#opoutbox-srccrosschainopoutboxsol] The second `IL1Outbox`: mirrors counts to an OP-stack L2 through the canonical `L1CrossDomainMessenger`. Delivery is paid by this transaction's own L1 gas, so there is no submission cost, no `maxFeePerGas` and no float to underfund. If `minGasLimit` is too low the message is delivered and reverts on L2, which is a visible failure a replay can fix. `Ownable2Step`. **Constructor** * `constructor(address messenger_, address owner_)` - reverts `ZeroAddress`. `minGasLimit` starts at `300_000` **Constants** * `MIN_GAS_FLOOR = 250_000` (`uint32`) - floor under `minGasLimit`, sized off the measured write through the receiver. A floor below the write is a legal setting under which every first-ever message for a wallet reverts out of gas on arrival **State variables (public getters)** * `messenger: IL1CrossDomainMessenger` (immutable) - a swappable messenger is a swappable trust root * `receiver: address` - the L2 `OpMirrorReceiver`, not the registry itself * `staking: address` - the escrow permitted to broadcast * `minGasLimit: uint32` - execution budget requested on L2 * `lastMirroredPlusOne(address wallet) view returns (uint256)`, `sendCounter(address wallet) view returns (uint256)` - as on the Arbitrum adapter **External and public functions** * `setStaking(address staking_)`, `setReceiver(address receiver_)` - **owner only**. Reverts `ZeroAddress`. Emits `StakingSet` / `ReceiverSet` * `setMinGasLimit(uint32 minGasLimit_)` - **owner only**. Reverts `MinGasLimitTooLow(given, floor)` under `MIN_GAS_FLOOR`. Emits `MinGasLimitSet` * `send(bytes payload) payable` - **staking only** (`NotStaking`); `ReceiverNotSet` before `setReceiver`. Pay-once guard, then stamps the key and calls `messenger.sendMessage(receiver, setStake(wallet, stakedCount, orderingKey), minGasLimit)` forwarding `msg.value`. Emits `MessageSent` * `resend(address wallet) payable` - anyone, caller-funded by the transaction's own gas. `ReceiverNotSet`, `StakingNotSet`, `ZeroAddress`. Reads the count live from the escrow. Guard carried through unchanged; only the counter moves. Emits `Resent` * Inherited ownership surface **Events** ```solidity event StakingSet(address indexed staking); event ReceiverSet(address indexed receiver); event MinGasLimitSet(uint32 minGasLimit); event MessageSent(address indexed wallet, uint256 stakedCount, uint256 orderingKey, uint32 minGasLimit); event AlreadyMirrored(address indexed wallet, uint256 stakedCount); event Resent(address indexed wallet, address indexed payer, uint256 stakedCount); ``` **Custom errors**: `ZeroAddress()`, `NotStaking()`, `ReceiverNotSet()`, `MinGasLimitTooLow(uint32 given, uint32 floor)`, `StakingNotSet()`. **Access control**: `send` is staking-only; `resend` is open; the setters and ownership are owner-only. There is no `receive`, no `sweep` and no float on this adapter. #### LzOutbox (`src/crosschain/LzOutbox.sol`) [#lzoutbox-srccrosschainlzoutboxsol] The third `IL1Outbox`: mirrors counts over LayerZero V2 to a chain with no canonical bridge from Ethereum (BSC). LayerZero charges a real per-message fee in native token, so the pay-once rule matters more here, not less, and the fee is quoted from the endpoint rather than guessed. `Ownable2Step`. **Constructor** * `constructor(address endpoint_, address owner_)` - reverts `ZeroAddress`. `refundTo` defaults to the owner **State variables (public getters)** * `endpoint: ILayerZeroEndpointV2` (immutable) * `dstEid: uint32`, `peer: bytes32` - the destination endpoint id and the receiver there, as LayerZero addresses it * `options: bytes` - execution options handed to LayerZero. Opaque here. **Empty is not a benign default**: probed against EndpointV2 on mainnet, an empty options blob makes the quote revert, so an unset lane cannot send at all, and because the escrow catches the revert, a future BSC mirror would never update * `staking: address`, `refundTo: address` * `lastMirroredPlusOne(address wallet) view returns (uint256)`, `sendCounter(address wallet) view returns (uint256)` **External and public functions** * `setStaking(address staking_)`, `setRefundTo(address refundTo_)` - **owner only**. Reverts `ZeroAddress` * `setPeer(uint32 dstEid_, bytes32 peer_)` - **owner only**. Both together, because they are one fact. Reverts `ZeroAddress` if either is zero. Emits `PeerSet` * `setOptions(bytes options_)` - **owner only**. Reverts `OptionsNotSet` on empty. Emits `OptionsSet` * `quoteFee(address wallet, uint256 stakedCount) view returns (uint256)` - anyone. The native fee the next message for this wallet would cost, quoted from the endpoint * `send(bytes payload) payable` - **staking only** (`NotStaking`); `PeerNotSet`, `OptionsNotSet`. Emits `Funded` if value arrived. Pay-once guard, then quotes against the real key and reverts `Underfunded(required, held)` if the float cannot cover it. Sends with `refundTo` as the refund address. Emits `MessageSent` with the LayerZero `guid` * `resend(address wallet) payable` - anyone, caller-funded. `PeerNotSet`, `OptionsNotSet`, `StakingNotSet`, `ZeroAddress`. Reads the count live. Quoted once, against the key actually sent, and charged to the caller (`ResendUnderfunded(required, sent)`); the caller is the refund address. Overpayment stays as float and is logged as `Funded`. Emits `Resent` * `receive() payable` - anyone. Emits `Funded` * `sweep(address to)` - **owner only**. Sweeps the whole balance. Reverts `ZeroAddress`, `SweepFailed`. Emits `Swept` * Inherited ownership surface **Events** ```solidity event StakingSet(address indexed staking); event PeerSet(uint32 dstEid, bytes32 peer); event OptionsSet(bytes options); event RefundToSet(address indexed refundTo); event MessageSent(address indexed wallet, uint256 stakedCount, uint256 orderingKey, uint256 fee, bytes32 guid); event AlreadyMirrored(address indexed wallet, uint256 stakedCount); event Resent(address indexed wallet, address indexed payer, uint256 stakedCount, uint256 paid); event Funded(address indexed from, uint256 amount); event Swept(address indexed to, uint256 amount); ``` **Custom errors**: `ZeroAddress()`, `NotStaking()`, `PeerNotSet()`, `Underfunded(uint256 required, uint256 held)`, `SweepFailed()`, `ResendUnderfunded(uint256 required, uint256 sent)`, `StakingNotSet()`, `OptionsNotSet()`. The message body is `abi.encode(wallet, stakedCount, orderingKey)`, decoded by `LzMirrorReceiver`. #### OpMirrorReceiver (`src/crosschain/OpMirrorReceiver.sol`) [#opmirrorreceiver-srccrosschainopmirrorreceiversol] Authenticates an L1 cat-stake message on the OP stack and forwards it to an unchanged `MirrorRegistry`. A receiver rather than a second registry, so the ordering-key invariant keeps one implementation. `Ownable2Step`. * `constructor(address messenger_, address registry_, address owner_)` - reverts `ZeroAddress` * `messenger: IL2CrossDomainMessenger` (immutable), `registry: IMirrorRegistryWrite` (immutable), `l1Outbox: address` - the L1 adapter whose messages are accepted. Owner-set, because the L1 side may be redeployed without redeploying this * `setL1Outbox(address l1Outbox_)` - **owner only**. Reverts `ZeroAddress`. Emits `L1OutboxSet` * `setStake(address wallet, uint256 stakedCount, uint256 orderingKey)` - **the L2 messenger only** (`NotMessenger`), and the L1 sender it reports must equal `l1Outbox` (`OutboxNotSet`, `NotL1Outbox(reported)`). Both halves are checked: `xDomainMessageSender` is stale-but-readable outside a cross-domain call, so checking only the second would let anyone call in while it happened to hold the right value. Forwards to `registry.setStake`. Emits `StakeForwarded`. Same signature as the registry's own, so the L1 adapter encodes one call for either stack * `aliasPreimage() view returns (address)` - anyone. `address(this) - 0x1111...1111`, wrapping. **The value the registry's `l1Outbox` must be set to** for this receiver to be accepted: the registry authenticates by Arbitrum alias arithmetic and the OP stack has no aliasing, so setting the preimage makes the sum land back on this contract. It is not an address anyone controls, and correcting it later kills the lane silently * `registryAcceptsThisReceiver() view returns (bool)` - anyone. On-chain proof of the round trip. Deploy scripts and operators should call this rather than reasoning about the arithmetic ```solidity event L1OutboxSet(address indexed l1Outbox); event StakeForwarded(address indexed wallet, uint256 stakedCount, uint256 orderingKey); ``` Errors: `ZeroAddress()`, `NotMessenger()`, `NotL1Outbox(address reported)`, `OutboxNotSet()`. #### LzMirrorReceiver (`src/crosschain/LzMirrorReceiver.sol`) [#lzmirrorreceiver-srccrosschainlzmirrorreceiversol] Authenticates a cat-stake message arriving over LayerZero and forwards it to an unchanged `MirrorRegistry`. Implements `ILayerZeroReceiver`. `Ownable2Step`. * `constructor(address endpoint_, address registry_, address owner_)` - reverts `ZeroAddress` * `endpoint: address` (immutable), `registry: IMirrorRegistryWrite` (immutable), `srcEid: uint32`, `peer: bytes32` * `setPeer(uint32 srcEid_, bytes32 peer_)` - **owner only**. Both together. Reverts `ZeroAddress` if either is zero. Emits `PeerSet` * `lzReceive(Origin origin, bytes32 guid, bytes message, address executor, bytes extraData) payable` - **the endpoint only** (`NotEndpoint`), then `PeerNotSet`, `WrongSourceChain(got, expected)` if `origin.srcEid != srcEid`, `WrongPeer(got, expected)` if `origin.sender != peer`. Three checks, not one: without the endpoint check anyone calls in with whatever origin they like; without the source-chain check the real peer address on a different chain is accepted; without the sender check any contract on the right chain can write the mirror. `executor` and `extraData` are ignored deliberately, because they describe who paid to deliver, not who sent. Decodes `(wallet, stakedCount, orderingKey)`, forwards to `registry.setStake`, emits `StakeForwarded` * `sweep(address to)` - **owner only**. Recovers native token that arrived with a delivery, because `lzReceive` is payable and an executor option set with value would otherwise strand it. Swept rather than rejected, so a misconfiguration is a bookkeeping problem and not a stale mirror. Reverts `ZeroAddress`, `SweepFailed`. Emits `Swept` * `aliasPreimage() view returns (address)`, `registryAcceptsThisReceiver() view returns (bool)` - as on `OpMirrorReceiver`, for the same reason ```solidity event PeerSet(uint32 srcEid, bytes32 peer); event StakeForwarded(address indexed wallet, uint256 stakedCount, uint256 orderingKey); event Swept(address indexed to, uint256 amount); ``` Errors: `ZeroAddress()`, `SweepFailed()`, `NotEndpoint()`, `PeerNotSet()`, `WrongSourceChain(uint32 got, uint32 expected)`, `WrongPeer(bytes32 got, bytes32 expected)`. #### MirrorRegistry (`src/crosschain/MirrorRegistry.sol`) [#mirrorregistry-srccrosschainmirrorregistrysol] The L2 side of the mirror: how many cats a wallet has staked on Ethereum, readable on the L2 so PvE can size a split there. This registry decides who can claim PvE, so anyone who can write it can mint themselves a share. It is therefore writable only by the aliased L1 outbox, or by a receiver whose alias preimage was installed as `l1Outbox`. Historical lookups, not just current state: writes are checkpointed in the same append-only shape `MotocatStakingV3` uses, so a past-block split cannot be staked into. `Ownable2Step`, Solidity `^0.8.20`. **Constructor** * `constructor(address owner_)` **Types** ```solidity struct Checkpoint { uint32 fromBlock; uint224 value; } // same packed shape as MotocatStaking.Checkpoint ``` **State variables (public getters)** * `l1Outbox: address` - the L1 adapter permitted to write, stored as its L1 address (or a receiver's alias preimage) * `seedingClosed: bool` - whether the initial backfill is finished. One-way. **The name and signature must stay exactly `seedingClosed()` returning `bool`**, because that is what `PveVault` probes, and the vault's probe treats a revert as "closed". Against a registry without this function the mid-seed hold could never engage on an L2 * `stakeOf(address wallet) -> uint256` - the latest mirrored stake per wallet * `totalStaked: uint256` - the sum of every wallet's mirrored stake, maintained as a running delta * `lastOrderingKey(address wallet) -> uint256` - `(L1 block << 64) | counter` of the newest message applied for the wallet **External and public functions** * `closeSeeding()` - **owner only**. One-way; a second call reverts `SeedingAlreadyClosed` so a runbook mistake is visible. Releases `PveVault`'s mid-seed hold on this chain; anything the vault held while this was false is released afterwards by `recreditPending`, one call per token. Emits `SeedingClosed`. Call it only after the last wallet is in: closing late loses nothing, closing early cannot be undone * `setL1Outbox(address l1Outbox_)` - **owner only**. Reverts `ZeroAddress`. Emits `L1OutboxSet` * `expectedAliasedSender() view returns (address)` - anyone. `l1Outbox + 0x1111...1111`, unchecked because Arbitrum's aliasing wraps modulo 2^160 * `setStake(address wallet, uint256 stakedCount, uint256 orderingKey)` - **the aliased L1 outbox only** (`NotAliasedL1Outbox`, also when `l1Outbox` is unset). A message with `orderingKey <= lastOrderingKey[wallet]` is **ignored, not reverted**: `StaleMessageIgnored` fires and nothing changes, because reverting would leave a retryable permanently unredeemable for an outcome that is correct, and the newer message already carried the truth. `<=` rather than `<`, so equal stamps do not race. Otherwise records the key, writes the checkpoint, emits `StakeMirrored` * `forceSync(address wallet, uint256 stakedCount)` - **owner only**. Owner-attested write for when the L1 path itself is unavailable. Try `rebroadcast` or `resend` on L1 first; they cannot assert something false. Advances the key to outrank the last source block the adapter was heard from, with the counter bits saturated, so an in-flight stale ticket cannot undo the correction. Emits `StakeForceSynced`, deliberately distinct from `StakeMirrored` * `forceSync(address wallet, uint256 stakedCount, uint256 outrankSourceBlock)` - **owner only**. The same attestation, naming the source block it has to outrank, for a ticket minted in a later block that then stuck long enough to be out of date. Reverts `ForceSyncWouldNotStick(wallet, currentKey, proposedKey)` if the named block would not outrank the applied key, because a write that silently does not stick is worse than a revert * `resetOrderingKey(address wallet, uint256 key)` - **owner only**. Drops a wallet's key **downwards only** (`OrderingKeyNotAhead(wallet, currentKey)` otherwise), to unfreeze a mirror that a garbage-stamped key has locked out, which is the one state `forceSync` cannot escape. Does not change the mirrored count. Its own gate and its own event because lowering a key re-opens the window a stale in-flight ticket needs. Emits `OrderingKeyReset`. Do not `forceSync` in the same block afterwards; it saturates the counter bits and would re-block every adapter message from that block * `stakeOfAt(address wallet, uint256 blockNumber) view returns (uint256)` - anyone. The wallet's mirrored count as of the end of `blockNumber`. Reverts `FutureLookup` for the current block or later * `stakedBalanceOfAt(address wallet, uint256 blockNumber) view returns (uint256)` - anyone. Alias of `stakeOfAt` under the name `PveVault` calls. **This is the point of the contract**: matching the vault's read shape means the audited vault deploys unchanged * `totalStakedAt(uint256 blockNumber) view returns (uint256)` - anyone. Total **mirrored** stake as of the end of `blockNumber`, the denominator of an L2 PvE split. On Ethereum this denominator is the truth; here it is what the mirror has been told, and a holder never broadcast is not counted * `checkpointCount(address wallet) view returns (uint256)`, `totalCheckpointCount() view returns (uint256)` - anyone * `lastSourceBlockOf(address wallet) view returns (uint256)` - anyone. `lastOrderingKey >> 64` * `lastCounterOf(address wallet) view returns (uint64)` - anyone. The low 64 bits * Inherited ownership surface **Events** ```solidity event L1OutboxSet(address indexed l1Outbox); event SeedingClosed(); event StaleMessageIgnored(address indexed wallet, uint256 orderingKey, uint256 applied); event StakeMirrored(address indexed wallet, uint256 stakedCount); event StakeForceSynced(address indexed wallet, uint256 stakedCount); event OrderingKeyReset(address indexed wallet, uint256 previousKey, uint256 newKey); ``` **Custom errors** * `ZeroAddress()` - `setL1Outbox` received zero, or a write named the zero wallet * `SeedingAlreadyClosed()` - a second `closeSeeding` * `NotAliasedL1Outbox()` - `setStake` from anyone but the aliased outbox, or before `setL1Outbox` * `FutureLookup()` - a historical read at the current block or later * `CheckpointOverflow()` - a block number above `uint32` or a value above `uint224` * `StaleMessage(address wallet, uint256 orderingKey, uint256 applied)` - declared; `setStake` emits `StaleMessageIgnored` and returns rather than raising it * `OrderingKeyNotAhead(address wallet, uint256 currentKey)` - `resetOrderingKey` with a key that is not below the current one * `ForceSyncWouldNotStick(address wallet, uint256 currentKey, uint256 proposedKey)` - the three-argument `forceSync` named a block that would not outrank the applied key **Access control** | Function | Who | | :------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------- | | `setStake` | the aliased L1 outbox (or a receiver via its alias preimage) | | `closeSeeding`, `setL1Outbox`, both `forceSync` forms, `resetOrderingKey`, `transferOwnership`, `renounceOwnership` | owner | | `acceptOwnership` | pending owner | | every `*At`, `*Of`, `*Count` read | anyone | **Invariants** * Ordering keys per wallet only ever move up, except through `resetOrderingKey`, which is owner-gated, downward-only, and has its own event * Several writes in one block collapse to one checkpoint, so a lookup at that block sees the final value * `totalStaked` moves by the difference on every write, because a mirror write replaces a wallet's count * Writes come from one of three transports through one implementation. Auth is native to each stack and lives in the adapter or receiver, never here *** ## Cross-cutting mechanics [#cross-cutting-mechanics] ### The bonding curve [#the-bonding-curve] Constant product against virtual reserves on both sides. The virtual token reserve is the zero-burn condition `W = R^2 / (S - 2R)`, rounded up to a whole token, where `R` is the coin's floor supply. `vEth`, `floorSupply` and `vTok` below are the coin's **own** parameters, resolved once per call from its `paramsId` (`paramsOf(token)` from outside). They equal `VIRTUAL_ETH`, `FLOOR` and `VIRTUAL_TOKENS` only for a coin whose `paramsId` is `0`. ```solidity (uint256 vEth, uint256 floorSupply, uint256 vTok) = _params(c); // buy: fee off the input, then the swap uint256 totalFeeBps = protocolFeeBps + creatorFeeBps; uint256 net = gross - (gross * totalFeeBps) / BPS_DENOM; uint256 ethVirt = vEth + c.realEth; uint256 tokVirt = vTok + c.tokenReserve; tokensOut = (tokVirt * net) / (ethVirt + net); // floor-crossing partial fill: clamp to the floor, ceil the ETH needed, refund the rest uint256 maxOut = c.tokenReserve - floorSupply; if (tokensOut >= maxOut) { tokensOut = maxOut; uint256 netNeeded = (ethVirt * tokensOut + (tokVirt - tokensOut) - 1) / (tokVirt - tokensOut); grossUsed = (netNeeded * BPS_DENOM + (BPS_DENOM - totalFeeBps) - 1) / (BPS_DENOM - totalFeeBps); if (grossUsed > gross) grossUsed = gross; net = grossUsed - (grossUsed * totalFeeBps) / BPS_DENOM; } // sell: the swap, then the fee off the output uint256 grossOut = (ethVirt * tokensIn) / (tokVirt + tokensIn); if (grossOut > c.realEth) revert CurveInsolvent(); uint256 fee = (grossOut * totalFeeBps) / BPS_DENOM; ethOut = grossOut - fee; // spot, 1e18 fixed point, the same number Trade.priceX18 carries price = ((vEth + c.realEth) * 1e18) / (vTok + c.tokenReserve); // progress, 0 to 1e18 progress = ((SUPPLY - c.tokenReserve) * 1e18) / (SUPPLY - floorSupply); ``` Only the net, post-fee ETH enters `realEth`; the fee is wrapped and routed out in the same call. Both ceilings in the partial fill round in the curve's favour, which is what makes the sell path structurally solvent. ### Slippage [#slippage] * Buys take `minTokensOut` only; sells take `minEthOut`. **No curve trade has a deadline parameter**, by design: there is no LP-side staleness. The only deadline on the user path is `intent.deadline` * The buy check is price-equivalent, `tokensOut * gross >= minTokensOut * grossUsed`. On a full fill `grossUsed == gross` and it reduces to `tokensOut >= minTokensOut`. On a floor-crossing partial fill it compares the price actually paid against the price the caller asked for, so a `minTokensOut` set for the full amount still passes when the curve takes less ETH and returns fewer coins at the same rate * `tokensOut == 0` always reverts `Slippage()` ### The fee model [#the-fee-model] One blended rate on both sides: `totalFeeBps = protocolFeeBps + creatorFeeBps`, shipping at `70 + 50 = 120` bps. On a buy the fee comes off the input; on a sell it comes off the ETH output. The creator fee ships at its cap, so it can only be lowered. The split, in `_routeFees`: * `creatorCut = (grossUsed * creatorFeeBps) / BPS_DENOM`, clamped to the fee; `protocolCut = fee - creatorCut` * The registry's `activeCreator(token)` is read through a guarded staticcall. Zero (disabled, or never registered) means the creator cut is zero and the whole fee is protocol * If there is a creator cut, the vault's `isDepositor(curve)` is read the same way. A missing or false answer marks the routing degraded * If either read failed or the depositor grant is gone, and the creator cut was non-zero, `CreatorFeeRoutedToCollector` fires and the entire fee goes to the Collector. **The trader-facing rate never changes.** The registry and vault are governed by canonical governance and are immutable on the curve, so an upstream drift must not be able to stop a sell when the owner is forbidden to * The whole fee is wrapped to WETH. The protocol share transfers to `collector`. The creator share transfers to the vault and is credited per coin via `notifyDeposit` The curve pays no referral fee. Referrals still earn on moto.fun: the referrer gets 10% of a referee's trading Points, the same as on Motoswap. The PvE vault receives no fee share: PvE is a coin donation funded by the buyer's own ETH. Graduation adds one more fee leg: the MOTO buyback routes through the public `FeeRouter`, so the protocol fee and the pair's live `swapFeeBps` both apply, and both are read live inside the floor calculation. ### Graduation [#graduation] The trigger is a reserve check, not a market cap check. When a buy would take `tokenReserve` below the coin's `floorSupply`, the fill clamps exactly to the floor, the excess ETH is refunded, `status` becomes `Frozen`, `frozenAt` is stamped and `FloorReached` fires. Inside that transaction `FloorReached` is emitted **before** the buy's own `Trade`, so an indexer folding status in log order sees the freeze first. A keeper normally lands `graduate` within about a minute, which is a handful of blocks, so `Frozen` is a state you will observe and must model. `graduate(token)` then runs, permissionless and unpausable, in two phases: **Cheap-fail phase, no state written.** Everything here is arithmetic and guarded reads, so a graduation that cannot be priced right now costs its keeper a few staticcalls rather than a full seeding that unwinds at the end. 1. The bounty is taken from the pot and clamped to `pot / 50`. The remainder splits: `motoLegWeth = wethAmt / 2`, `wethLp = wethAmt - motoLegWeth` 2. Both coin legs are sized at the final curve price, `p = (vEth + pot) / (floorSupply + vTok)`, using the coin's own parameters, with clamps guaranteeing `tokensLpWeth + tokensLpMoto <= floorSupply`. At `bounty = 0` the unclamped sum exceeds the floor by a sub-token amount, and an unchecked subtraction once bricked graduation permanently 3. The MOTO floor is computed (below). An unreadable pool reverts `MotoPoolUnreadable`; a missing reference reverts one of the `MotoRef*` errors 4. A zero `minMotoOut`, `tokensLpWeth`, `wethLp` or `tokensLpMoto` reverts `MotoPoolUnreadable`. Both pools are a hard guarantee, so a MOTO leg with no coin side fails cheaply here rather than opening one pool **Commit phase.** Nothing below returns a failure code; anything wrong reverts the whole thing. 5. `status = Graduated`, `realEth = 0`, `tokenReserve = 0`, and the pot minus the bounty is wrapped to WETH; the bounty stays as ETH for the keeper push 6. **Swap before bond.** The MOTO buy runs through the FeeRouter while the coin is still unbonded, so its own launch blocklist still refuses transfers to the future TOKEN/MOTO pair address. `motoOut` is measured as the received balance delta, never the router's return value, and re-checked against the floor (`MotoOutBelowFloor`) 7. `setBonded()` lifts the blocklist and the curve's allowance exemption permanently 8. **TOKEN/WETH pool.** `getPair` or `createPair`, then the leg is scaled onto the pair's live reserve ratio if the pair is not empty, so the protocol never first-mints blind into someone else's ratio. `mint(0xdead)`. A leg that shrank to zero reverts `MotoPoolUnreadable` 9. **TOKEN/MOTO pool.** `useMoto = min(motoOut, expectedOut)`, priced against the ratio TOKEN/WETH just opened at, then scaled onto the live pair ratio. **Two pools or nothing**: a dust-sized leg reverts `MotoLegTooSmall`. `mint(0xdead)` 10. **Leftovers.** Surplus MOTO above `useMoto` and any WETH the ratio match shrank off go to the Collector. The coin-side remainder, `floorSupply - tokensLpWeth - useTokens`, burns to `0xdead` 11. The PvE credit runs, inside `try/catch` with an explicit code-length check first, so it can never revert the graduation 12. The bounty is pushed to `msg.sender`, falling back to `bountyOwed` on failure 13. `Graduated` fires **LP disposition:** both pools mint LP to `0xdead`. Burned, not locked, not vested, not held. **Why atomic.** Deferring the buyback meant graduation could complete with one pool seeded and the other owed, which forced a second transaction, a second bounty, an escrow, and a hold on the TOKEN/MOTO pair address. Every one of those existed to cover a window this function no longer opens. **Why holders are not stranded by a broken upstream.** A graduation that reverts every block because the FeeRouter is paused, the factory refuses pairs, or the MOTO pool is unreadable would leave the coin `Frozen` for as long as somebody else's outage lasts. `sell` re-opens at `SELL_REOPEN_DELAY` and flips the coin back to `Trading`, so the graduation math is untouched and holders can exit. ### The MOTO price reference and floor [#the-moto-price-reference-and-floor] The MOTO leg is not gated by a spot-versus-TWAP deviation check. That design could not separate a sandwich from an honest rally, because both look like spot diverging from a lagging average, and a gate tight enough to catch the first refused the second. Instead the curve keeps a two-slot ring of checkpoints of the MOTO/WETH pair's own cumulative price, and at graduation reconstructs what the reserves would be at the average price with today's depth: ``` twap = (cumulativeNow - cumulativeRef) / elapsed // UQ112x112, MOTO per WETH rW' = ceil(sqrt(k * 2^112 / twap)) // rounded up, so rM' rounds down rM' = k / rW' net = wethIn * (BPS - protocolFeeBps) / BPS * (BPS - swapFeeBps) expectedOut = net * rM' / (rW' * BPS + net) minMotoOut = expectedOut * (BPS - maxTwapDeviationBps) / BPS ``` A swap moves price but does not move `k`, so a front-runner who pushes the pool cannot lower this floor; they can only make their own fill worse. What makes manipulation unprofitable is the fee stack: every public swap must route the FeeRouter, so a manipulator pays the pair fee plus the router fee on both legs. Both fees are read live from the factory and the FeeRouter and refused at or above 100%. The reference window is bounded below by `MIN_TWAP_WINDOW` and above by `MAX_TWAP_WINDOW`. The older slot is preferred because a longer window is more expensive to move. When the pair wrote its own cumulative within `TAIL_ANCHOR_MAX`, the window ends at that write rather than at an extrapolated tail, both on the way in (`_rollCheckpoint`) and on the way out (`_motoFloor`): the extrapolated tail is spot times elapsed at the current spot, which is the one input an attacker controls for free inside a block, and a V2 cumulative only ever records pre-swap prices, so a one-block push lives purely in that tail. Refusing to read the tail gives it zero weight. Sustained multi-window manipulation remains the known, expensive residual. Checkpoints advance for free off ordinary traffic: every buy ends with `try this.pokePriceCheckpoint{ gas: 120_000 }() {} catch {}`. It is an external self-call rather than an internal one so that a hostile pair's revert classes, including the extcodesize check and the ABI decode, land in a frame the catch can see, and the gas cap bounds the griefing. `pokePriceCheckpoint` is not `nonReentrant` for exactly this reason, and it writes only the checkpoint slots. ### PvE [#pve] The curve is a funnel into an external escrow plus one deferred credit. It never computes stake and never pays anyone. * The vault is `pveVault`, owner-settable. Zero disables fills. **Per coin the vault is pinned at the first fill** in `pveVaultOf` and never changes * Entry points: the `pveEth` slice of `launch`, and the public `buyPve`. Nothing else reaches `_escrowPve` * Each fill adds to `pveAccrued`, transfers the coins to the pinned vault immediately, and on the **first fill only** calls `notePendingArrival` fail-closed. The vault's marker is one-shot and stays set through every later fill until `recordPve` clears it, so per-fill calling would revert the second fill of every multi-fill coin * The credit runs exactly once, at graduation, inside `try/catch` after an explicit code-length check on the pinned vault. Success clears `pveAccrued` and emits `PveRecorded`; failure keeps the accrual and emits `PveRecordFailed` * **The beneficiary is `creatorOf[token]`, the launch signer**, read from the curve's own storage. Never the registry's fee recipient, and never dependent on the registry answering * `recordPveLate` is the retry path and is **owner or `pveKeeper` only**, because the vault sizes the credit at the caller's block * On the vault side the credit is one per token, sized at the preceding block, held rather than reverted when nobody is staked or the seed is in flight, and claimed pull-based by each staker against the frozen snapshot. On a future L2 deployment the vault would read a `MirrorRegistry` instead of the staking contract The deploy order that keeps the first `notePendingArrival` from reverting `NotLauncher` is: vault first, with the curve granted as an extra launcher, then the curve. A vault whose implementation predates `notePendingArrival` fails the curve closed on the first fill; see the deploy section. ### Launch and anti-snipe constraints [#launch-and-anti-snipe-constraints] **Present on chain:** * The pre-bond pool blocklist, derived per launch on every configured venue (Motoswap, and Uniswap V2 and Sushi when configured) against WETH, MOTO and every entry of `blockedQuoteAssets`. Every venue gets the same quote set. The Motoswap init-code hash is read **live** per launch through `pairCodeHash()`, because pairs are beacon proxies behind a UUPS factory and a cached hash could silently re-open pre-bond seeding after an upgrade. Coins launched before such an upgrade keep their baked lists; graduation resolves pairs live through `getPair`, so those coins still graduate correctly * The narrow curve custody exemption on the token, only while unbonded and only when `to == curve` * Atomic dev buy handling: one buy at one price, split into PvE and dev slices after, with the PvE half transferred straight to the vault and never through the creator's balance * `LaunchIntent.submitter` binding, and the rule that a funded `buyWithIntent` must name its submitter. Unbound and funded, the signature would be a bearer token a mempool watcher could take the whole dev-buy allocation with * A creator-bound CREATE2 salt, `keccak256(abi.encode(creator, salt))`, so a replayed salt from another wallet lands at a different address * Nonce and deadline on intents * Name and symbol length rules: symbol 1 to 16 bytes, name 1 to 48 bytes. No character-set rule, no uniqueness * The fee-recipient blocklist (`BadFeeRecipient`) * Registration hijack checks: an address already registered to somebody else reverts `CreatorAlreadyClaimed`; an unreadable registry reverts `RegistryUnreadable`; a refusal with the slot still empty reverts `RegistryRefused`. A refusal because the coin is already registered to this same creator is benign and the launch proceeds. No coin launches without its creator fee registered * Pause levers on launches, all buys, and per-coin buys **Deliberately absent:** * **No ticker exclusivity or reservation window.** Duplicate symbols are allowed and normal; the CREATE2 address is the identity, not the string. A reservation window only ever rationed a free resource, which made squatting the cheap attack and gave honest launches a way to fail. A short reserved list is refused outright (MOTO, WETH, ETH, USDC and USDT), which is the reserved-list check the app backend runs on `symbol`; every other symbol is open to anyone * **No block delay, no cooldown, no max buy, no per-wallet cap, no anti-bot window.** The first buyer can take everything above the coin's graduation floor in one transaction. The partial-fill clamp bounds a single buy at the graduation floor, not below it * **No deadline on curve trades** * **No LP lock**, because LP is burned * **No withdraw or sweep on the curve** * **No owner path over `graduate` or over sells** outside the bounded `Frozen` window *** ## Deploy and upgrade shape [#deploy-and-upgrade-shape] ### What is deployed, per chain [#what-is-deployed-per-chain] | Contract | Deployed by | Upgradeable | Owner | | :------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------- | | `LaunchpadCurve` | moto-fun `Deploy.s.sol`, through `ProxyDeploy`: the proxy is deployed and `initialize` runs in one transaction | Yes. UUPS behind an ERC-1967 proxy. Upgrades belong to `upgradeAuthority`, a timelock deployed by `DeployCurveTimelock.s.sol`, and can be renounced one-way | `Config.owner`, `Ownable2Step`. The owner cannot upgrade | | `GraduationSeeder`, `MotoFloorMath`, `LaunchpadTokenDeployer` | moto-fun `Deploy.s.sol`, linked into the curve implementation | No. Libraries, replaced only by upgrading the curve to an implementation linked against new ones | none | | `LaunchpadToken` (implementation) | the curve's `initialize`, through `LaunchpadTokenDeployer` | No. Clones are EIP-1167 proxies over it | none | | `LaunchpadLens` | moto-fun `Deploy.s.sol`, same batch as the curve | No | none | | `GasTank` | moto-fun `DeployGasTank.s.sol` | No | its owner, `Ownable2Step`, renounce disabled | | `CreatorFeeRegistry` | the DEX suite (canonical) | No. `Ownable2Step` | governance | | `CreatorFeeVault` | the DEX suite (canonical) | No. `Ownable2Step` | governance | | `PveVault` | the DEX suite (canonical) | UUPS, with `renounceUpgradeability` | the suite's owner key | | `MirrorRegistry`, the outboxes, the receivers | the DEX suite (`DeployCrosschain.s.sol`) | No. `Ownable2Step` | the suite's owner key | The curve is the one upgradeable contract in the moto-fun repo. `Config` is an initializer argument, what used to be immutables are storage set once, and the address you read from `/config` is the proxy. Its implementation address is in the ERC-1967 implementation slot and changes on an upgrade, so never pin it. Confirm the deployed shape against `/config` and the chain rather than against this page. ### The dependency order [#the-dependency-order] Standing moto.fun up on a chain has a fixed order, and every step has a readback. Steps 1 to 3 and 5 belong to the DEX suite; 4 and 6 to 8 to moto.fun. 1. **MOTO exists on the chain.** Ethereum: the canonical ERC-20. Later networks will use a bridged child MOTO 2. **Quote registry.** MOTO is a quote asset and `quoteRank(WETH) > quoteRank(MOTO)`, so the FeeRouter skims the WETH side of the graduation swap, which is what the curve's floor arithmetic assumes. The preflight hard-asserts this; the setting is the suite's 3. **MOTO/WETH pool seeded to the armed depth floor.** The preflight's `DEFAULT_MIN_MOTO_POOL_WETH` is `100 ether` on the WETH side, overridable by `MIN_MOTO_POOL_WETH`, and the protocol-owned LP share must be at least `MIN_POL_SHARE_BPS = 9_000` when `POL_HOLDER` is given. Read the pair after the seed, not the seed transaction; fee dust has drifted a seed under the floor before. Depth is a throughput floor, not a fill-quality knob: graduation is fill-or-revert-retry with no single-pool fallback, so back-to-back graduations beyond roughly one per reference window wait a bounded time rather than degrading 4. **PvE vault.** The suite deploys the canonical `PveVault`; moto.fun binds to it. `motocatStaking` is the chain's staking contract or mirror, or zero until the mirror exists, in which case credits hold. The curve is admitted through `setExtraLauncher(curve, true)` and is the sole launcher. **Never `setLauncher`**: that slot refuses zero and cannot be un-set, the old launch contract that held it is retired, and nothing new should claim it. Before the curve goes on a chain, probe the vault implementation for `notePendingArrival(address)`; a vault without it fails the curve closed on the first PvE fill. Read back `owner`, `launcher`, `extraLaunchers(curve)`, `motocatStaking` 5. **Chef and escrow against MOTO.** Not moto.fun's contracts, but the Points and Rakeback regime assumes the chain's MasterChef and RewardVestingEscrow are wired to this MOTO 6. **Curve.** `Deploy.s.sol`, dry run first with every `preflight:` line passing, then broadcast, then the three grants by readback, then `VerifyDeployment.s.sol` 7. **Env and address books.** Backend per-chain service and web address books read the curve, lens, token implementation and PvE vault; `/config` serves them 8. **Armed-graduation proof** with a non-creator wallet: launch with a PvE slice, stage to 90%, finish from a second wallet, and the keeper must graduate unprompted. Both pairs created, LP 100% burned, MOTO leg inside the band, bounty paid, `PveReceived` to the signer ### Deploy.s.sol (moto-fun, `contracts/script/Deploy.s.sol`) [#deployssol-moto-fun-contractsscriptdeployssol] Reads everything from env so the same script serves every environment. It deploys the curve behind its proxy through `ProxyDeploy`, so the proxy deploy and `initialize` are one transaction. Required env: `OWNER`, `DEPLOYER_KEY`, `WETH`, `MOTO`, `MOTO_FACTORY`, `FEE_ROUTER`, `COLLECTOR`, `UPGRADE_AUTHORITY` (the timelock; there is no fallback to the owner, and `initialize` refuses zero), and `DEPLOY_MODE`, which must be exactly `dry` or `broadcast`. Unset, or anything else, is refused. Optional: `POL_CHEF` and `POL_CHEF_PID`, `PVE_VAULT` (zero disables PvE), `PVE_KEEPER` (zero leaves `recordPveLate` owner-only), `CREATOR_FEE_REGISTRY` and `CREATOR_FEE_VAULT` (both or neither), `UNI_FACTORY` with `UNI_PAIR_INIT_CODE_HASH`, `SUSHI_FACTORY` with `SUSHI_PAIR_INIT_CODE_HASH`, `BLOCKED_QUOTE_ASSETS` (an address array; `USDC` and `USDT` on Ethereum), `QUOTE_REGISTRY`, `MIN_MOTO_POOL_WETH`, `POL_HOLDER`. What it does, in order: 1. Runs `LaunchpadPreflight.assertChainConfig` before a wei of gas is spent: the FeeRouter's protocol fee is readable and below 100%; the factory's swap fee is readable and within its own cap; MOTO is a quote asset and WETH outranks it; the MOTO/WETH pair exists and holds at least the depth floor; the POL share meets its floor when a holder is named, and says loudly that it skipped the check when not 2. With both canonical creator-fee addresses set, points the curve at them. With neither, deploys the vendored pair, wires `setRegistrar(curve, true)` while the deployer still owns the registry, and starts the two-step ownership transfer. Standalone testnets only 3. Deploys `LaunchpadCurve` with the `Config` above, then `LaunchpadLens` against it in the same batch. A deployment that skips the lens ships a UI with no prices 4. Grants `pveKeeper` when `PVE_KEEPER` is set and the deploy key is still the owner, and reads it back. When `OWNER` is already an account other than the deployer, prints the exact `setPveKeeper` call the owner must make instead 5. Seeds the price reference with `pokePriceCheckpoint()` inside the broadcast, and reverts the deploy if that fails, because the preflight already proved a funded pair exists and an unseeded curve would revert every graduation `MotoRefMissing` until somebody noticed 6. Prints the governance asks it cannot make itself ### The three grants [#the-three-grants] The curve needs exactly three grants on contracts it does not own, and none of them are made by the script on the canonical path: | Grant | On | Made by | If missed | | :------------------------------ | :------------------- | :-------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `setRegistrar(curve, true)` | `CreatorFeeRegistry` | governance | every launch reverts `RegistryRefused`. No coin launches unregistered | | `setDepositor(curve, true)` | `CreatorFeeVault` | governance | every trade routes its whole fee to the Collector with `CreatorFeeRoutedToCollector`. Trading continues; the creator share is recoverable by governance afterwards | | `setExtraLauncher(curve, true)` | `PveVault` | the vault owner | the first PvE fill on every coin reverts `NotLauncher` inside `notePendingArrival`, fail-closed | The curve needs **no** `factory.swapAllowed` grant. The graduation MOTO leg routes through the public FeeRouter, and the deployment's verification invariant is that only the router and the FeeRouter sit on the swap perimeter. Do not ask to widen it. ### VerifyDeployment.s.sol (moto-fun, `contracts/script/VerifyDeployment.s.sol`) [#verifydeploymentssol-moto-fun-contractsscriptverifydeploymentssol] The second half of the deploy, run after the governance calls and before opening launches. Every expectation is a required env var with no default (`CURVE`, `EXPECT_OWNER`, `EXPECT_COLLECTOR`, `EXPECT_WETH`, `EXPECT_MOTO`, `EXPECT_MOTO_FACTORY`, `EXPECT_FEE_ROUTER`, `EXPECT_PVE_VAULT`, `EXPECT_REGISTRY`, `EXPECT_CREATOR_FEE_VAULT`, the two owner expectations, `EXPECT_PROTOCOL_FEE_BPS`, `EXPECT_CREATOR_FEE_BPS`, `EXPECT_FEE_ROUTER_PROTOCOL_FEE_BPS`, `EXPECT_FACTORY_SWAP_FEE_BPS`, `EXPECT_PVE_KEEPER`, `EXPECT_LAUNCHES_PAUSED`, `EXPECT_ALL_BUYS_PAUSED`, `EXPECT_GRADUATION_BOUNTY_WEI`, `EXPECT_MAX_TWAP_DEVIATION_BPS`, `EXPECT_LAUNCH_INTENT_TYPEHASH`, `EXPECT_MIN_MOTO_POOL_WETH`, `EXPECT_POL_HOLDER`, and `EXPECT_BLOCKED_QUOTE_ASSETS`, which must exist even when empty). It re-runs every preflight assertion, asserts the three grants, and reads the curve's own storage back against the deploy book. The failure it exists to catch is silent. A missed `setDepositor` leaves a launchpad that looks perfectly deployed: the contract is there, zero-value launches succeed, the UI renders, and every trade quietly routes its whole fee to the Collector. ### DeployGasTank.s.sol (moto-fun, `contracts/script/DeployGasTank.s.sol`) [#deploygastankssol-moto-fun-contractsscriptdeploygastankssol] Deploys `GasTank` owned by the deploy key, writes one `setPayee` row per address in `GAS_TANK_PAYEES`, optionally funds it, starts the two-step transfer to `GAS_TANK_OWNER`, then reads every row and every setting back from storage before printing anything. Refuses before broadcasting a zero payee, a payee equal to the owner, a duplicate payee, a target not above its floor, a zero per-call cap, a per-day cap below the per-call cap, or a payee with code unless `GAS_TANK_ALLOW_CONTRACT_PAYEE` is set. Sizing env, each either one value for all payees or one per payee: `GAS_TANK_FLOORS_WEI`, `GAS_TANK_TARGETS_WEI`, `GAS_TANK_PER_CALL_CAPS_WEI`, `GAS_TANK_PER_DAY_CAPS_WEI`. Plus `GAS_TANK_LOW_WATERMARK_WEI`, `GAS_TANK_SEND_GAS`, `GAS_TANK_INITIAL_FUNDING_WEI`. The script's defaults for the moto.fun keeper on Ethereum: floor `0.75 ether`, target `2.5 ether`, per call `2 ether`, per window `7.5 ether` (half of the 15 ETH per 24 hours actually meant, because the window is tumbling), watermark `2.5 ether`, send gas `10_000`. The real runway comes from the owner after `acceptOwnership`, never from the deployer account. Then point the refiller cron at the address and watch one refill land: an armed thing that has never fired is not proven. ### Retiring a curve [#retiring-a-curve] A curve redeploy on a chain that already has one is a new singleton with a new coin set. Nothing migrates: the old curve keeps serving its own coins until none of them is `Trading` or `Frozen`, and only then is it marked retired on the backend and its grants revoked, `setExtraLauncher(old, false)` on the vault and `setRegistrar(old, false)` on the registry, last. The app's rule is to compare the served curve address with the one baked into its build before any wallet prompt, and to fail closed on a mismatch rather than follow the new address blindly. Integrators should do the same. *** ## Notes for integrators [#notes-for-integrators] * Every contract here is non-upgradeable except `LaunchpadCurve` and `PveVault`, both UUPS proxies, and production points at the deployed canonical registry and vault rather than the vendored copies. The curve address in `/config` is the proxy. Confirm the deployed shape against `/config` and the chain rather than against this page * `FeeRouter`, `MotoSwapFactory`, `MotoSwapPair`, `Collector` and `MotocatStakingV3` are external to moto.fun. Only the surfaces the curve or the mirror touch are described here; see the [Motoswap contracts inventory](/dev/reference/contracts-full-inventory) for the full contracts * Two error names do not match their conditions. `claimBounty` reverts `PveNothingToRecord()` when nothing is owed, and `MotoPoolUnreadable()` covers several unrelated read failures including the post-match WETH leg check. Decode by selector, and do not infer the cause from the name * `PveVault.AlreadyClaimed()` and `MirrorRegistry.StaleMessage(...)` are declared and never raised at these commits. A claimed wallet reverts `NothingToClaim`, and a stale mirror message is ignored with an event * The `Trade` event's `isBuy` is not indexed, `Graduated` does not index the pair addresses, and `FeeRecipientSet` fires only on a redirect. Build the indexer around those three facts * A funded `buyWithIntent` requires a named submitter. A backend that publishes open intents and then lets the creator fund one from their own wallet gets `ValueNeedsSubmitter`, not a launch. Route a creator's own dev buy through `launch`, or bind the intent to them * Quote through `LaunchpadLens`, never through a local copy of the math. Fees are owner-tunable state and the lens reads them at call time. A create form must use `quoteLaunch`, not `quoteBuy` * A keeper should drive graduation off `graduationReadiness` and sleep until `readyAt` on `TooFresh`. Poking during `TooFresh` makes it worse. `Upstream` is the only regime that means something is broken * Once an L2 deployment exists, `PveVault.totalStakedAt` there will be the mirror's count, not the truth. Before treating an L2 PvE credit as fair, check that the mirror was swept and `seedingClosed` on the `MirrorRegistry` is `true` * The withheld parameters (the window bounds, the tail anchor cap, the band and its two bounds, and the blocked-quote cap, all named in the curve's constants list above) are public getters on the curve. Read them once at startup, and read `maxTwapDeviationBps` again whenever `MaxTwapDeviationBpsSet` fires # Motoswap SDK - Full Public API Inventory (/dev/reference/sdk-full-inventory) The `@motoswap/sdk` package is a TypeScript library of pure functions used by the Motoswap backend and frontend. All math is deterministic; there is no network I/O and no wallet state. ## Package status [#package-status] **Package name:** `@motoswap/sdk`. **Availability:** not published yet. The package is not on any public registry and there is no install path for an outside integrator. Use this page as the specification: every function below states its formula, and each is a few lines of `bigint` math. The samples show call shapes and cannot run until the package is published. **Dependencies:** * `viem` (^2.21.0) - address utilities and type definitions * `@noble/hashes` - keccak for `grindVanitySalt` * `@motoswap/shared` (workspace) - domain types (`Address`, `VaultId`, `LockPosition`, etc.) and constants **Imports:** ```typescript import { bestRoute, getAmountsOut, bestSplitRoute, bestMidRate, pathMidRate, splitMidRate, optimalSwapInput, getAmountOut, sqrtBigInt, effectiveStake, isUnlocked, summarizeUserPositions, computeMasterChefPoolAPR, computeVaultAPR, priceFromReserves, toUsd, amountOutMin, submitAmountOutMin, amountInMax, midPriceBps, executionPriceBps, priceImpactBps, grindVanitySalt, type Hop, type MidRate, type RoutePair, type RouteResult, type SplitLeg, type SplitRouteResult, type GrindVanityParams, type VanityResult, } from "@motoswap/sdk"; ``` That is the whole export list. `BPS_DENOMINATOR` is used inside the slippage module but is not re-exported. Its value is `10_000n`; define it yourself, since `@motoswap/shared` is not published either. **Build:** TypeScript + Bun (`bun test`), exports as ES modules (`.ts` entry in `package.json`). *** ## `routing.ts` - Multi-hop swap routing and price discovery [#routingts---multi-hop-swap-routing-and-price-discovery] ### `bestRoute(tokenIn, tokenOut, pairs, amountIn, maxHops, feeBps?)` [#bestroutea-extends-stringtokenin-tokenout-pairs-amountin-maxhops-feebps] Find the highest-output path from one token to another across a curated set of pairs, within a hop limit. Generic over address type (preserves `Address` branding). Returns `null` if tokens are identical or no path exists. Enumerates all simple paths up to `maxHops` edges, prices each via `getAmountsOut`, returns argmax by output (ties keep shorter paths). The backend and frontend both use this for identical routing logic. * `tokenIn, tokenOut: A` - branded address type (typically `Address`). * `pairs: readonly RoutePair[]` - the routing graph; must include all candidate pairs (e.g., direct pairs + hub routes). * `amountIn: bigint` - exact input amount in base units. * `maxHops: number` - maximum edges to traverse (e.g., 2 for `TOKEN → HUB → WETH`). * `feeBps?: bigint` - per-pool LP swap fee in basis points; defaults to `SWAP_FEE_BPS` (30 bps). It must not fold in the once-per-trade `FeeRouter` protocol fee: pricing a multi-hop route at a combined bps-per-hop double-counts that fee and mis-ranks routes against a direct swap. The protocol fee is netted once by the caller, not per hop. **Returns:** `RouteResult | null` - `` `{ path: A[], amountOut: bigint, hops: number }` ``. ```ts const route = bestRoute(tokenA, tokenB, pairs, 1_000_000n, 2); if (route) { console.log(route.path); // [tokenA, hub, tokenB] console.log(route.amountOut); // final output in base units console.log(route.hops); // e.g. 2 } ``` ### `getAmountsOut(amountIn, hops, feeBps?)` [#getamountsoutamountin-hops-feebps] Chains `getAmountOut` across a path's hops (Uniswap V2 style), returning per-hop intermediates. If any hop is dry (non-positive reserve), collapses the rest to zero rather than throwing. * `amountIn: bigint` - starting amount in base units. * `hops: readonly Hop[]` - array of `` `{ reserveIn, reserveOut }` `` pairs, one per edge, oriented in swap direction. * `feeBps?: bigint` - per-hop swap fee in basis points; defaults to `SWAP_FEE_BPS` (30 bps). **Returns:** `bigint[]` with length = `hops.length + 1`, where `result[0] = amountIn`. ### `bestSplitRoute(tokenIn, tokenOut, pairs, amountIn, maxHops, minImprovementBps?, feeBps?)` [#bestsplitrouteatokenin-tokenout-pairs-amountin-maxhops-minimprovementbps-feebps] Find a two-leg split order when it beats the best single path (e.g., a token whose liquidity sits half in TOKEN/WETH and half in TOKEN/MOTO). It only combines paths that share no pool. Ternary-searches the split fraction; returns `null` if improvement \< `minImprovementBps` (default 25 bps) or fewer than 2 viable paths exist, or when `amountIn <= 1n`. * `minImprovementBps?: number` - a plain number, default `25`. * `feeBps?: bigint` - per-pool LP fee, default `SWAP_FEE_BPS`. **Returns:** `SplitRouteResult | null`: ```ts { legs: [SplitLeg, SplitLeg], amountOut: bigint, // combined singleOut: bigint, // best single-path for comparison improvementBps: number, // e.g. 150 = 1.5% better } ``` `SplitLeg` = `` `{ path: A[], amountIn: bigint, amountOut: bigint }` ``. ### `bestMidRate(tokenIn, tokenOut, pairs, maxHops)` [#bestmidrateatokenin-tokenout-pairs-maxhops] Fee-free reference price (MID price) chained across the best route. This is the route-selection reference: which path has the best marginal rate. Returned as an exact rational `` `{ num, den }` `` so you preserve precision. Returns `MidRate | null`. To grade the impact of a trade, use `pathMidRate` or `splitMidRate` on the path it actually executed on. ### `pathMidRate(path, pairs, feeBps)` [#pathmidrateapath-pairs-feebps] Mid rate of one specific path, chained hop by hop. This is the reference a trade actually executed on that path must be graded against. Grading against `bestMidRate` is wrong when the best-mid path is a thin, skewed pool: the reference becomes a rate no real size can fill at and the reported impact inflates. Returns `MidRate | null` (`null` when a hop is missing or dry). * `path: readonly A[]` - the token sequence, at least 2 entries. * `pairs: readonly RoutePair[]` - pairs covering every hop in `path`. * `feeBps: bigint` - **required, no default.** With a positive value each hop's LP fee is folded into the rate, so an impact graded against it measures size only and does not grow with hop count. Pass `0n` for the raw fee-free rate. ### `splitMidRate(legs, pairs, feeBps)` [#splitmidratealegs-pairs-feebps] Amount-weighted mid rate across a split order's legs, `(Σ amountIn_i × mid_i) / Σ amountIn_i`, exact-rational. Grades a combined split execution on the same scale as a single-path impact. Returns `MidRate | null` (`null` on an empty leg set, a non-positive `amountIn`, or any leg whose `pathMidRate` is `null`). * `legs: readonly { path: readonly A[]; amountIn: bigint }[]` - the split legs, shaped like `bestSplitRoute`'s output. * `pairs: readonly RoutePair[]` - pairs covering every hop in every leg. * `feeBps: bigint` - **required, no default.** Same meaning as on `pathMidRate`, applied inside each leg before the amount weighting. **Types:** * `Hop = { reserveIn: bigint; reserveOut: bigint }` * `RoutePair = { token0: A; token1: A; reserve0: bigint; reserve1: bigint }` * `RouteResult = { path: A[]; amountOut: bigint; hops: number }` * `MidRate = { num: bigint; den: bigint }` * `SplitLeg = { path: A[]; amountIn: bigint; amountOut: bigint }` * `SplitRouteResult = { legs: [SplitLeg, SplitLeg]; amountOut: bigint; singleOut: bigint; improvementBps: number }` *** ## `zap.ts` - Single-sided liquidity math [#zapts---single-sided-liquidity-math] ### `optimalSwapInput(amountIn, reserveIn, feeBps)` [#optimalswapinputamountin-reservein-feebps] Closed-form optimal amount to swap in a constant-product zap, so the swap output plus remaining input combine into liquidity with near-zero dust. Output is independent of the pool's output reserve. Matches the on-chain `MotoSwapZap._getSwapAmount` when fees are in sync. * `amountIn: bigint` - total single-token input (base units). * `reserveIn: bigint` - pool reserve of input token (must be positive). * `feeBps: bigint` - input-side swap fee. **Required, no default.** Pass the pool's live `factory.swapFeeBps()`, or `SWAP_FEE_BPS` (30 bps). ```ts const toSwap = optimalSwapInput(10_000_000n, 1_000_000_000n, SWAP_FEE_BPS); const toAdd = 10_000_000n - toSwap; ``` ### `getAmountOut(amountIn, reserveIn, reserveOut, feeBps)` [#getamountoutamountin-reservein-reserveout-feebps] Standard constant-product output for a swap with an input-side fee. Used internally by `optimalSwapInput` and exposed for simulation callers. **Formula:** `amountOut = (amountInWithFee * reserveOut) / (reserveIn * FEE_DENOMINATOR + amountInWithFee)` where `amountInWithFee = amountIn * (10_000 - feeBps)`. `feeBps` is **required, no default** - pass the pool's live `factory.swapFeeBps()`, or `SWAP_FEE_BPS` (30 bps). Throws `RangeError` if either reserve is non-positive. ### `sqrtBigInt(value)` [#sqrtbigintvalue] Integer square root via Newton's method (floor). Throws `RangeError` on negative input. *** ## `pricing.ts` - Token pricing utilities [#pricingts---token-pricing-utilities] ### `priceFromReserves({ tokenReserve, stableReserve, tokenDecimals, stableDecimals })` [#pricefromreserves-tokenreserve-stablereserve-tokendecimals-stabledecimals-] Spot price of a token in USD, derived from a pool's reserves against a stablecoin reference (assumed \~$1). Reserves go in as `bigint`. Returns USD per whole token as a JS `number`, which is display precision only: never feed the result back into amount math. **Formula:** `(stableReserve / 10^stableDecimals) / (tokenReserve / 10^tokenDecimals)` Returns `0` if `tokenReserve` is zero. ### `toUsd(amount, decimals, priceUsd)` [#tousdamount-decimals-priceusd] USD value of a base-unit token amount given a spot price. `(amount / 10^decimals) * priceUsd`. `amount` is a `bigint`; `decimals` and `priceUsd` are numbers; the result is a display `number`. *** ## `apr.ts` - APR math for farm/pool [#aprts---apr-math-for-farmpool] ### `computeMasterChefPoolAPR(motoPerBlock, allocPoint, totalAllocPoint, lpStaked, motoPriceUsd, lpPriceUsd)` [#computemasterchefpoolaprmotoperblock-allocpoint-totalallocpoint-lpstaked-motopriceusd-lppriceusd] Annualized percentage rate for a MasterChef LP pool. `MasterChef` has no emission campaign running and none is planned, so there is no live rate to feed it. Returns 0 when inputs make APR undefined (zero total alloc, zero stake, zero LP price). All six arguments and the result are JS `number`s in whole-token units, not Wei: this is a display figure. Convert on-chain `bigint` values with `formatUnits` first. **Formula:** `(motoPerBlock × allocPoint / totalAllocPoint × BLOCKS_PER_YEAR × motoPriceUsd) / (lpStaked × lpPriceUsd) × 100` **Warning:** `MasterChef` emits per second, not per block, so compute APR from `currentRewardPerSecond()` rather than from this helper. ### `computeVaultAPR(vaultId, recentFeeVolumeUsd, totalEffectiveStake, motoPriceUsd)` [#computevaultaprvaultid-recentfeevolumeusd-totaleffectivestake-motopriceusd] This function, along with `effectiveStake`, `VAULT_IDS` and `VAULT_CONFIG`, uses static multiplier values. `MotoStaking` holds one position per wallet with a lock option 0 to 3. The source of truth is `MotoStaking.boostBps(durationOption)`, the extra on top of a 1x base (`multiplier = 1 + boostBps / 10000`), which governance can change. The symbols still ship, so they are documented here, but do not build against them. Annualized percentage rate for a stake vault. Fee income is annualized from a recent (e.g., 24h) USD fee figure; stake value is the vault's effective stake (multiplier-weighted) priced in MOTO. Throws `RangeError` if `vaultId` is not in `[0, 1, 2, 3]`. **Formula:** `(recentFeeVolumeUsd × 365) / (totalEffectiveStake × motoPriceUsd) × 100` *** ## `positions.ts` - LP / farm / stake position math [#positionsts---lp--farm--stake-position-math] ### `effectiveStake(amount, vaultId)` [#effectivestakeamount-vaultid] Uses the static boost constants (see the warning on `computeVaultAPR`). Do not build against it. Multiplier-weighted stake for a position, derived from its vault's reward multiplier. Option 0 is the shortest lock; longer locks scale higher. There is no flexible, no-lock tier. **Formula:** `(amount × VAULT_CONFIG[vaultId].rewardMultiplierBps) / 10_000` ### `isUnlocked(position, now)` [#isunlockedposition-now] Returns `true` when `now >= position.lockUntil` (unix seconds). ### `summarizeUserPositions(userAddress, positions, now)` [#summarizeuserpositionsuseraddress-positions-now] Aggregates a user's vault positions into a single summary. **Returns:** `UserStakeSummary`: ```ts { userAddress: Address, totalStaked: bigint, totalEffectiveStake: bigint, positionCount: number, unlockedCount: number, } ``` **Types (imported from `@motoswap/shared`):** * `LockPosition = { positionId: number; vaultId: VaultId; amount: bigint; lockUntil: number }` * `UserStakeSummary` - as above. *** ## `slippage.ts` - Slippage helpers [#slippagets---slippage-helpers] ### `amountOutMin(quote, toleranceBps)` [#amountoutminquote-tolerancebps] `quote × (10_000 − toleranceBps) / 10_000`. Throws on negative inputs or tolerance ≥ 100%. ### `submitAmountOutMin(quoted, live, toleranceBps)` [#submitamountoutminquoted-live-tolerancebps] Floor to write into swap calldata when a live pre-submit re-quote differs from what the user reviewed. Takes the better of the two. ### `amountInMax(quote, toleranceBps)` [#amountinmaxquote-tolerancebps] `quote × (10_000 + toleranceBps) / 10_000`. For exact-out swaps. ### `midPriceBps(reserveIn, reserveOut)` [#midpricebpsreservein-reserveout] `(reserveOut × 10_000) / reserveIn`. Returns `null` for empty pools. ### `executionPriceBps(amountIn, amountOut)` [#executionpricebpsamountin-amountout] Effective execution price at `10_000` precision. Returns `null` if `amountIn == 0`. ### `priceImpactBps({ amountIn, reserveIn, reserveOut, feeBps })` [#priceimpactbps-amountin-reservein-reserveout-feebps-] Price impact in basis points, measured as the wedge between mid and execution prices. **Formula:** `impact = 1 − (amountOut × reserveIn) / (amountInAfterFee × reserveOut)`, scaled to bps, where `amountInAfterFee = amountIn × (10_000 − feeBps) / 10_000`. The LP fee is excluded on purpose: the figure measures trade size against the curve, and the fee is its own display line. Returns `bigint | null`: `null` for empty pools or zero input. `feeBps` is **required, no default** - pass the pool's live `factory.swapFeeBps()`, or 30 for a Uniswap V2 pair. *** ## `vanity.ts` - Vanity address grinding [#vanityts---vanity-address-grinding] ### `grindVanitySalt(params: GrindVanityParams): VanityResult` [#grindvanitysaltparams-grindvanityparams-vanityresult] Legacy. The `TokenLauncher` contract this targets is retired and has no mainnet address, so there is nothing to build against. The symbol still ships. Grinds a salt to find a CREATE2 address that matches a trailing vanity suffix. `TokenLauncher` deployments required each launched token to end in a specific pattern (default `5afe`). Caller-bound: the CREATE2 salt includes `msg.sender`, so a bot cannot replay a victim's ground salt. Runs in a hot loop using raw `@noble/hashes` keccak (\~3x faster than viem marshalling). Run in a Worker to avoid UI jank. **Params:** ```ts { launcher: Address, // TokenLauncher contract deployer: Address, // caller (baked into salt) name: string, symbol: string, creationBytecode: Hex, // LaunchedToken init code from @motoswap/abis suffix: number, // e.g. 0x5afe nibbles: number, // trailing hex nibbles to match (0 disables) maxIterations?: number, // default 5,000,000 } ``` **Returns:** `` `{ salt: Hex, token: Address, iterations: number }` ``. Throws `Error` if no matching salt found within `maxIterations`. *** ## Constants (from `@motoswap/shared`) [#constants-from-motoswapshared] * `SWAP_FEE_BPS = 30n` - per-pool Uniswap V2 LP fee in basis points (0.30%). * `BLOCKS_PER_YEAR = 2_628_000` (12-second blocks). * `BPS_DENOMINATOR = 10_000n`. * `VAULT_IDS = [0, 1, 2, 3]` and `VAULT_CONFIG: Record` - lock option indices and static configuration (e.g., `rewardMultiplierBps`). The multipliers are static; the source of truth is `MotoStaking.boostBps(durationOption)` (`multiplier = 1 + boostBps / 10000`). Do not build against them. *** ## Summary of Key Integration Patterns [#summary-of-key-integration-patterns] **Routing (most integrators start here):** ```ts const route = bestRoute(tokenA, tokenB, allPairs, amountIn, maxHops); // amountIn: bigint if (route) { const minOut = amountOutMin(route.amountOut, toleranceBps); // toleranceBps: bigint, 50n = 0.5% // submit swap with path = route.path, amountOutMin = minOut } ``` **Zap (single-sided LP entry):** ```ts // feeBps is required on both calls - read the live pool fee, or SWAP_FEE_BPS. // factory comes from GET /config. Check it against the zero address before you call it. const feeBps = await factory.read.swapFeeBps(); const toSwap = optimalSwapInput(amountIn, reserveIn, feeBps); const toAdd = amountIn - toSwap; const bought = getAmountOut(toSwap, reserveIn, reserveOut, feeBps); // add LP with (toAdd, bought) ``` **Pricing & APR:** ```ts const priceUsd = priceFromReserves({ tokenReserve: reserve0, stableReserve: reserve1, tokenDecimals: 18, stableDecimals: 6, }); const apr = computeMasterChefPoolAPR( motoPerBlock, allocPoint, totalAllocPoint, lpStaked, motoPriceUsd, lpPriceUsd, ); ``` **Positions & Staking:** ```ts const summary = summarizeUserPositions(userAddr, positions, now); console.log(summary.totalEffectiveStake, summary.unlockedCount); ``` # Add and remove liquidity (/dev/guides/add-remove-liquidity) Both flows call `MotoSwapRouter02` directly. Unlike swaps, anyone can add or remove liquidity: there is no swap allowlist check. `router` and `factory` come from `GET /config`, and the [deployment status table](/dev/addresses#deployment-status-on-ethereum-chain-1) tracks each contract. A zero address there is unset for the current phase, so check before you send. Uniswap V2 LP tokens are added and removed on Uniswap, not here. ## Setup used by every example [#setup-used-by-every-example] `writeContract` returns a transaction hash, not the function's return values. To read `amountA`, `amountB` and `liquidity` before you send, simulate first and pass the simulated request to the wallet. ```ts import { parseAbi, type Address } from "viem"; const routerAbi = parseAbi([ "function addLiquidity(address tokenA, address tokenB, uint256 amountADesired, uint256 amountBDesired, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline) returns (uint256 amountA, uint256 amountB, uint256 liquidity)", "function addLiquidityETH(address token, uint256 amountTokenDesired, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline) payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity)", "function removeLiquidity(address tokenA, address tokenB, uint256 liquidity, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline) returns (uint256 amountA, uint256 amountB)", "function removeLiquidityETH(address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline) returns (uint256 amountToken, uint256 amountETH)", "function removeLiquidityWithPermit(address tokenA, address tokenB, uint256 liquidity, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline, bool approveMax, uint8 v, bytes32 r, bytes32 s) returns (uint256 amountA, uint256 amountB)", ]); const erc20Abi = parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]); // `publicClient`, `wallet` and `acct` are built as in the quickstart. `router` comes from GET /config. async function approve(token: Address, spender: Address, amount: bigint) { const hash = await wallet.writeContract({ address: token, abi: erc20Abi, functionName: "approve", args: [spender, amount] }); await publicClient.waitForTransactionReceipt({ hash }); } const deadline = BigInt(Math.floor(Date.now() / 1000) + 600); ``` ## Add liquidity: token and token [#add-liquidity-token-and-token] ```ts // 1. Approve both tokens to Router02. await approve(tokenA, router, amountADesired); await approve(tokenB, router, amountBDesired); // 2. Simulate to read the amounts the pool will take, then send. const { request, result } = await publicClient.simulateContract({ address: router, abi: routerAbi, functionName: "addLiquidity", args: [tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin, acct.address, deadline], account: acct, }); const [amountA, amountB, liquidity] = result; await wallet.writeContract(request); ``` Amounts are matched to the current reserve ratio. The router takes all of one side and the matching amount of the other, and never pulls the rest. The `amount*Min` floors protect against a moved pool between quote and execution. If the pair does not exist, `addLiquidity` creates it through `MotoSwapFactory.createPair` in the same transaction. `createPair` has two requirements beyond Uniswap V2's: * Both tokens must already have code (`MotoSwap: TOKEN_NOT_DEPLOYED`). * While the factory has a `quoteRegistry` set, one side must be a registered quote asset (`MotoSwap: NO_QUOTE`). A pool between two non-quote tokens cannot be created. For a **first-time pair**, the `MINIMUM_LIQUIDITY = 1000` LP is minted to `0x0` (Uniswap-V2 convention) and the first deposit sets the price. ## Add liquidity: token and ETH [#add-liquidity-token-and-eth] ```ts await approve(token, router, amountTokenDesired); const { request, result } = await publicClient.simulateContract({ address: router, abi: routerAbi, functionName: "addLiquidityETH", args: [token, amountTokenDesired, amountTokenMin, amountETHMin, acct.address, deadline], value: amountETHDesired, // send ETH; router wraps to WETH and refunds the unused part account: acct, }); const [amountToken, amountETH, liquidity] = result; await wallet.writeContract(request); ``` ## Remove liquidity: token and token [#remove-liquidity-token-and-token] The LP token is the pair itself. Approve the pair to the router first. ```ts await approve(pairAddress, router, liquidity); const { request, result } = await publicClient.simulateContract({ address: router, abi: routerAbi, functionName: "removeLiquidity", args: [tokenA, tokenB, liquidity, amountAMin, amountBMin, acct.address, deadline], account: acct, }); const [amountA, amountB] = result; await wallet.writeContract(request); ``` ## Remove liquidity: token and ETH [#remove-liquidity-token-and-eth] ```ts await approve(pairAddress, router, liquidity); const { request, result } = await publicClient.simulateContract({ address: router, abi: routerAbi, functionName: "removeLiquidityETH", args: [token, liquidity, amountTokenMin, amountETHMin, acct.address, deadline], account: acct, }); const [amountToken, amountETH] = result; await wallet.writeContract(request); ``` ## Finding the pair address [#finding-the-pair-address] Read it: `factory.getPair(tokenA, tokenB)`. It returns the zero address when the pair does not exist. To compute it offline, note that pairs are ERC-1967 beacon proxies deployed with CREATE2, so the init code hash is not the Uniswap V2 constant and is different on every deployment. Read it from the factory. Never hardcode it. ```ts import { encodePacked, getContractAddress, keccak256, parseAbi, type Address } from "viem"; const initCodeHash = await publicClient.readContract({ address: factory, abi: parseAbi(["function pairCodeHash() view returns (bytes32)"]), functionName: "pairCodeHash", }); function pairFor(tokenA: Address, tokenB: Address): Address { const [token0, token1] = tokenA.toLowerCase() < tokenB.toLowerCase() ? [tokenA, tokenB] : [tokenB, tokenA]; return getContractAddress({ opcode: "CREATE2", from: factory, salt: keccak256(encodePacked(["address", "address"], [token0, token1])), bytecodeHash: initCodeHash, }); } ``` `INIT_CODE_PAIR_HASH()` is an alias that returns the same value. ## Permit variants (skip the `approve` tx) [#permit-variants-skip-the-approve-tx] The pair is an EIP-2612 token (`MotoSwapERC20`), so you can sign the LP approval off-chain and pass it in one call: ```ts import { parseSignature } from "viem"; const nonce = await publicClient.readContract({ address: pairAddress, abi: parseAbi(["function nonces(address) view returns (uint256)"]), functionName: "nonces", args: [acct.address], }); const signature = await wallet.signTypedData({ account: acct, domain: { name: "Motoswap V1", version: "1", chainId: config.chainId, verifyingContract: pairAddress }, types: { Permit: [ { name: "owner", type: "address" }, { name: "spender", type: "address" }, { name: "value", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" }, ], }, primaryType: "Permit", message: { owner: acct.address, spender: router, value: liquidity, nonce, deadline }, }); const { v, r, s } = parseSignature(signature); await wallet.writeContract({ address: router, abi: routerAbi, functionName: "removeLiquidityWithPermit", args: [tokenA, tokenB, liquidity, amountAMin, amountBMin, acct.address, deadline, false, Number(v), r, s], }); ``` The domain is `name = "Motoswap V1"`, `version = "1"`, the chain id, and the pair address. `DOMAIN_SEPARATOR()` is recomputed on every call, so it is fork-safe. With `approveMax = true`, sign `value = 2^256 - 1` instead of `liquidity`. The permit and the router call share one `deadline`. The router wraps the `permit` call in a try/catch. If someone front-runs your permit, the allowance is already set and the removal still goes through. ## Fee-on-transfer variants [#fee-on-transfer-variants] ### Which variants exist [#which-variants-exist] Only ETH-out remove has FoT variants: * `removeLiquidityETHSupportingFeeOnTransferTokens(token, liquidity, amountTokenMin, amountETHMin, to, deadline)` * `removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(token, liquidity, amountTokenMin, amountETHMin, to, deadline, approveMax, v, r, s)` Both return `amountETH` only. ### When to use them [#when-to-use-them] Use these when the token taxes its own transfers. They send you the token balance the router actually received instead of the nominal amount. Regular `removeLiquidity` works for every other token. ## Events emitted [#events-emitted] ```solidity event Mint(address indexed sender, uint256 amount0, uint256 amount1); event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); event Sync(uint112 reserve0, uint112 reserve1); event Transfer(address indexed from, address indexed to, uint256 value); // LP token ``` `Mint` fires on add-liquidity and `Burn` on remove-liquidity. `sender` is the router on both. `Mint` has no recipient field, so indexers take it from the LP `Transfer` from `0x0`. Read [the `feeTo` log order](/dev/guides/watch-events#the-feeto-log-order-on-uniswap-v2-pairs) before you do: a fee mint to `factory.feeTo()` can precede the user's transfer. ## Common errors [#common-errors] | Error | Cause | Fix | | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | `TransferHelper: TRANSFER_FROM_FAILED` | Router could not pull a token. Almost always a missing or too-small `approve` to the router, sometimes an insufficient balance. **The most common first-run error.** | Run the `approve` step for BOTH tokens (or the pair, on remove) before calling. | | `MotoSwapRouter: DESIRED_BELOW_MIN` | `amountADesired < amountAMin` or `amountBDesired < amountBMin`. | Fix the arguments. A min can never exceed its desired amount. | | `MotoSwapRouter: INSUFFICIENT_A_AMOUNT` / `..._B_AMOUNT` | Actual side taken \< `amount*Min`. | Re-quote, or widen slippage. | | `MotoSwap: TOKEN_NOT_DEPLOYED` | Auto-create: one of the tokens has no code. | Deploy the token first. | | `MotoSwap: NO_QUOTE` | Auto-create: neither token is a registered quote asset. | Pair against WETH, USDC, USDT or MOTO. | | `MotoSwap: INSUFFICIENT_LIQUIDITY_MINTED` | Minted LP is zero. On a first add where `sqrt(amount0 * amount1)` is below `MINIMUM_LIQUIDITY`, the pair reverts with an arithmetic panic (`0x11`) instead. | Add more liquidity. | | `MotoSwap: INSUFFICIENT_LIQUIDITY_BURNED` | Burnt LP too small to receive tokens (pair-level string). | Burn more LP. | | `MotoSwapRouter: EXPIRED` | `deadline < block.timestamp`. | Use a later deadline. | | Panic `0x11` on remove | The LP `transferFrom` underflowed: no LP allowance to the router, or not enough LP. `MotoSwapERC20` uses checked math and does not return `false`, so `MotoSwapRouter: LP_TRANSFER_FAILED` is not what you see. | Approve the pair to the router, or use a permit variant. | Note the two prefixes if you string-match: router-level reverts carry `MotoSwapRouter:`, pair-level reverts carry `MotoSwap:`. ## When to use the Zap instead [#when-to-use-the-zap-instead] If you only have one side of a pair, `MotoSwapZap.zapInToken` / `zapInETH` swaps optimally and adds liquidity in a single tx (with dust refund). See [`zap-in`](/dev/guides/zap-in). # Caching and ETags (/dev/guides/caching-and-etags) Caching is **per-route opt-in** on the API: most public GETs mount the `cache()` middleware (`Cache-Control` + `ETag`), live routes mount `noStore`, and a route that mounts neither sets no cache header at all. Follow these two rules and most of your polls come back as a cheap `304`. ## Rule 1: send `If-None-Match` [#rule-1-send-if-none-match] Cache the response body along with its `ETag`. On every subsequent request, send `If-None-Match: `. The API responds with `304 Not Modified` (no body, tiny latency, no work on the server) whenever your cached copy is still current. Tags are weak (`W/"..."`) and the match is an exact string comparison, so send the header value back verbatim, `W/` prefix and quotes included. A real exchange: ``` $ curl -si https://api.motoswap.org/points/rules | grep -iE '^(HTTP|cache-control|etag)' HTTP/2 200 cache-control: public, max-age=300, s-maxage=300, stale-while-revalidate=900 etag: W/"/points/rules|g2|points-rules-v6.0-dbdd67e9ab749e53" $ curl -si https://api.motoswap.org/points/rules \ -H 'If-None-Match: W/"/points/rules|g2|points-rules-v6.0-dbdd67e9ab749e53"' | head -1 HTTP/2 304 ``` Treat the tag as opaque. Its contents change between releases. ```ts let cache: { etag?: string; body?: unknown } = {}; async function fetchWithEtag(url: string): Promise { const r = await fetch(url, { headers: cache.etag ? { "If-None-Match": cache.etag } : {}, }); if (r.status === 304) return cache.body as T; const body = await r.json(); cache = { etag: r.headers.get("etag") ?? undefined, body }; return body; } ``` ## Rule 2: respect `Cache-Control` [#rule-2-respect-cache-control] Do not re-fetch before `max-age` elapses. The tiers: | Tier | `max-age` | `stale-while-revalidate` | Used by | | ----------- | --------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `HEARTBEAT` | 1 s | 9 s | `GET /chain/head` | | `BLOCK` | 3 s | 9 s | Indexer-fresh routes (`/tokens/...`, `/token/{addr}/price`, `/dex/pools`, `/portfolio/...`, `/rakeback/{addr}`, `/analytics/overview`, `/analytics/*/series`, `/stats/series`) | | `CRON` | 30 s | 90 s | Cron-derived routes (`/analytics/breakdown`, `/swap/candidates`, `/explore/creators`, `/points/leaderboard`, `/points/weeks`, `/points/leagues`) | | `STATIC` | 300 s | 900 s | Config-stable routes (`/config`, `/points/rules`, `/rakeback/stats`, past-epoch `/rakeback/{addr}/epoch/{epoch}`) | (`CRON_FAST` is mounted on `GET /profiles`. `/rakeback/{addr}/proof` is `STATIC`, because a posted merkle root never changes, so its proofs don't either. The tag folds the address with a roots watermark, so it only flips when a new tree the wallet appears in is published.) The full header is `public, max-age=N, s-maxage=N, stale-while-revalidate=M`. `GET /config` carries the `STATIC` header but no `ETag`, so it never answers `304`; cache it for its `max-age`. `/health/live` and `/version` are `private, no-store`. Browsers and CDNs do this for you. Bots and backends have to do it themselves. From a browser on another origin, a plain `GET` works, but a `fetch` where your script sets `If-None-Match` triggers a preflight, and the API's CORS policy, which is set for the Motoswap app origin, blocks it. Conditional polling with an explicit `If-None-Match` must run server-side, or rely on the browser's own HTTP cache, which revalidates without a preflight. ## How the version is derived [#how-the-version-is-derived] The `ETag` is built from the request path, the query params the route recognises, and a **version key**. Unrecognised query params do not change the tag, so a cache-busting `?z=123` gains nothing. The version keys: ### `blockVersion()` [#blockversion] `(indexer head block number + head hash)`. Used by any indexer-driven route. A reorg changes the hash, so the ETag changes even when the block number stays the same. ### `cronVersion([CronJob…])` [#cronversioncronjob] `max(last_run_at)` over the named cron jobs. Used by routes that depend on a cron output (e.g. `refresh-rankings` for `/swap/candidates`). ### `addrVersion(addr, userVersion)` [#addrversionaddr-userversion] The wallet's `last_activity_block`. Used by per-user routes (`/portfolio/{addr}`, `/rakeback/{addr}`, etc.). Only refreshes when THAT user transacts. ### `userVersion(addr, variant)` [#userversionaddr-variant] Subset of `addrVersion`; used when a per-user route depends on activity plus a variant discriminator. ## Per-user routes are safe under `304` [#per-user-routes-are-safe-under-304] `/portfolio/{addr}`, `/rakeback/{addr}`, `/{addr}/meta`, etc. key the ETag on the address's `last_activity_block`. Rapid polling for a wallet's state is cheap: the response is `304` until the wallet transacts. `/points/{addr}` is the one exception: it keys on `userPointsVersion` (the wallet's dropped-day + total + activity block), so its ETag also moves when the daily drop lands, not only when the wallet transacts. ## Cross-user isolation [#cross-user-isolation] The address is in the URL **path**, so: * CDN cache keys are per-address by construction (no additional `Vary` is needed). * One wallet's cached entry can never be served for another wallet. ## Cursor pagination + ETag [#cursor-pagination--etag] The full URL (including `?cursor=…`) is the cache key. A different cursor gets a different ETag. Consecutive pages under a stable cursor cache independently. ## Writes are `no-store` [#writes-are-no-store] Every POST endpoint sets `Cache-Control: private, no-store`. Don't cache them. ## Error / 4xx / 5xx responses [#error--4xx--5xx-responses] An error is **never** cached. If the tier says `max-age=300` but the handler returns `503`, the response header is force-overridden to `no-store` in the middleware and the `ETag` is dropped, so a transient failure won't sit in your cache. The same holds for `400`, `403`, `404` and `429`. ## Example: a client-side `fetch` wrapper [#example-a-client-side-fetch-wrapper] ```ts export class CachedFetch { private store = new Map(); async get(url: string): Promise { const now = Date.now(); const hit = this.store.get(url); // Serve from cache if fresh - no request at all. if (hit && hit.expires > now) return hit.body as T; const r = await fetch(url, { headers: hit?.etag ? { "If-None-Match": hit.etag } : {}, }); // Freshness window from Cache-Control (max-age). const cc = r.headers.get("cache-control") ?? ""; const maxAge = /max-age=(\d+)/.exec(cc)?.[1]; const expires = now + (maxAge ? Number(maxAge) * 1000 : 3000); if (r.status === 304 && hit) { hit.expires = expires; return hit.body as T; } const body = await r.json(); const etag = r.headers.get("etag"); if (etag) this.store.set(url, { etag, body, expires }); return body as T; } } ``` Combine with a `stale-while-revalidate` background refresh if you want sub-`max-age` freshness without adding latency to the read path. # Claim Rakeback (/dev/guides/claim-rakeback) Rakeback is the trader's share of the protocol fee. On every `FeeRouter` swap, `2 / 7` of the protocol fee is earmarked to the trader. That is the Rakeback bucket's weight in the `Collector` split, which seeds at 2 : 2 : 2 : 1 across staking, treasury, Rakeback and buyback and burn. Each swap pays 1% at launch: 0.30% to LPs and a 0.70% protocol fee. Rakeback's share of that protocol fee is 0.20% of the swap's quote-side volume. Both the fee and the weights are settable state, so read them live if you model it. `/config.addresses.rakeback` is the only source for the `RakebackV2` address (see the [deployment status table](/dev/addresses#deployment-status-on-ethereum-chain-1)). A wallet that paid no protocol fee in an epoch gets an empty list from the `/rakeback/*` routes rather than an error. So does a wallet that has traded but has no epoch open to it yet: epochs close Sunday `00:00 UTC` and open to claim about a day later, so the first root lands the week after the DEX opens. Payout runs on weekly epochs. The backend derives one merkle tree per epoch from indexed chain data, an automated poster puts the root on chain, and you claim a leaf with its proof. There is no signature to fetch, no deadline on the proof, and no signing key on the claim path. ## The lifecycle [#the-lifecycle] ### 1. Trade [#1-trade] You trade via `FeeRouter` in `epoch = floor((block.timestamp - 259200) / 604800)` (Unix weeks that start on Sunday. The `259200` offset shifts the Thursday Unix epoch start to Sunday 00:00 UTC). ### 2. Indexer captures [#2-indexer-captures] The indexer captures `feeAmount` from `FeeRouter.Swap` and records the accrual in the quote asset, at `2 / 7` of the fee. ### 3. Backend derives the epoch tree [#3-backend-derives-the-epoch-tree] Once the epoch closes, the backend fans each wallet's raw accruals through the vest (below), sums them per `(account, token)`, and builds one tree for the whole epoch. ### 4. Poster puts the root on chain [#4-poster-puts-the-root-on-chain] `postRoot(epoch, root, tokens[], declaredTotals[])`, poster-gated. The declared total per token is capped at the token's measured `Collector` inflow, so a root can never declare more money than the pot actually received. At most `MAX_TOKENS_PER_EPOCH = 8` tokens per epoch. ### 5. Activation opens claims [#5-activation-opens-claims] `activate(epoch, token)` is permissionless. It succeeds once the activation delay has elapsed since `postRoot` and the pot holds enough to cover every active slice for that token plus this one. That funding gate is why an active claim can never fail on funds: claims are delayed, never partial. The delay is `activationDelay`, bounded by `MIN_ACTIVATION_DELAY` and `MAX_ACTIVATION_DELAY`. Read the live values from the contract. It is the public verification window, and the window in which a guardian can `cancelRoot` a bad root. Once any slice of an epoch has activated, cancel is closed. ### 6. Claim within 30 days [#6-claim-within-30-days] `CLAIM_WINDOW = 30 days`, measured from the slice's `Activated` timestamp, not from epoch close. After that, `expireRoot(epoch, token)` releases the unclaimed remainder to the treasury's sweepable surplus. ## Fetch claimable state [#fetch-claimable-state] `GET /rakeback/{address}` carries everything the claim needs, including the merkle preimage and proof for every published leaf: ```ts const rb = await fetch( `https://api.motoswap.org/rakeback/${addr}`, ).then(r => r.json()); // rb.epochs: one row per published (epoch, token) leaf this wallet holds. // Each row: { epoch, token, status, amount, index, proof, root, // activatedAt, claimDeadline, usdValue } const claimable = rb.epochs.filter(l => l.status === "claimable"); ``` The guide's `fetch` calls are simple GETs, which work from any origin. A request that triggers a CORS preflight (a script-set `If-None-Match`, a JSON `POST`) only passes for the Motoswap app origin, so make those server-side. `status` is one of: | Status | Meaning | | ----------- | -------------------------------------------------------- | | `pending` | Root posted, slice not activated yet. Nothing to submit. | | `claimable` | Activated and inside the 30-day window. Submit it. | | `claimed` | The bitmap bit is set on chain. | | `expired` | The window lapsed, or the root was cancelled or expired. | Two other reads: ``` GET /rakeback/{address}/epoch/{epoch} # one epoch's leaves for the wallet GET /rakeback/{address}/proof # proofs only, across every published tree GET /rakeback/stats # currentEpoch, nextEpoch, totalEpochs ``` `/rakeback/{address}` and `/rakeback/{address}/epoch/{epoch}` return `503` when the indexer head is lagging. That is deliberate: a stale head would report an expired leaf as still claimable, so the route refuses rather than lying. `/rakeback/{address}/proof` returns `{ items: [{ epoch, token, index, amount, proof, root }] }`. An unknown wallet gets an empty `items` array. ## Claim (the normal path) [#claim-the-normal-path] `claimMany` is the usual call, because a wallet typically holds several leaves at once: ```ts import { parseAbi } from "viem"; const { epochs } = await fetch( `https://api.motoswap.org/rakeback/${addr}`, ).then(r => r.json()); const leaves = epochs.filter(l => l.status === "claimable"); const total = await wallet.writeContract({ address: rakeback, abi: parseAbi([ "function claimMany(uint256[] epochs, address[] tokens, uint256[] indexes, address[] accounts, uint256[] amounts, bytes32[][] proofs) returns (uint256)", ]), functionName: "claimMany", args: [ leaves.map(l => BigInt(l.epoch)), leaves.map(l => l.token), leaves.map(l => BigInt(l.index)), leaves.map(() => addr), leaves.map(l => BigInt(l.amount)), leaves.map(l => l.proof), ], }); ``` All six arrays must be the same length, which mapping over one source list gives you for free. `LengthMismatch` reverts otherwise. `claimMany` is **atomic**. One bad entry (already claimed, expired, bad proof) reverts the whole batch, so send only entries you believe are claimable. The API list rides indexed events, so a claim submitted from another device (or one the indexer has not caught up to) can still appear as `claimable`. One already-claimed entry reverts the entire batch. Multicall `isClaimed(epoch, token, index)` over your candidate set and drop the hits before you build the args. The contract's bitmap cannot be stale. ## Claim a single leaf [#claim-a-single-leaf] ```ts const paid = await wallet.writeContract({ address: rakeback, abi: parseAbi([ "function claim(uint256 epoch, address token, uint256 index, address account, uint256 amount, bytes32[] proof) returns (uint256)", ]), functionName: "claim", args: [BigInt(epoch), token, BigInt(index), account, BigInt(amount), proof], }); ``` A leaf is **all-or-nothing**. The contract flips one bitmap bit and pays the full `amount`; there is no cumulative counter and no partial claim. Delivery goes to the leaf's `account`, not to `msg.sender`, so anyone can push a claim to its rightful owner. ## Claim as MOTO (auto-swap in same tx) [#claim-as-moto-auto-swap-in-same-tx] ```ts const motoOut = await wallet.writeContract({ address: rakeback, abi: parseAbi([ "function claimAsMoto(uint256 epoch, address token, uint256 index, uint256 amount, bytes32[] proof, uint256 minMotoOut, address[] path, uint256 deadline) returns (uint256)", ]), functionName: "claimAsMoto", args: [BigInt(epoch), token, BigInt(index), BigInt(amount), proof, minMotoOut, [token, motoToken], deadline], }); ``` There is no `account` argument: the proof must bind `msg.sender` as the leaf account, so this one claims your own leaf only. `path` must start at `token` and end at MOTO. The swap goes through `FeeRouter`, so it pays the protocol fee on that leg like any other trade. `minMotoOut` must be a fee-netted quote (see [`swap-via-fee-router`](/dev/guides/swap-via-fee-router)). The `Swap` event for that leg names the `RakebackV2` contract as `user`, because it is the caller. When `token` is already MOTO the swap is skipped and the leaf is transferred straight out. ## The 50 / 25 / 25 vest [#the-50--25--25-vest] The vest is applied by the backend **at post time** and baked into the leaf `amount`. There is no read-time vest and no cumulative unlock on chain. Raw accrual `R[x]` from epoch `x` is fanned across three epochs by age: ``` slice0 = (R * 5000) / 10000 slice1 = (R * 2500) / 10000 slice2 = R - slice0 - slice1 // the remainder, so the three sum to R exactly ``` Epoch `x`'s published pool for a wallet is `slice0(R[x]) + slice1(R[x-1]) + slice2(R[x-2])`. So the leaf you claim for epoch `x` already blends this week's half with the tails of the two weeks before it, and you claim it once. ## Leaf and tree construction [#leaf-and-tree-construction] If you want to verify a proof yourself rather than trust the API: ``` leaf = keccak256(abi.encodePacked(uint256 index, address account, address token, uint256 amount)) parent = keccak256(min(a, b) concat max(a, b)) ``` Sorted-pair (commutative) hashing, matching OpenZeppelin's `MerkleProof.verify`. An odd trailing node is promoted to the next level unhashed. This is **not** OpenZeppelin `StandardMerkleTree`, which double-hashes its leaves. Leaves are ordered by `token` ascending, then `account` ascending, and `index` is the global 0-based position in that ordering. One tree covers the whole epoch across every token; the money, though, is accounted per `(epoch, token)`, so activation, funding, the claim bitmap and expiry are all per slice. ## The 30-day expiry [#the-30-day-expiry] `expireRoot(epoch, token)` is permissionless and lazy. After `activatedAt + CLAIM_WINDOW`, it releases the slice's unclaimed remainder from `outstanding` so the owner can `sweep` it to the treasury. `sweep` can only take the surplus above the token's live liability and can only pay the treasury address. A slice that was posted but never activated exits a different way: `cancelRoot(epoch)` while pending (guardian or owner), or the owner-only `expirePendingSlice(epoch, token)` once its activation window has fully lapsed and the pot still cannot fund it. Calling `expireRoot` on a slice that never activated reverts `NotActivated()`. ## Solidity ABI summary [#solidity-abi-summary] ```solidity // Reads function isClaimed(uint256 epoch, address token, uint256 index) view returns (bool); function epochRoots(uint256 epoch) view returns (bytes32 root, uint64 postedAt); function slices(uint256 epoch, address token) view returns (uint256 declaredTotal, uint256 claimedTotal, uint64 activatedAt, bool expired); function outstanding(address token) view returns (uint256); function activeOutstanding(address token) view returns (uint256); function totalDeclared(address token) view returns (uint256); function lifetimeInflow(address token) view returns (uint256); function accountedBalance(address token) view returns (uint256); function epochEnd(uint256 epoch) pure returns (uint256); function activationDelay() view returns (uint64); function CLAIM_WINDOW() view returns (uint256); function MIN_ACTIVATION_DELAY() view returns (uint64); function MAX_ACTIVATION_DELAY() view returns (uint64); function MAX_TOKENS_PER_EPOCH() view returns (uint256); function poster() view returns (address); function guardian() view returns (address); // Writes (permissionless) function syncInflow(address token); function activate(uint256 epoch, address token); function claim(uint256 epoch, address token, uint256 index, address account, uint256 amount, bytes32[] proof) returns (uint256); function claimMany(uint256[] epochs, address[] tokens, uint256[] indexes, address[] accounts, uint256[] amounts, bytes32[][] proofs) returns (uint256 total); function claimAsMoto(uint256 epoch, address token, uint256 index, uint256 amount, bytes32[] proof, uint256 minMotoOut, address[] path, uint256 deadline) returns (uint256 motoOut); function expireRoot(uint256 epoch, address token); // Role-gated function expirePendingSlice(uint256 epoch, address token); // owner function postRoot(uint256 epoch, bytes32 root, address[] tokens, uint256[] declaredTotals); // poster function cancelRoot(uint256 epoch); // guardian or owner function sweep(address token, uint256 amount); // owner ``` ## Events [#events] ```solidity event RootPosted(uint256 indexed epoch, bytes32 root, uint64 postedAt); event SlicePosted(uint256 indexed epoch, address indexed token, uint256 declaredTotal); event Activated(uint256 indexed epoch, address indexed token, uint64 activatedAt); event Claimed(uint256 indexed epoch, address indexed token, uint256 index, address account, uint256 amount); event ClaimedAsMoto(address indexed user, address indexed token, uint256 indexed epoch, uint256 amountIn, uint256 motoOut); event Expired(uint256 indexed epoch, address indexed token, uint256 remainder); event PendingSliceExpired(uint256 indexed epoch, address indexed token, uint256 declared); event RootCancelled(uint256 indexed epoch); event InflowSynced(address indexed token, uint256 delta, uint256 lifetimeInflow); event Swept(address indexed token, uint256 amount); ``` ## Common errors [#common-errors] | Error | Cause | Fix | | --------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | `RootPending()` | The slice has not been activated yet. | Wait for the activation delay, or call `activate` yourself. | | `RootExpired()` | Past `activatedAt + CLAIM_WINDOW`. | Nothing to do. The remainder went to treasury. | | `AlreadyClaimed()` | The bitmap bit for that `index` is already set. | Drop the leaf. Pre-filter with `isClaimed`. | | `InvalidProof()` | Proof does not verify against the epoch root. | Refetch the proof. A cancel-and-repost changes the root. | | `LengthMismatch()` | `claimMany` arrays are different lengths. | Fix your batch. | | `BadPath()` | `claimAsMoto` path does not run `token` to MOTO. | First element must be `token`, last must be MOTO. | | `NoRoot()` | No root posted for that epoch. | Nothing published yet. | | `NotFunded()` | `activate` ran before the pot could cover the slice. | Retry after the Collector distributes. | | `NotYetActivatable()` | `activate` ran inside the activation delay. | Wait out `activationDelay`. | | `Overclaim()` | The leaf would push the slice past its `declaredTotal`. A bad root, not a caller error. | Nothing the caller can fix. Do not retry that leaf. | | `NotActivated()` | `expireRoot` on a slice that never activated. | Nothing to expire. | ## Things you can skip [#things-you-can-skip] * Nothing off-chain is signed. The only signature involved is the wallet signing its own claim transaction. * Proofs do not expire. A published root is immutable, so the API serves proofs from a `STATIC` cache and you can hold one indefinitely. The one case that invalidates a proof is a guardian `cancelRoot` followed by a corrected re-post, which is why `InvalidProof` means refetch. * The tokens that pay Rakeback are the quote assets. Enumerate it from `GET /rakeback/{address}` rather than on chain. # Cursor Pagination (/dev/guides/cursor-pagination) List endpoints on the API are **cursor-paginated** with an opaque HMAC token, except two bounded sets served whole and three per-token and per-pool lists that take a page number. ## Cursor style (default) [#cursor-style-default] ``` GET /activity?cursor= GET /dex/pools?cursor= GET /explore/tokens?cursor= GET /explore/farms?cursor= # array is named "farms" GET /explore/creators?cursor= # array is named "creators" GET /swap/candidates # (unpaginated: bounded candidate set) GET /farms # (unpaginated: bounded pool set) ``` Response shape: ```json { "items": [ ... ], "nextCursor": "ZXlKcFpDSTZOREkyTURkOQ.WZ6-aoIxCHpYZLEc", "hasMore": true } ``` * `items` - page of records (`farms` on `/explore/farms`, `creators` on `/explore/creators`). * `nextCursor` - pass to `?cursor=` on the next request. `null` when `hasMore == false`. The token is two URL-safe base64 parts joined by a dot, a payload and a signature. Treat it as opaque and URL-encode it anyway. * `hasMore` - derived by over-fetching `PAGE_SIZE + 1` records. Page size is a **server constant** per endpoint. There is no `?limit=` on cursor endpoints. ### Scope binding: a cursor cannot be replayed [#scope-binding-a-cursor-cannot-be-replayed] The cursor is an HMAC-signed token over `(keyset value, scope string)`. The scope string includes the endpoint path AND its query filters. So: ``` GET /activity?user=0xA&cursor=X # OK GET /activity?user=0xB&cursor=X # 403 - cursor was signed for user=0xA GET /dex/pools?cursor=X # 403 - cursor was signed for /activity ``` A tampered or replayed cursor returns **`403`**, not `400`. This means: always pass the exact same filters on every page. Changing `?user=` or `?type=` mid-pagination requires a fresh first page (no cursor). ### Iterating a list [#iterating-a-list] The unfiltered feed is long. Filter it, keep the same filter on every page, and stop when you have what you need: ```ts type Activity = { transactionId: string; kind: string; timestamp: number }; const base = "https://api.motoswap.org/activity?type=farm_harvest"; const MAX_PAGES = 3; let cursor: string | null = null; const all: Activity[] = []; for (let i = 0; i < MAX_PAGES; i++) { const q = cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""; const r = await fetch(base + q); if (!r.ok) throw new Error(`activity ${r.status}`); const page = await r.json(); all.push(...page.items); if (!page.hasMore) break; cursor = page.nextCursor; } ``` `/activity` pages hold 25 rows. `/dex/pools`, `/explore/tokens` and `/explore/farms` hold 100. ### First page [#first-page] Just omit `?cursor=`. The server returns the first page and sets `nextCursor` for whatever comes next. ## Unpaginated (bounded set) [#unpaginated-bounded-set] Two routes are unpaginated because the set behind them is small and fixed by the team: * `GET /farms` - every farm pool in one response. * `GET /swap/candidates?tokenIn=&tokenOut=` - the candidate pair set for a route pair. Amount-independent, cache-friendly, small. Both are safe to load in full. The Motoswap app filters and sorts them in the browser. ## Page-numbered lists [#page-numbered-lists] Three lists take `?page=` (1 to 200, 50 rows per page) instead of a cursor, and their envelopes differ: | Route | Envelope | | ----------------------------- | ------------------------------------ | | `GET /tokens/{addr}/swaps` | `{ items, page, pageSize, hasMore }` | | `GET /tokens/{addr}/pools` | `{ items, page, totalPages, total }` | | `GET /dex/pools/{pair}/swaps` | `{ items, page, totalPages, total }` | `/dex/pools/{pair}/swaps` reaches back 200 pages, so `total` tops out at `10000` on a busy pool. ## Combining with ETags [#combining-with-etags] Cursor URL + ETag = per-page cache. Consecutive pages under a stable cursor each get their own ETag. See [`caching-and-etags`](/dev/guides/caching-and-etags). ## Sort direction & keyset [#sort-direction--keyset] You cannot re-sort a cursor list by passing a different `sort=`. The cursor encodes the keyset it was cut from (e.g. `(tvl_usd, pair_address)`), so switching the sort mid-pagination fails the signature. Start fresh (no cursor) with the new sort. ## Common errors [#common-errors] | Status | Cause | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `403` | Any cursor failure - malformed token, bad signature, or scope mismatch (endpoint, filters, or query changed). Body: `{"error":"invalid cursor"}`. Refetch the first page. | | `400` | Validation failure on a non-cursor field (bad address, bad range enum). | # Points and referrals (/dev/guides/points-and-referrals) Points track activity on Motoswap and count toward the SZN 1 airdrop. The scoring engine runs in the indexer; **its internals (rates, curves, tiers, milestone bases, boost formulas) are not published**. What is public: * Every fee-paying swap earns Points, at any size. Trades on moto.fun earn trading Points too. * Motocats multiply swap Points. **Staked cats only**: a cat earns for the wallet that staked it in `MotocatStaking`, and only while it is staked. A cat merely held in a wallet counts for nothing. * Referrals pay the referrer **10%** of the referee's Points, one level, added on top. This includes Points from the referee's moto.fun trades. * Balances and the leaderboard publish once a day at the **00:00 UTC drop**. Per-swap deltas are never observable via the API. Before the first daily drop, balances are `0`, `rank` is `null` and the leaderboard is empty. Every response already has its final shape. ## Read Points [#read-points] ``` GET /points/{address} # per-wallet balance + rank (updates at the daily drop) GET /points/leaderboard # top 100 GET /points/rules # the canonical public season config GET /points/weeks # settled weeks GET /points/leagues # current week's leagues GET /points/fees # public fee dashboard GET /points/pair/{pair} # a pair's public label: "standard" or "burn-verified" GET /referrals/{address} # referrer, referee count, referral Points ``` ```ts const wallet = "0x0000000000000000000000000000000000000001"; const p = await (await fetch(`https://api.motoswap.org/points/${wallet}`)).json(); console.log(p.totalPoints, p.rank, p.referral.linkable); // 0 null true const rules = await (await fetch("https://api.motoswap.org/points/rules")).json(); console.log(rules.referralRateBps); // 1000 = 10% ``` `totalPoints` and `referralPoints` are plain numbers, not token amounts. Render `referralPointsDisplay` where you show referral Points. An unknown wallet answers `200` with zeros, never `404`. `GET /points/rules` is the canonical public source. Read season numbers from it at runtime rather than hard-coding them. Its `copy` block is display text whose wording changes without notice: render it, never parse it. ## Bind a referral [#bind-a-referral] A referral binds when the referee signs to confirm it, **before their first trade**, and cannot be changed after. In API terms: the referee signs a message, you post it, and the API refuses the binding once that wallet has scored Points. Check first. `referral` on `GET /points/{address}` is the same test the write runs: | `referral.reason` | Meaning | | ----------------- | -------------------------------------------------------------- | | `ok` | Nothing about this wallet blocks a binding (`linkable: true`). | | `already_linked` | The wallet already has a referrer. Permanent. | | `already_scored` | The wallet has already scored Points. Too late to bind. | Then have the referee sign this exact text with `personal_sign` and post it: ```ts import type { WalletClient } from "viem"; function referralMessage(referee: string, referrer: string, chainId: number): string { return ( `Activate your Motoswap referral link.\n\n` + `This is a free signature. It does not send a transaction, spend gas, or approve any funds. ` + `It links your wallet to the person who referred you.\n\n` + `Referred by: ${referrer.toLowerCase()}\n` + `Your wallet: ${referee.toLowerCase()}\n\n` + `motoswap-referral:${referee.toLowerCase()}:${referrer.toLowerCase()}:${chainId}` ); } // In the browser: the referee signs. chainId is `chainId` from GET /config. export async function signReferral(wallet: WalletClient, referrer: `0x${string}`, chainId: number) { const referee = wallet.account!.address; const signature = await wallet.signMessage({ account: wallet.account!, message: referralMessage(referee, referrer, chainId), }); return { referee, referrer, signature }; } // On your server: post what the browser sent you. export async function postReferral(referee: string, referrer: string, signature: string) { const r = await fetch("https://api.motoswap.org/referrals/attribute", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ referee, referrer, signature, source: "my-app" }), }); return { status: r.status, body: await r.json() }; } ``` | Status | Body | Meaning | | ------ | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `200` | `{ "bound": true, "binding": { ... } }` | Bound. | | `200` | `{ "bound": false, "already": true, "binding": { ... } }` | The referee already had a referrer. Nothing changed. | | `400` | `{ "bound": false, "error": "referee-already-active" }` | The referee has scored Points. Other `400` reasons cover self-referral and cycles. | | `403` | `{ "bound": false, "error": "signature does not authorize this binding" }` | The signature is not the referee's, or it was made over different text. | Smart accounts work: when plain signature recovery fails, the API checks ERC-1271 on chain. The signature costs no gas and the binding is off chain. Post from your server: the API's CORS policy is set for the Motoswap app origin, so a browser on another origin blocks this `POST`. See the [API reference](/dev/reference/api-full-inventory) for every field. # Read Prices (/dev/guides/read-prices) Four ways, each with a specific niche: | Path | Freshness | Answers with | Use when | | --------------------------- | ----------------------- | -------------------------------------------------- | ------------------------------------------------------------ | | REST `/token/{addr}/price` | \~1 block (indexer lag) | One price, or `null` when unknown | One spot price in USD. | | REST `/holdings/tokens` | \~1 block | Prices for the curated set | Bulk price book in one call, with full-precision `ethPrice`. | | REST `/tokens/{addr}/chart` | \~1 block | Candles, empty for a token with no trading history | UI display; historical windows. | | RPC `getReserves()` | Immediate | Reserves on any V2-shaped pair | Pre-submit sanity; quoting a specific pool. | REST prices come from the pairs the indexer tracks, Motoswap and Uniswap V2 both. Per-contract deployment status is on the [contract addresses page](/dev/addresses). The API's CORS policy is set for the Motoswap app origin. From a browser on another origin a plain `GET` works, but a `fetch` where your script sets `If-None-Match` is preflighted and blocked. Run the ETag examples on this page server-side. ## Option A: REST, per token [#option-a-rest-per-token] Mind the path. The price route is singular, `/token/{address}/price`. The chart route in Option C is plural, `/tokens/{address}/chart`. Swap them and you get a 404: `/tokens/{address}/price` and `/token/{address}/chart` do not exist. This runs as written and prints the MOTO price: ```ts const MOTO = "0xbd965230588eaa536de6aa45e8ebbc01638535e0"; const r = await fetch(`https://api.motoswap.org/token/${MOTO}/price`); const { priceUsd, source } = await r.json(); console.log(priceUsd, source); // "0.003295" "onchain" ``` The full response: ```json { "priceUsd": "0.003295", "source": "onchain", "lastUpdated": "YYYY-MM-DDTHH:mm:ss.sssZ" } ``` | Field | Type | Meaning | | ------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `priceUsd` | `string \| null` | Decimal-dollar string with up to six decimals. `null` for an unpriced or unknown token. | | `source` | `string \| null` | Pricing source label. `"onchain"` for a pool-derived price. `null` when there is no price row. | | `lastUpdated` | `string \| null` | ISO 8601 UTC timestamp with milliseconds, from the token's price row. It can trail the price itself, so do not use it as a freshness signal. Use `GET /chain/head` for that. | An unknown token answers `200` with all three fields `null`, not `404`. For a full-precision price use `ethPrice` from Option B. Address case does not matter on this route. Tested with the MOTO address in three forms: | Form | Example | Answer | | ------------- | -------------------------------------------- | ------------------------- | | Lowercase | `0xbd965230588eaa536de6aa45e8ebbc01638535e0` | `200` with the MOTO price | | Checksummed | `0xBd965230588EAA536dE6aA45E8ebbc01638535e0` | `200` with the MOTO price | | Uppercase hex | `0xBD965230588EAA536DE6AA45E8EBBC01638535E0` | `200` with the MOTO price | The route does not verify the checksum. An address of the wrong length, or one without the `0x` prefix, answers `400`. Each spelling is a separate cache entry at the edge, so pick one form (lowercase matches what the API returns everywhere else) and keep to it. The response carries `Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9` and a weak `ETag`. ## Option B: REST, bulk price book [#option-b-rest-bulk-price-book] ```ts const MOTO = "0xbd965230588eaa536de6aa45e8ebbc01638535e0"; const book = await (await fetch("https://api.motoswap.org/holdings/tokens")).json(); // book.tokens[].{address, decimals, priceUsd, ethPrice, priceUsd24hAgo} const moto = book.tokens.find((t: { address: string }) => t.address === MOTO); // Value 10,000 MOTO in WETH wei with integer math only. const amount = 10_000n * 10n ** BigInt(moto.decimals); const valueWethWei = (amount * BigInt(moto.ethPrice)) / 10n ** BigInt(moto.decimals); console.log(moto.priceUsd, valueWethWei); // "0.003296" 13133693188290000n ``` The shape is strict: address, decimals, the two USD prices, and `ethPrice` (WETH per whole token scaled by 1e18, an integer string, nullable). There is no symbol or name (resolve those from `GET /tokens`). Best when you have a wallet's balances and want to value them all at once. On this endpoint `decimals` is always a number (the set is curated), and for precision-critical valuation of low-priced tokens prefer `ethPrice x` your WETH/USD anchor over the `priceUsd`, which is cut to six decimal places. `priceUsd24hAgo` is `null` on every row until candles exist. ## Option C: REST chart [#option-c-rest-chart] ```ts const r = await fetch( `https://api.motoswap.org/tokens/${tokenAddress}/chart?range=24h`, { headers: savedEtag ? { "If-None-Match": savedEtag } : {} }, ); if (r.status === 304) { // cache hit - reuse your saved data } else { const data = await r.json(); savedEtag = r.headers.get("etag"); console.log(data.price); // decimal-dollar string, or null console.log(data.change24hPct); // number, e.g. 5.0, or null console.log(data.volume24hUsd); // decimal-dollar string (or "0") console.log(data.candles); // array of OHLC candles } ``` Candles are built from Motoswap pair trades, so a token with no Motoswap trading history answers `price: null`, `null` deltas, `"0"` volumes and `candles: []`. Use Option A or B for a spot price. Range values: the long frames `24h` | `7d` | `30d` (buckets 1min / 30min / 120min) plus the short frames `1m` | `5m` | `10m` | `30m`, named for their candle interval (windows 6h / 12h / 24h / 3d). The bucket is echoed back as `bucketMinutes`. Each candle looks like this: ```json { "minute": 29762621, "open": "0.0003459298143979567603005925", "high": "0.0003459298143979567603005925", "low": "0.0003454649992475997730919503", "close": "0.0003454649992475997730919503", "volumeUsd": "199.325593" } ``` `minute` is a **bucket index, not a timestamp**. Multiply by 60 to get Unix seconds for your x-axis: ```ts const unixSeconds = candle.minute * 60; const date = new Date(unixSeconds * 1000); ``` **Price-change formula** (`change_W = (spot − ref_W) / ref_W × 100`): * `ref_W` = last candle close at-or-before `now − W` via LOCF. * If the token is younger than the window, `ref_W` clamps to the FIRST priced tick (the launch price), not the creator's first buy. * If the last trade is older than the window → `null` (rendered as a dash), not stale. ## Option D: read reserves on chain (live) [#option-d-read-reserves-on-chain-live] The on-chain MOTO/WETH pair for the current phase is the one `GET /config` serves as `motoUniswapPair`, and that is what this example reads. It runs as written with your own mainnet RPC endpoint in `RPC_URL`: ```ts import { createPublicClient, http, parseAbi } from "viem"; const API = "https://api.motoswap.org"; const RPC_URL = process.env.RPC_URL!; // your own Ethereum mainnet RPC endpoint const { addresses } = await (await fetch(`${API}/config`)).json(); const client = createPublicClient({ transport: http(RPC_URL) }); const pairAbi = parseAbi([ "function getReserves() view returns (uint112, uint112, uint32)", "function token0() view returns (address)", ]); const pair = addresses.motoUniswapPair; const [r0, r1] = await client.readContract({ address: pair, abi: pairAbi, functionName: "getReserves" }); const token0 = await client.readContract({ address: pair, abi: pairAbi, functionName: "token0" }); const motoIsToken0 = token0.toLowerCase() === addresses.motoToken; const motoReserve = motoIsToken0 ? r0 : r1; const wethReserve = motoIsToken0 ? r1 : r0; // WETH per whole MOTO, scaled by 1e18. Both tokens have 18 decimals. const ethPerMoto = (wethReserve * 10n ** 18n) / motoReserve; console.log(ethPerMoto); // e.g. 1314427199219n = 0.000001314 WETH ``` The same read works on any Motoswap pair. Get the pair from `factory.getPair(tokenA, tokenB)`, with `factory` taken from `GET /config`. A zero `factory` there is unset for the phase, so do not call it. The SDK specifies the same math as `priceFromReserves` (against a stablecoin side) and `bestRoute` (multi-hop over the candidate set from `GET /swap/candidates`, see [`reference/sdk-full-inventory`](/dev/reference/sdk-full-inventory#routingts---multi-hop-swap-routing-and-price-discovery)). The SDK is not published yet, so port the formulas from that page. ## Wire types & precision [#wire-types--precision] * USD values (REST) are **decimal-dollar STRINGS** (e.g. `"394066.852165"`) - parse as a decimal, never `parseInt`. Zero is `"0"`, never `null`; `priceUsd` alone may be `null` when genuinely unpriced. * `ethPrice` and every token amount are integer strings. Parse them with `BigInt`, never `Number`. * `getReserves` (chain) returns **raw base units** as `uint112`. ## When to use which [#when-to-use-which] ### UI [#ui] REST, through your server. Cache the ETag; polls collapse to `304` (free). ### Pre-submit slippage check [#pre-submit-slippage-check] Chain-direct. Reserves may have moved since the quote you presented; re-price live before signing. ### Batch valuation [#batch-valuation] REST `/holdings/tokens` (one call, returns the whole curated set). ### Aggregator / MEV bot [#aggregator--mev-bot] Always chain-direct via multicall; REST is not meant for hot-loop pricing. ## Why the API doesn't drift [#why-the-api-doesnt-drift] Both the REST API and the chain-direct path read the **same reserves**. The API's freshness lag is about one block: the REST value is a snapshot of the reserves your live `getReserves` would have returned a block earlier. # Stake LP in a farm (/dev/guides/stake-lp-into-farm) Launch farming (`VampChef`) ran once, for 14 days at token launch, and has ended. `MasterChef` is deployed on Ethereum but not running: no emission campaign is running and none is planned. The reference below documents both chefs' ABI and reward math as contract reference, not a live program you can stake into. LP already staked in `MasterChef` can be withdrawn at any time; see [Emergency withdraw](#emergency-withdraw-forfeit-rewards). Motoswap has two farm contracts. They share the accrual core and the reward split, and differ on exits and fees. | | Launch farm, ended (`VampChef`) | `MasterChef`, not running | | -------------- | ------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Deployment | Deployed on Ethereum; campaign ended | Deployed on Ethereum; no campaign running and none planned | | What you stake | Uniswap V2 LP tokens (launch farming) | Motoswap LP tokens; no campaign, so nothing earns here | | Emission | Flat: `rewardBudget / DURATION`, one window of 14 days (`DURATION()` on the contract is the source of truth), one campaign ever | No campaign running or planned. Contract math: period `k` emits `initialPeriodReward >> k`. `halvingPeriod` and `numPeriods` are settable state (42 days and 8 at initialize). Read them live. | | Deposit fee | None | Per pool, `depositFeeBps`, capped at `MAX_DEPOSIT_FEE_BPS = 1000` | | Withdraw | Instant, one call | Two calls: `withdraw` queues, `claimWithdrawal` pays after the cooldown | | Reward payout | 50% liquid, 50% streamed over 180 days | Same | | Address | `/config.addresses.vampChef` | `/config.addresses.masterChef` | While `launchPhase` was `vamp`, `GET /config` served the launch farm address under both `vampChef` and `masterChef` (see the [deployment status table](/dev/addresses#deployment-status-on-ethereum-chain-1)). Every pool in `GET /farms` carries a `chef` field naming the contract it lives on, so deposit to that rather than to a key you picked. Use the `VampChef` ABI for the launch farm: `claimWithdrawal`, `WithdrawQueued` and deposit fees do not exist on it. ## Discover the pool ids [#discover-the-pool-ids] ```ts const farms = await fetch("https://api.motoswap.org/farms").then((r) => r.json()); for (const p of farms.pools) { console.log(p.chef, p.pid, p.lpToken, p.allocPoint, p.depositFeeBps, p.aprBps); } ``` The guide's `fetch` calls are simple GETs, which work from any origin. A request that triggers a CORS preflight (a script-set `If-None-Match`, a JSON `POST`) only passes for the Motoswap app origin, so make those server-side. `GET /farms` is unpaginated because the pool set is bounded (one `add()` per pool, hard-capped at `MAX_POOLS = 500`). Every pool has: * `chef` - the farm contract the pool lives on. * `pid` - the on-chain pool id on that chef. * `lpToken` - the staked token. On the launch farm, a Uniswap V2 pair. * `allocPoint` - pool weight (share of emission = `allocPoint / totalAllocPoint`). * `depositFeeBps` - 0-1000 (up to 10%). Always 0 on the launch farm. * `pair` - the pair details for an LP pool; `null` if single-sided. * `aprBps` - the live APR the app shows, in basis points. ## Stake [#stake] Both chefs take the same call. ```ts import { parseAbi } from "viem"; const chefAbi = parseAbi([ "function deposit(uint256 pid, uint256 amount)", "function harvest(uint256 pid)", "function withdraw(uint256 pid, uint256 amount)", "function emergencyWithdraw(uint256 pid)", "function pendingReward(uint256 pid, address user) view returns (uint256)", "function stakedBalance(uint256 pid, address user) view returns (uint256)", ]); const erc20Abi = parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]); // 1. Approve the LP token to the chef. await wallet.writeContract({ address: lpToken, abi: erc20Abi, functionName: "approve", args: [chef, amount] }); // 2. Deposit. `amount` is a bigint in LP-token wei. await wallet.writeContract({ address: chef, abi: chefAbi, functionName: "deposit", args: [pid, amount] }); ``` `deposit` settles first: any pending reward is paid out, then the stake grows. The chef credits the balance that actually arrived, and on `MasterChef` the deposit fee comes off that amount and goes to `feeRecipient`. The `Deposit` event carries the credited amount. ### Depositing for someone else [#depositing-for-someone-else] ```ts await wallet.writeContract({ address: chef, abi: parseAbi(["function depositFor(uint256 pid, uint256 amount, address user)"]), functionName: "depositFor", args: [pid, amount, otherUser], }); ``` A third party can only open a new position. If `user` already has a stake in that pool, the call reverts `Unauthorized()` unless the caller is `user` or the chef's `trustedZapper`. The reason: a deposit settles the target's pending reward, and nobody else should be able to trigger that for you. A third-party call with `amount == 0` returns without doing anything. ## Live pending rewards [#live-pending-rewards] Off-chain APIs are indexer-driven and lag the chain. For live rewards read the chain directly: ```ts const pending = await publicClient.readContract({ address: chef, abi: chefAbi, functionName: "pendingReward", args: [pid, user], }); ``` ## Harvest [#harvest] `harvest(pid)` pays your pending reward without touching your stake. It does exactly what `deposit(pid, 0)` does, under a name a wallet shows correctly. ```ts await wallet.writeContract({ address: chef, abi: chefAbi, functionName: "harvest", args: [pid] }); ``` ## How a reward is paid: the liquid half and the streamed half [#how-a-reward-is-paid-the-liquid-half-and-the-streamed-half] Every payout (`harvest`, and the settle inside `deposit` and `withdraw`) goes through one internal function on both chefs: 1. `vested = amount / 2`, rounded down. 2. `amount - vested` MOTO is transferred to you in the same transaction. An odd wei goes to the liquid half. 3. `vested` MOTO is handed to `RewardVestingEscrow.depositFor(you, vested)`. 4. `RewardPaid(user, pid, amount)` is emitted with the **full** amount, not the liquid half. The escrow keeps one schedule per wallet, 180 days long (`VEST_DURATION`), and folds every new deposit into it: * What the old schedule had already released but you had not claimed is banked. It stays claimable at any time. * What had not released yet, plus the new amount, is re-spread linearly over a fresh 180 days starting at that block. So each harvest restarts the clock on the unreleased tail. Harvesting often does not lose anything, but it pushes the end date out. `vestEndOf(account)` returns the end of the schedule as it stands. ```ts const escrowAbi = parseAbi([ "function claimable(address account) view returns (uint256)", // released + banked, payable now "function vesting(address account) view returns (uint256)", // not released yet "function vestEndOf(address account) view returns (uint64)", // 0 when there is no schedule "function claim() returns (uint256 amount)", ]); await wallet.writeContract({ address: rewardVestingEscrow, abi: escrowAbi, functionName: "claim" }); ``` `claim()` pays the caller only. Claiming does not reset the schedule. On `MasterChef`, if the reward pot cannot cover a payout, the chef pays what is available, emits `RewardShortfall(user, pid, owed, paid)`, and forfeits the unpaid part. The deployed `VampChef` does not have `RewardShortfall` yet (see [watch events](/dev/guides/watch-events)). It does not revert, so your principal can always exit. The budget is pulled up front at `startEmission`, so this is a backstop. ## Withdraw from launch farming (ended) [#withdraw-from-launch-farming-ended] One call. It settles your reward and returns the LP in the same transaction. ```ts await wallet.writeContract({ address: chef, abi: chefAbi, functionName: "withdraw", args: [pid, amount] }); // Emits Withdraw(user, pid, amount) ``` `InsufficientStaked()` if `amount` is zero or more than your stake. ## Withdraw from MasterChef (two-step) [#withdraw-from-masterchef-two-step] `withdraw` queues the exit and `claimWithdrawal` pays it. ```ts const masterChefAbi = parseAbi([ "function withdraw(uint256 pid, uint256 amount)", "function claimWithdrawal(uint256 pid)", "function cooldownWaivedFor(uint256 pid) view returns (bool)", "function userInfo(uint256 pid, address user) view returns (uint256 amount, uint256 rewardDebt, uint256 pendingWithdrawal, uint64 cooldownEnd)", ]); // 1. Queue. Settles rewards, stops the queued amount earning. await wallet.writeContract({ address: masterChef, abi: masterChefAbi, functionName: "withdraw", args: [pid, amount] }); // Emits WithdrawQueued(user, pid, amount, cooldownEnd) // 2. At or after cooldownEnd: await wallet.writeContract({ address: masterChef, abi: masterChefAbi, functionName: "claimWithdrawal", args: [pid] }); // Emits WithdrawClaimed(user, pid, amount) ``` * `cooldownEnd` is `now + 24 hours` (`COOLDOWN`), capped at the campaign's `emissionEnd`. Read it from the event or from `userInfo`. * The cooldown is waived, so `cooldownEnd` is the current block time, when `cooldownWaivedFor(pid)` is true: no campaign is emitting, or the pool (or every pool) has zero allocation. It is still two calls. * One queued withdrawal per `(user, pid)`. While one is queued, `withdraw`, `deposit`, `depositFor` and `harvest` on that pool all revert `CooldownActive()`. Claim it first. * `claimWithdrawal` before `cooldownEnd` reverts `CooldownNotElapsed()`. ### Emergency withdraw (forfeit rewards) [#emergency-withdraw-forfeit-rewards] ```ts await wallet.writeContract({ address: chef, abi: chefAbi, functionName: "emergencyWithdraw", args: [pid] }); ``` Returns your whole active stake at once and forfeits all pending reward (`RewardForfeited`). On `MasterChef` it skips the cooldown for the active stake. An amount you already queued with `withdraw` is not touched and still exits through `claimWithdrawal`. With no campaign running on `MasterChef` there is no reward to forfeit, so this is the plain way out: one call, one signature. The Farm page on motoswap.org uses it for `MasterChef` stakes. ## Events emitted [#events-emitted] ```solidity // Both chefs event Deposit(address indexed user, uint256 indexed pid, uint256 amount); event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount); event RewardPaid(address indexed user, uint256 indexed pid, uint256 amount); // full amount, both halves event RewardForfeited(address indexed user, uint256 indexed pid, uint256 amount); // MasterChef only (VampChef: in the source, not in the deployed implementation yet) event RewardShortfall(address indexed user, uint256 indexed pid, uint256 owed, uint256 paid); // VampChef only event Withdraw(address indexed user, uint256 indexed pid, uint256 amount); // MasterChef only event WithdrawQueued(address indexed user, uint256 indexed pid, uint256 amount, uint64 cooldownEnd); event WithdrawClaimed(address indexed user, uint256 indexed pid, uint256 amount); // RewardVestingEscrow event Deposited(address indexed user, uint256 amount, uint256 scheduleTotal, uint64 start); event Claimed(address indexed user, uint256 amount); ``` ## Emission math [#emission-math] Launch farm: * One campaign, started once with `startEmission(budget, startTime)`. `rewardPerSecond = rewardBudget / DURATION`, flat from `emissionStart` to `emissionEnd`, then zero. `MasterChef` (no campaign is running and none is planned; this is the contract math): * One campaign at a time, started with `startEmission(initialPeriodReward, startTime)` by the owner. A new one can start after the previous one ends. * Period 0 emits `initialPeriodReward`. Period `k` emits `initialPeriodReward >> k`, spread linearly over `campaignHalvingPeriod`. The running campaign's schedule is `campaignHalvingPeriod` and `campaignNumPeriods`; `halvingPeriod` and `numPeriods` configure the next one. * The whole campaign total is pulled from the owner at `startEmission`. Both: * `currentRewardPerSecond()` returns the live rate (0 outside the window). * `emittedByTime(t)` is the cumulative emission up to time `t`, and `emissionBetween(from, to)` the emission in a window. * A pool's share is `allocPoint / totalAllocPoint`. APR, client-side, with every input as a plain number in the same currency: ```ts const SECONDS_PER_YEAR = 31_536_000; const poolRewardPerYear = rewardPerSecond * SECONDS_PER_YEAR * (allocPoint / totalAllocPoint); // in whole MOTO const apr = (poolRewardPerYear * motoPriceUsd) / poolTvlUsd; ``` `GET /farms` already serves this as `aprBps` per pool. ## Common errors [#common-errors] | Error | Chef | Cause | | ----------------------- | ------------ | ------------------------------------------------------------------------------------------- | | `Unauthorized()` | both | `depositFor` top-up of someone else's existing position. | | `InsufficientStaked()` | both | `withdraw` with zero, or more than `stakedBalance`. | | `InvalidPool()` | both | `pid >= poolLength()`. | | `ZeroAddress()` | both | `depositFor` with a zero `user`. | | `CooldownActive()` | `MasterChef` | `deposit`, `depositFor`, `harvest` or `withdraw` while a withdrawal is queued on that pool. | | `CooldownNotElapsed()` | `MasterChef` | `claimWithdrawal` before `cooldownEnd`. | | `NoPendingWithdrawal()` | `MasterChef` | `claimWithdrawal` with nothing queued. | `RewardsExhausted()` is not reachable from a user call. It guards the owner's `recoverUndistributedRewards` only. # Stake MOTO (/dev/guides/stake-moto) `MotoStaking` locks MOTO for one of four durations at a boosted weight and pays multi-token revenue share from the `Collector` (the staking bucket of the protocol fee. The split starts at 2 : 2 : 2 : 1 across staking, treasury, Rakeback, and buyback and burn, which is 0.20%, 0.20%, 0.20% and 0.10% of each swap at launch). `MotoStaking` is deployed on Ethereum at `/config.addresses.motoStaking` (see the [deployment status table](/dev/addresses#deployment-status-on-ethereum-chain-1)). Staking, appending, unstaking and every read on this page work against it. The deployed implementation has no reward tokens registered and its `feeRouter()` is a `0x…dEaD` placeholder, so `rewardTokensLength()` reads 0 and `claimAsMoto` reverts. Both arrive with a contract upgrade. ## Lock options [#lock-options] | `durationOption` | Duration | Boost | | ---------------- | -------- | ----- | | 0 | 90 days | 1x | | 1 | 180 days | 2x | | 2 | 365 days | 4x | | 3 | 730 days | 8x | Read the ladder live from `durations(i)` and `boostBps(i)`. A position's weight is `amount * (BPS + boostBps) / BPS` with `BPS = 10000`, so the multiplier is `1 + boostBps / 10000`: the four tiers read `0`, `10000`, `30000` and `70000` for 1x, 2x, 4x and 8x. Boost is snapshotted when the position opens. Rewards arrive in the quote assets fees were collected in, not as MOTO emissions. ## The essentials [#the-essentials] * `stake(amount, durationOption)` opens a position at the chosen tier. * `appendStake(amount)` adds to an existing position; the remaining lock is **extended by a weighted average** (old money keeps its remaining clock, new money carries a full lock). The lock never shortens, and a dust append never re-locks a matured position. * Principal vests linearly over the lock; `requestUnstake` moves vested principal into a **30-day linear drip** you claim from as it arrives. * Reward claims have no cooldown. Vault metadata is served at `GET /stake/vaults`. Per-user positions and pending rewards are **chain-direct reads**. There is no per-user stake endpoint on the API. ## Reading a position [#reading-a-position] ```ts // One position per wallet. Returns a 7-field tuple. const [amount, original, durationOption, start, boostBps, duration, availableFloor] = await publicClient.readContract({ address: motoStaking, abi: parseAbi([ "function positions(address) view returns (uint256 amount, uint256 original, uint8 durationOption, uint64 start, uint256 boostBps, uint256 duration, uint256 availableFloor)", ]), functionName: "positions", args: [user], }); // Accrued reward for one reward token. const owed = await publicClient.readContract({ address: motoStaking, abi: parseAbi(["function pendingReward(address account, address token) view returns (uint256)"]), functionName: "pendingReward", args: [user, weth], }); ``` `duration` is the **lock length snapshotted at stake time**, and it is what vesting is computed against, not whatever the tier ladder says today. A later change to the ladder therefore cannot retroactively re-vest an open position. `boostBps` is snapshotted for the same reason. There is no `getPositions` and no `pendingClaim` on this contract. If you find either in older material, it is wrong. ## Write surface [#write-surface] * `stake(uint256 amount, uint8 durationOption)` - approve MOTO to `MotoStaking` first. One position per wallet: a second `stake` reverts `PositionExists()`, use `appendStake` * `appendStake(uint256 amount)` - one argument; the tier is inherited from the open position * `requestUnstake(uint256 amount)` * `claimUnstaked()` drains every matured ticket; `claimUnstakedFrom(uint256 maxTickets)` bounds the walk when a wallet has many * `claim(address token)`, the no-arg `claim()` that settles every reward token at once, and `claimAsMoto(address token, uint256 minMotoOut, address[] path, uint256 deadline)` (note the trailing `deadline`). `path` must run `token` to MOTO, the swap goes through the `FeeRouter` and pays the protocol fee, and `minMotoOut` is checked against the MOTO that actually reached you (`BelowMinMotoOut`) Operators: `purgeRewardToken` takes `(address token, uint256 maxResidual)`, with `maxResidual` capped at `MAX_PURGE_RESIDUAL`. In the current source, reward tokens are an owner-curated list (`addRewardToken`), and `notifyReward` reverts `RewardTokenNotApproved(token)` for anything else. `addRewardToken` is not on the deployed implementation yet. It arrives with a contract upgrade. Full function/event surface: [`reference/contracts-full-inventory`](/dev/reference/contracts-full-inventory). # Stake Motocat NFTs (/dev/guides/stake-motocats) `MotocatStakingV3` sits behind a UUPS proxy. The address to call is `addresses.motocatStaking` from `GET /config`, and the collection is `addresses.motocatNft`. A non-zero value there means the contract is deployed. Motoswap runs on Ethereum only for now. The per-contract status for Ethereum is in one place: [deployment status](/dev/addresses#deployment-status-on-ethereum-chain-1). The contract has no pause function and no setter that gates `unstake`. It is a pure escrow with a memory. Deposit Motocats, 1 cat = 1 share, instant unstake, no lockup, no fee, and **no rewards paid by this contract at all**. What it keeps is history: per-wallet and global staked balances checkpointed per block, which two consumers read: * **`PveVault`** sizes every PvE drop against the snapshot at the block before the credit landed, and pays claims there, forever. * **The Points indexer** reads a wallet's staked cat count (staked only; a held cat counts for nothing) at the block before each swap for the Points boost. The boost formula is not published. Earlier deployments carried a streamed reward track (`depositReward`, `pendingReward`, `claim`/`claimAll`, warmup, a 32-token list, pause). All of it is gone. Stake and unstake gas no longer varies with anything global, there is no pause, and the only claim surface is the vault below. ## Stake [#stake] ```ts import { parseAbi } from "viem"; // 1. Approve the collection once (setApprovalForAll). await wallet.writeContract({ address: motocatNft, // both addresses come from GET /config abi: parseAbi(["function setApprovalForAll(address operator, bool approved)"]), functionName: "setApprovalForAll", args: [motocatStaking, true], }); // 2. Stake any number of cats in one transaction. The batch is uncapped by // the contract; per-transaction gas caps it around a couple hundred cats. await wallet.writeContract({ address: motocatStaking, abi: parseAbi(["function stake(uint256[] tokenIds)"]), functionName: "stake", args: [[1n, 42n, 137n]], }); ``` Two gates to know about: * `DirectTransferNotAllowed` - a raw `safeTransferFrom` into the contract reverts. Staking only goes through `stake()`, so every escrowed cat has a recorded staker who can withdraw it. * `SeedingNotClosed` - on a freshly deployed contract, public staking opens only after the team finishes seeding the cats staked on distribution day (a one-way switch, `seedingClosed()`). Unstake is never gated, in any state. ## Unstake [#unstake] ```ts await wallet.writeContract({ address: motocatStaking, abi: parseAbi(["function unstake(uint256[] tokenIds)"]), functionName: "unstake", args: [[1n, 42n]], }); ``` Instant, always. `NotStaker(tokenId)` if you did not stake that specific cat; `UnstakeExceedsStaked` if you list more than you have; `EmptyArray` for `[]`. Unstaking after a PvE credit does not change that credit. Your share was fixed at its snapshot block. ## Read state, current and historical [#read-state-current-and-historical] ```ts const count = await publicClient.readContract({ address: motocatStaking, abi: parseAbi(["function stakedBalanceOf(address wallet) view returns (uint256)"]), functionName: "stakedBalanceOf", args: [user], }); const ids = await publicClient.readContract({ address: motocatStaking, abi: parseAbi(["function stakedTokensOf(address wallet) view returns (uint256[])"]), functionName: "stakedTokensOf", args: [user], }); // The checkpoint reads - the whole point of the contract. "As of the END of // block N." Asking about the current or a future block reverts FutureLookup. const wasStaked = await publicClient.readContract({ address: motocatStaking, abi: parseAbi(["function stakedBalanceOfAt(address wallet, uint256 blockNumber) view returns (uint256)"]), functionName: "stakedBalanceOfAt", args: [user, creditBlock - 1n], }); // totalStakedAt(blockNumber) is the matching global read, and the // denominator every PvE split uses. ``` `isStaked(uint256 tokenId) view returns (bool)` answers for one cat without listing a wallet. `stakedTokensOf` returns the wallet's whole list in one call, so its cost grows with the number of cats the wallet has staked. For a large wallet, prefer `stakedBalanceOf` when you only need the count. A paged read, `stakedTokensOfSlice(address, uint256, uint256)`, exists in the contract's source and is **not on the deployed contract yet**: a call to it reverts. It arrives with a contract upgrade, so do not build on it until then. ## Claim PvE drops (from the vault, not here) [#claim-pve-drops-from-the-vault-not-here] Read `addresses.pveVault` from `/config` and branch on a zero address before calling anything below. Launch PvE proceeds land in `PveVault` and are claimed there. In the app, holders of staked Motocats claim on Motoswap's staking page (`/stake/motocats`) from launch day. One credit per token, ever; your share is fixed at the credit block and never expires. You do not need to still be staked to claim it. ```ts // Argument order: TOKEN first, then wallet. const owed = await publicClient.readContract({ address: pveVault, abi: parseAbi(["function claimable(address token, address wallet) view returns (uint256)"]), functionName: "claimable", args: [token, user], }); // Single token, or any set in one transaction. Entries with nothing to claim // are skipped; the call reverts only if the WHOLE batch yields nothing. await wallet.writeContract({ address: pveVault, abi: parseAbi(["function claimMany(address[] tokens) returns (uint256)"]), functionName: "claimMany", args: [[tokenA, tokenB]], }); ``` The eligibility rule, precisely: the vault reads `stakedBalanceOfAt(wallet, creditBlock - 1)`. Staking in the same block as the credit earns nothing from it (reading the block before stops anyone staking just in time for a credit); staking after it earns nothing from that token and everything from the next one. `PveVault.pending(token)` is escrow waiting to be credited (a launch that landed while nobody was staked), not anyone's claimable amount. Per-wallet truth is `claimable(token, wallet)`. ## Events [#events] ```solidity event Staked(address indexed wallet, uint256[] tokenIds); event Unstaked(address indexed wallet, uint256[] tokenIds); // Distribution-day seed rows. Emitted ALONGSIDE Staked for the same cats, so // indexers that only know Staked keep working; Seeded marks rows where the // wallet did not act (do not score them as user actions). event Seeded(address indexed wallet, uint256[] tokenIds); event SeedingClosed(); // Cross-chain mirror, for future networks. When an outbox is registered, every stake and unstake // pushes the wallet's new staked count (a number, never an NFT) to it. A failed push never reverts the stake: it emits // BroadcastFailed, and anyone can repair it later with rebroadcast(wallet). event Broadcast(address indexed wallet, uint256 stakedCount); event BroadcastFailed(address indexed outbox, address indexed wallet); ``` Index `Staked` and `Unstaked` for balances. `Broadcast` is not a second source of truth for them, and it is not guaranteed to appear at all: with no outbox registered, neither `Broadcast` nor `BroadcastFailed` fires. `outboxCount()` tells you how many are registered. The vault side emits `PveReceived`, `PveCredited(token, amount, totalStaked, creditBlock)`, `PveHeld`, `PveClaimed(token, wallet, amount)`. ## Read via REST [#read-via-rest] ``` GET /motocats/{address} -> the wallet's cat inventory GET /pve/wallet/{address} -> every token credited to this wallet's history, with its credit block and claim status; pair it with claimable() for live amounts ``` ## Common errors [#common-errors] | Error | Cause | | :--------------------------- | :------------------------------------------------ | | `DirectTransferNotAllowed()` | Raw NFT transfer to the contract. Use `stake()`. | | `SeedingNotClosed()` | Public staking before the seed latch closed. | | `NotStaker(tokenId)` | `unstake` for a cat you did not stake. | | `UnstakeExceedsStaked()` | `unstake` more than you have staked. | | `EmptyArray()` | `stake`/`unstake` with `[]`. | | `FutureLookup()` | Checkpoint read at the current or a future block. | | `NothingToClaim()` (vault) | `claim`/`claimMany` with no claimable balance. | # Swap via FeeRouter (/dev/guides/swap-via-fee-router) `FeeRouter` is the swap entrypoint for every outside integrator. It takes the protocol fee (`protocolFeeBps`, read it live) from the quote-asset leg, sends it to the `Collector`, and routes the rest through `MotoSwapRouter02`. `feeRouter`, `router` and `factory` come from `GET /config` at runtime, and the [deployment status table](/dev/addresses#deployment-status-on-ethereum-chain-1) tracks each contract. Treat a zero address there as unset for the current phase and stop rather than sending. ## What the FeeRouter is and is not [#what-the-feerouter-is-and-is-not] | Property | Value | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Swap shape | Exact input only. There is no `swapTokensForExactTokens` or any other exact-output entrypoint. | | Quote view | None. `feeOnOutput(path)` is the only swap-related view. | | Flash swaps | Closed. `MotoSwapPair.swap` reverts `MotoSwap: FLASH_SWAPS_CLOSED` on non-empty `data`. | | Protocol fee | `protocolFeeBps`, owner-set, hard cap `MAX_PROTOCOL_FEE_BPS = 100`. The launch value is `LAUNCH_PROTOCOL_FEE_BPS = 70` (0.70%). With the 0.30% pair fee, a swap pays 1% in total at launch. | | Creator fee | `creatorFeeBps`, owner-set, hard cap `MAX_CREATOR_FEE_BPS = 50`. Charged on top, only when a path endpoint has an active creator. | | Pair fee | `factory.swapFeeBps()`, read live by every pair. Already inside `getAmountsOut`. | ## The swap perimeter [#the-swap-perimeter] `MotoSwapPair.swap` and every swap entrypoint on `MotoSwapRouter02` require `factory.swapAllowed(msg.sender)`. The deploy admits exactly two addresses: the core router and the `FeeRouter`. The core router is on the list only because the `FeeRouter` reaches pairs through it. So for an outside integrator: * Calling `MotoSwapRouter02.swap*` directly reverts `MotoSwapRouter: SWAPPER_NOT_ALLOWED`. * Calling `MotoSwapPair.swap` directly reverts `MotoSwap: SWAPPER_NOT_ALLOWED`. * Every swap goes through the `FeeRouter` and pays the full fee. There is no integrator tier, no fee exemption and no allowlist to apply for. Adding and removing liquidity on `MotoSwapRouter02` is permissionless. Only the swap leg is gated. ## Choosing the right entrypoint [#choosing-the-right-entrypoint] | Trade shape | Entrypoint | Returns | | -------------------------------- | --------------------------------------------------------------------------------------------------- | --------------------------------------- | | Token → token | `swapExactTokensForTokens(amountIn, amountOutMin, path, to, deadline)` | `uint256[] amounts` | | Token → token, Permit2 signature | `swapExactTokensForTokensPermit2(permit, signature, amountIn, amountOutMin, path, to, deadline)` | `uint256[] amounts` | | Token → token, fee-on-transfer | `swapExactTokensForTokensSupportingFeeOnTransferTokens(amountIn, amountOutMin, path, to, deadline)` | `uint256 delivered` | | ETH → token | `swapExactETHForTokens(amountOutMin, path, to, deadline)` payable | `uint256[] amounts` | | ETH → token, fee-on-transfer | `swapExactETHForTokensSupportingFeeOnTransferTokens(amountOutMin, path, to, deadline)` payable | `uint256 delivered` | | Token → ETH | `swapExactTokensForETH(amountIn, amountOutMin, path, to, deadline)` | `uint256[] amounts` | | Token → ETH, Permit2 signature | `swapExactTokensForETHPermit2(permit, signature, amountIn, amountOutMin, path, to, deadline)` | `uint256[] amounts` | | Token → ETH, fee-on-transfer | `swapExactTokensForETHSupportingFeeOnTransferTokens(amountIn, amountOutMin, path, to, deadline)` | `uint256 net` | | Split fill, up to 4 legs, atomic | `swapExactTokensForTokensSplit`, `swapExactETHForTokensSplit`, `swapExactTokensForETHSplit` | `(uint256[] legOuts, uint256 totalOut)` | Rules that hold for all of them: * **Approvals.** Token-in entrypoints pull with `transferFrom`, so approve the `FeeRouter`, not `MotoSwapRouter02`. Only the two `*Permit2` entrypoints accept a signature. The fee-on-transfer and split entrypoints have no Permit2 twin and need a normal allowance. * **Permit2 availability.** Take the Permit2 address from `/config.addresses.permit2`, or read `permit2()` on the `FeeRouter`. A zero address means the signature entrypoints are not available, they revert `Permit2NotSet()`, and the plain approve path applies. * **ETH in.** `msg.value` is the input amount. `path[0]` must be WETH or the call reverts `InvalidPath()`. For the ETH split, `msg.value` must equal the sum of the leg amounts or it reverts `ValueMismatch()`. * **ETH out.** `path[path.length - 1]` must be WETH, else `InvalidPath()`. ETH is sent to `to` with a plain call, so `to` must be able to receive it. * **Deadline.** The `FeeRouter` does not check it. `MotoSwapRouter02` does (`deadline >= block.timestamp`) and reverts `MotoSwapRouter: EXPIRED`. * **`amountOutMin`** is enforced on the net amount `to` receives, after the protocol fee and any creator fee. ## Fee placement rule [#fee-placement-rule] The protocol fee is charged once per swap, always in a quote asset. The quote assets are the entries of `QuoteAssetRegistry` (WETH, USDT, USDC, MOTO). Exactly one side is charged: | Path | Where the fee is taken | `feeOnOutput(path)` | | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | Only the output is a quote asset | Output: swap first, skim the gross output | `true` | | Only the input is a quote asset | Input: skim first, swap the remainder | `false` | | Both endpoints are quote assets | The side with the higher `quoteRank`. A tie, or an unranked registry, charges the output. The deploy ranks `WETH > USDT > USDC > MOTO`. | rank based | | Neither endpoint is a quote asset | At the first quote asset inside the path: leg A swaps into it, the fee is skimmed there, leg B swaps the rest | not used (returns `true`) | | No quote asset anywhere in the path | Reverts `NoQuoteSide()` | - | The ETH entrypoints require a quote endpoint and do not take the mid-path route. That is always satisfied while WETH is a registered quote asset. `feeOnOutput(path)` only looks at `path[0]` and the last element. For a path with no quote endpoint it returns `true` and means nothing. Check the endpoints with `registry.isQuote(token)` first. The creator fee is a second skim from the same quote leg, on the same base amount. For each path endpoint with a non-zero `creatorRegistry.activeCreator(token)`, the router sends `base * creatorFeeBps / 10_000` to the `CreatorFeeVault`. Two registered endpoints pay it twice. Quote assets are never registered. ## Quoting [#quoting] The `FeeRouter` has no `getAmountsOut`. Build the quote from `MotoSwapRouter02.getAmountsOut`, which already includes the pair fee, and apply the `FeeRouter` legs in the same order the contract does. Netting the fee matters. A router quote used as-is is high by the protocol fee, so any slippage tolerance tighter than the fee reverts every time with no market movement at all. The quote function is `quoteExactIn`, in [the quote recipe](/dev/integrations/aggregators#quote) on the aggregators page. That is the only copy in these docs. It reads `router()`, both fee rates and both registries from the `FeeRouter` at one block number, counts the path endpoints with an active creator, picks the fee leg from the table above, and applies the skims on the correct side of `getAmountsOut`. Save that code block as a module, for example `quote-exact-in.ts`, and import it: ```ts import { quoteExactIn } from "./quote-exact-in"; // the code block from the aggregators page const blockNumber = await publicClient.getBlockNumber(); const netOut = await quoteExactIn(publicClient, feeRouter, amountIn, path, blockNumber); ``` Then apply slippage to the net quote: ```ts const minOut = (netOut * (10_000n - slippageBps)) / 10_000n; ``` Read `protocolFeeBps` and `creatorFeeBps` live on every quote. Both are owner-settable state, not constants. For the fee-on-transfer entrypoints the token's own transfer tax comes off as well, and no on-chain view can tell you what it is. Quote as above, then lower `amountOutMin` by the tax you expect. ## Full example: ETH → MOTO [#full-example-eth--moto] ```ts import { createPublicClient, createWalletClient, defineChain, http, parseAbi, parseEther, zeroAddress } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { quoteExactIn } from "./quote-exact-in"; // the code block from the aggregators page, saved as a module const RPC = process.env.RPC_URL!; const CONFIG = await fetch("https://api.motoswap.org/config").then((r) => r.json()); const { feeRouter, weth, motoToken } = CONFIG.addresses; if (feeRouter === zeroAddress) throw new Error("/config serves no FeeRouter address for this phase."); const chain = defineChain({ id: CONFIG.chainId, name: `chain-${CONFIG.chainId}`, nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: [RPC] } }, }); const acct = privateKeyToAccount(process.env.PK as `0x${string}`); const publicClient = createPublicClient({ chain, transport: http(RPC) }); const wallet = createWalletClient({ chain, account: acct, transport: http(RPC) }); const amountIn = parseEther("0.01"); const path = [weth, motoToken]; // 1. Net quote: router getAmountsOut, then the FeeRouter fee legs. const blockNumber = await publicClient.getBlockNumber(); const netOut = await quoteExactIn(publicClient, feeRouter, amountIn, path, blockNumber); // 2. Slippage on the NET quote. 50 bps here. const minOut = (netOut * (10_000n - 50n)) / 10_000n; // 3. Submit. msg.value is the input amount; path[0] must be WETH. const deadline = BigInt(Math.floor(Date.now() / 1000) + 600); const hash = await wallet.writeContract({ address: feeRouter, abi: parseAbi([ "function swapExactETHForTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline) payable returns (uint256[])", ]), functionName: "swapExactETHForTokens", args: [minOut, path, acct.address, deadline], value: amountIn, }); ``` A token-in swap is the same, plus one `approve(feeRouter, amountIn)` on `path[0]` before the call. The guide's `fetch` calls are simple GETs, which work from any origin. A request that triggers a CORS preflight (a script-set `If-None-Match`, a JSON `POST`) only passes for the Motoswap app origin, so make those server-side. ## What the returned `amounts` array means [#what-the-returned-amounts-array-means] Rely on two elements only: `amounts[0]` is the caller's gross input, and the last element is the net amount `to` received. The exact fee is on the `Swap` event. * On a path with a mid-path fee, the array does not chain across the fee hop. If `j` is the index of the first quote asset inside the path, the pair `(amounts[j-1], amounts[j])` is not a swap that happened: `amounts[j]` is leg B's input, after the fee. * `swapExactTokensForETH` and its Permit2 twin fill only `amounts[0]` and the last element. On a path of three or more tokens the interior elements are zero. Do not derive a rate from consecutive entries. * The fee-on-transfer entrypoints return one number: what `to` actually received, measured as a balance delta. ## Split fills [#split-fills] The `Split` entrypoints fill one order across up to `MAX_SPLIT_LEGS = 4` paths in one transaction, all or nothing. Every leg must share `path[0]` and the final token. `minTotalOut` floors the sum of the legs' net outputs. There are no per-leg floors. `/config.splitSwapEnabled` is a Motoswap app setting, not an on-chain switch. It tells the app whether to send a split fill as one `Split` transaction or as one swap per leg. The contract entrypoints have no enable flag and work whatever it says. See [the FeeRouter interface](/dev/integrations/aggregators#the-feerouter-interface). ```ts await wallet.writeContract({ address: feeRouter, abi: parseAbi([ "struct SwapLeg { uint256 amountIn; address[] path; }", "function swapExactTokensForTokensSplit(SwapLeg[] legs, uint256 minTotalOut, address to, uint256 deadline) returns (uint256[] legOuts, uint256 totalOut)", ]), functionName: "swapExactTokensForTokensSplit", args: [ [ { amountIn: half, path: [A, WETH, B] }, // route 1 { amountIn: half, path: [A, USDC, B] }, // route 2 ], minTotalOut, acct.address, deadline, ], }); ``` The token-in splits pull the summed input in one `transferFrom`. Quote each leg with `quoteExactIn` and add the results. Each leg pays exactly the fee the same amount would pay as a single swap. | Revert | Cause | | ---------------------- | --------------------------------------------------------- | | `NoLegs()` | Empty `legs`, or a leg with `amountIn == 0`. | | `TooManyLegs()` | More than 4 legs. | | `LegMismatch()` | A leg's first or last token differs from leg 0. | | `ValueMismatch()` | ETH split: `msg.value` is not the sum of the leg amounts. | | `InsufficientOutput()` | The summed net output is below `minTotalOut`. | ## Events [#events] Each swap emits one `FeeRouter.Swap` and one `MotoSwapPair.Swap` per hop. A split emits one `FeeRouter.Swap` per leg, each with that leg's own fee token, because two legs of the same order can pay in different quote assets. ```solidity // FeeRouter - one per swap, one per leg on a split event Swap( address indexed user, // msg.sender of the FeeRouter call: the PAYER, never `to` address indexed tokenIn, // WETH for the ETH-in entrypoints address indexed tokenOut, // WETH for the ETH-out entrypoints uint256 amountIn, // gross input (realised amount on the fee-on-transfer entrypoints) uint256 amountOut, // net amount delivered to `to` address feeToken, // the quote asset the protocol fee was taken in uint256 feeAmount // protocol fee only; 0 when protocolFeeBps is 0 ); // A creator skim, when a path endpoint has an active creator event CreatorFee(address indexed token, address indexed creator, address quoteAsset, uint256 amount); // MotoSwapPair - one per hop event Swap( address indexed sender, // MotoSwapRouter02 uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to // next pair, the FeeRouter, or the final recipient ); ``` `user` is `msg.sender`. For a wallet swapping for itself that is the trader. For a contract integration it is your contract, whatever `to` you pass. Points, Rakeback and referral attribution are all derived from this field, so a contract that swaps on behalf of its users earns those under its own address. `feeAmount` never includes the creator fee. ## Common errors [#common-errors] | Error | Cause | Fix | | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `MotoSwapRouter: INSUFFICIENT_OUTPUT_AMOUNT` | Input-fee path: router output below `amountOutMin`. On a first try with a fresh quote, you almost certainly did not net the fee. | Quote with [`quoteExactIn`](/dev/integrations/aggregators#quote). | | `InsufficientOutput()` | Output-fee path, the fee-on-transfer entrypoints, and the split `minTotalOut` check. Same cause. | Same. | | `NoQuoteSide()` | No quote asset anywhere in the path (or no quote endpoint on an ETH entrypoint). | Route through WETH, USDC, USDT or MOTO. | | `InvalidPath()` | ETH-in path does not start at WETH, or ETH-out path does not end at WETH. | Fix the path. | | `MotoSwapRouter: EXPIRED` | `deadline < block.timestamp`. Checked by `MotoSwapRouter02`, not the `FeeRouter`. | Use a later deadline. | | `EthTransferFailed()` | ETH-out: `to` rejected the ETH. | Send to an address that can receive ETH. | | `PermitTokenMismatch()` | `permit.permitted.token != path[0]`. | Sign the permit for `path[0]`. | | `Permit2NotSet()` | A `*Permit2` entrypoint was called while `permit2()` is the zero address. | Use the approve path. | | `MotoSwapRouter: SWAPPER_NOT_ALLOWED` | You called `MotoSwapRouter02.swap*` directly. | Call the `FeeRouter`. | | `MotoSwap: SWAPPER_NOT_ALLOWED` | You called `MotoSwapPair.swap` directly. | Call the `FeeRouter`. | | `MotoSwap: FLASH_SWAPS_CLOSED` | `MotoSwapPair.swap` received non-empty `data`. | Flash swaps do not exist on Motoswap. | | `NotDepositor()` (from `CreatorFeeVault`) | A path endpoint is a creator-fee token and the vault does not admit this router. Operator misconfiguration, not a caller error. | Nothing the caller can fix. Do not retry. Route around that token until the call stops reverting. | | `MotoSwap: K` | Constant-product check failed after the pair fee. | Refresh reserves and quote again. | # Watch on-chain events (/dev/guides/watch-events) Two integrator options. Pick based on your existing infra. ## Deployment status [#deployment-status] Status is per contract, and the [deployment status table](/dev/addresses#deployment-status-on-ethereum-chain-1) on the addresses page is the reference. Every address comes from `GET /config`. A zero address means the contract is unset for the current phase. Do not subscribe to a zero address. ## Option A: your own event-log provider (recommended) [#option-a-your-own-event-log-provider-recommended] Any RPC provider that supports `eth_getLogs` / websocket subscriptions works. The relevant contracts and their high-value events: ### The farm, staking and vesting contracts [#the-farm-staking-and-vesting-contracts] | Contract | Event | Meaning | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `VampChef` (`/config.addresses.vampChef`) | `Deposit`, `Withdraw`, `EmergencyWithdraw`, `RewardPaid`, `RewardForfeited`, `RewardShortfall` (not live yet, arrives with a contract upgrade) | Launch farm activity. `Withdraw` is instant here: there is no `WithdrawQueued` on this chef. `RewardShortfall` is in the contract source but not in the deployed implementation. | | `RewardVestingEscrow` | `Deposited`, `Claimed` | The streamed half of every farm harvest. | | Uniswap V2 pair | `Mint`, `Burn`, `Swap`, `Sync`, `Transfer` | The LP tokens the launch farm accepted. See the `feeTo` log order below before you index `Mint`. | | `UniswapZap` | `Zapped` | One-transaction zap into a Uniswap V2 LP. | | `MotoStaking` | `Staked`, `Appended`, `UnstakeRequested`, `Unstaked`, `Claimed`, `ClaimedAsMoto`, `RewardNotified` | MOTO staking activity. | | `MotocatStaking` | `Staked`, `Unstaked`, `Seeded`, `SeedingClosed`, `Broadcast`, `BroadcastFailed` | NFT escrow staking (no reward events; PvE pays from `PveVault`). | ### The DEX contracts [#the-dex-contracts] | Contract | Event | Meaning | | ----------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `FeeRouter` | `Swap` | **Canonical trade event.** One per fee-paying swap (multi-hop = 1 event). `user` is `msg.sender` of the `FeeRouter` call, and `feeAmount` is the exact protocol fee. | | `FeeRouter` | `CreatorFee` | A creator-fee skim on the same quote leg. | | `MotoSwapPair` | `Swap` | Per-hop swap. Emitted N times for an N-hop trade. | | `MotoSwapPair` | `Sync` | Reserves updated. Fires on every `mint`, `burn`, `swap` and `sync`. `skim` does not emit it. | | `MotoSwapPair` | `Mint` / `Burn` | LP added/removed. | | `MotoSwapFactory` | `PairCreated`, `FeeToSet`, `SwapFeeSet` | New pair deployed; protocol LP-fee recipient moved; pair fee changed. | | `MasterChef` | `Deposit`, `WithdrawQueued`, `WithdrawClaimed`, `EmergencyWithdraw`, `RewardPaid`, `RewardShortfall`, `RewardForfeited` | Stakes and withdrawals. No emission campaign is running and none is planned. | | `RakebackV2` | `RootPosted`, `SlicePosted`, `Activated`, `Claimed`, `ClaimedAsMoto`, `Expired`, `Swept` | Weekly epoch roots and the payouts claimed against them. `Claimed` indexes epoch and token only (the claimant `account` is not indexed, so match it in your handler); `ClaimedAsMoto` does index the user. | | `PveVault` | `PveReceived`, `PveCredited`, `PveHeld`, `PveClaimed`, `Forwarded` | PvE drops for staked Motocats. | | `Collector` | `Distributed`, `BucketPaid`, `BucketSkipped`, `BucketHeld`, `HeldReleased`, `HeldForceReleased` | Protocol-fee routing. | | `CreatorFeeVault` | `Accrued`, `Claimed` | Creator fee accrual + payout. | Full event signatures (with `indexed` args): [`reference/addresses-config-events`](/dev/reference/addresses-config-events#5-events---integrator-reference). ### The `feeTo` log order on Uniswap V2 pairs [#the-feeto-log-order-on-uniswap-v2-pairs] Every indexer that follows the launch farming pairs hits this. A `Mint` event carries no recipient: ```solidity event Mint(address indexed sender, uint256 amount0, uint256 amount1); // sender is the router ``` So you find the LP recipient from the LP-token `Transfer` events from the zero address that the same pair emits before the `Mint`, in the same transaction. `mint()` emits up to three of them, always in this order: 1. `_mintFee` runs first. When the factory's `feeTo` is set and fees have accrued, it mints the protocol's LP share: `Transfer(0x0 -> feeTo)`. 2. On a pair's first ever mint only, `_mint(address(0), MINIMUM_LIQUIDITY)` mints the minimum liquidity to the zero address: `Transfer(0x0 -> 0x0, 1000)`. 3. `_mint(to, liquidity)` mints the depositor's LP: `Transfer(0x0 -> to)`. Then the pair emits `Sync` and `Mint`. **The rule: the recipient of a `Mint` is the `to` of the last `Transfer` from the zero address that the same pair emitted before that `Mint`, in the same transaction.** Step 3 always comes last, so this holds in every case. An indexer that takes the first such `Transfer` credits the deposit to `feeTo` whenever step 1 fired. Do not filter out transfers to `feeTo`. The last transfer is never the fee mint, and `feeTo` can add liquidity itself. In that one case a `feeTo` filter drops the real recipient. The Uniswap V2 factory `0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f` returns `feeTo() = 0xf38521f130fcCF29dB1961597bc5d2B60F995f85`, so step 1 fires on the launch farming pairs. Read it yourself rather than trusting this page. Example: transaction `0x751c1e7c6f0257510e478335f0338d013694b7e2e636239bf31b784dfb9b6a71`, pair `0xca7c2771D248dCBe09EABE0CE57A62e18dA178c0` (launch farm pool 9). The pair's logs, in order: | Log index | Event | | --------- | -------------------------------------------------------------------------- | | 613 | `Sync` | | 614 | `Swap` | | 621 | `Transfer(0x0 -> 0xf385…5f85)`, the fee mint to `feeTo` | | 622 | `Transfer(0x0 -> 0x2b9d…0ab0)`, the depositor's LP. This is the recipient. | | 623 | `Sync` | | 624 | `Mint` | You supply your own Ethereum mainnet RPC endpoint. The sample reads it from the `RPC_URL` environment variable and stops with a clear error when it is unset. ```ts import { createPublicClient, http, zeroAddress, type Address, type Hex } from "viem"; import { mainnet } from "viem/chains"; // Your own Ethereum mainnet RPC endpoint. const RPC_URL = process.env.RPC_URL; if (!RPC_URL) throw new Error("Set RPC_URL to your own Ethereum mainnet RPC endpoint."); const client = createPublicClient({ chain: mainnet, transport: http(RPC_URL) }); const TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; const MINT = "0x4c209b5fc8ad50758f13e2e1088ba56a560dff690a1c6fef26394f4c03821c4f"; const topicToAddress = (t: Hex) => (`0x${t.slice(26)}`) as Address; /** LP recipient of every Mint in a transaction: [{ pair, mintLogIndex, recipient, liquidity }]. */ export async function mintRecipients(hash: Hex) { const { logs } = await client.getTransactionReceipt({ hash }); const out: { pair: Address; mintLogIndex: number; recipient: Address; liquidity: bigint }[] = []; logs.forEach((mint, i) => { if (mint.topics[0] !== MINT) return; // Walk BACKWARDS from the Mint. The first zero-address Transfer you meet on this pair is the last one emitted. for (let k = i - 1; k >= 0; k--) { const l = logs[k]; if (l.address.toLowerCase() !== mint.address.toLowerCase()) continue; if (l.topics[0] === MINT) break; // an earlier Mint on the same pair owns everything before it if (l.topics[0] !== TRANSFER || topicToAddress(l.topics[1]!) !== zeroAddress) continue; out.push({ pair: mint.address, mintLogIndex: mint.logIndex, recipient: topicToAddress(l.topics[2]!), liquidity: BigInt(l.data) }); break; } }); return out; } console.log(await mintRecipients("0x751c1e7c6f0257510e478335f0338d013694b7e2e636239bf31b784dfb9b6a71")); // [{ pair: 0xca7c…78c0, mintLogIndex: 624, recipient: 0x2b9d…0ab0, liquidity: 16388034085079n }] ``` `Burn` does not have this problem: it carries `to`. It is still preceded by the same fee mint to `feeTo`, so do not read every zero-address `Transfer` as a user deposit. **`MotoSwapPair` has the same order.** `mint()` calls `_mintFee` and then `_mint(to, liquidity)`, and its `Mint` event has no recipient either. The fee mint only happens while `factory.feeTo()` is non-zero. The deploy script sets it to the zero address, which makes the whole pair fee accrue to LPs, but it is a live setting the factory admin can change, and the factory emits `FeeToSet(previousFeeTo, newFeeTo)` when it does. Apply the same last-transfer rule from day one and a `feeTo` change cannot break your indexer. ### Example: watch `FeeRouter.Swap` [#example-watch-feerouterswap] ```ts import { createPublicClient, http, parseAbiItem } from "viem"; // `chain` is built from `/config.chainId` - see the quickstart. Never hardcode one. const client = createPublicClient({ chain, transport: http(RPC) }); const feeRouter = config.addresses.feeRouter; // from GET /config; a zero address there is unset for the phase const unwatch = client.watchEvent({ address: feeRouter, event: parseAbiItem( "event Swap(address indexed user, address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut, address feeToken, uint256 feeAmount)", ), onLogs: (logs) => { for (const log of logs) { const { user, tokenIn, tokenOut, amountIn, amountOut, feeToken, feeAmount } = log.args; console.log(`${user} swapped ${amountIn} ${tokenIn} → ${amountOut} ${tokenOut} (fee ${feeAmount} ${feeToken})`); } }, }); ``` Historical backfill via `eth_getLogs` (chunked by block range, indexed filters recommended for `user` / `tokenIn` / `tokenOut`). `user` is `msg.sender` of the `FeeRouter` call: the payer, never the `to` the caller passed. A contract that swaps for its users shows up as `user` itself. If you need the wallet behind it, attribute to the transaction sender. ### Split fills [#split-fills] `swap*Split` calls emit **one `FeeRouter.Swap` per leg**. To attribute the combined trade back to a single tx, group by `transactionHash` + `user`. The `amountIn` and `amountOut` per leg are the leg's amounts, not the total. Legs of one order can carry different `feeToken` values. ### Getting `topic0` values [#getting-topic0-values] Compute `keccak256("EventName(type1,type2,...)")`. viem's `toEventSelector` gives it: ```ts import { toEventSelector } from "viem"; toEventSelector("Swap(address,address,address,uint256,uint256,address,uint256)"); // => 0x886b45a07c2ce3f9de6744b942d8023d325ac0b64972104e38bb5f30d1452fc3 - FeeRouter.Swap toEventSelector("Swap(address,uint256,uint256,uint256,uint256,address)"); // => 0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822 - pair Swap ``` Pair `Swap` and FeeRouter `Swap` have DIFFERENT `topic0`: different argument lists. They will never collide. `MotoSwapPair` events share their signatures, and so their `topic0`, with Uniswap V2 pair events, so filter by pair address (from `PairCreated`), never by `topic0` alone. ## Option B: REST feed (`GET /activity`) [#option-b-rest-feed-get-activity] Instead of watching the chain yourself, poll the activity feed: ``` GET /activity?cursor= ``` Returns `{ items, nextCursor, hasMore }`: cursor-paginated events (swaps, LP adds/removes, stakes, harvests) with a normalised shape. Downsides: * Not real-time (\~1 block behind). * The cursor is tied to the filters you passed, so you cannot reuse it for a different query. The guide's `fetch` calls are simple GETs, which work from any origin. A request that triggers a CORS preflight (a script-set `If-None-Match`, a JSON `POST`) only passes for the Motoswap app origin, so make those server-side. Upsides: * Zero infra on your side. * Normalised into a single `activity` record shape. ## Reorgs [#reorgs] Chains reorg (rarely deeper than 1 block). Two mitigations: * **Chain-direct watchers:** wait `k` confirmations before treating an event as final. `k = 2-3` is usually enough. * **REST feed:** the indexer's reorg-safety already rolls back and re-derives from block-numbered raw event tables. `GET /chain/head` returns the current tip (with the head hash folded into the ETag), so a reorg shows up as a new ETag and your next read reflects the canonical chain. ## Retention [#retention] * Chain: as long as your RPC provider keeps history (archive nodes keep all of it). * REST: the API caches ETag-versioned reads; historical windows are bounded (`24h` / `7d` / `30d` for charts; feed is paginated by cursor back to the indexer's earliest indexed block). ## What NOT to do [#what-not-to-do] * **Don't** add up pair `Swap` events for volume. That double-counts multi-hop trades. Use `FeeRouter.Swap` (or the REST feed's `activity` rows, which are already de-duplicated per hop). * **Don't** filter pair `Swap.to` for "the trader". It's a router. Use `FeeRouter.Swap.user`. * **Don't** derive Rakeback from pair `Swap`. The fee is skimmed by `FeeRouter` and only surfaces as `feeAmount` on the FeeRouter event. * **Don't** pair a `Mint` with the first zero-address `Transfer` before it. That one can be the protocol fee mint to `feeTo`. # Zap into LP (single-sided) (/dev/guides/zap-in) A zap takes one token (or ETH), swaps the optimal fraction of it, adds the swap output plus the remainder as balanced liquidity, and refunds the dust. One transaction. The LP lands in your wallet. There are two zap contracts, one per venue. The [deployment status table](/dev/addresses#deployment-status-on-ethereum-chain-1) on the addresses page tracks where each is deployed. | | `UniswapZap` | `MotoSwapZap` | | ---------- | ----------------------------------- | -------------------------------------------------------- | | Deployment | Deployed on Ethereum | Deployed on Ethereum | | Venue | Uniswap V2 pairs | Motoswap pairs | | Swap leg | Uniswap V2 router. No Motoswap fee. | `FeeRouter`. Pays the protocol fee, and any creator fee. | | Address | `/config.addresses.uniswapZap` | `/config.addresses.zap` | **Constraints, both zaps:** * Fee-on-transfer tokens are **explicitly rejected** (`FeeOnTransferNotSupported`). The zap compares balance deltas to nominal amounts on both legs. * The pair MUST exist (`PairNotFound`). `UniswapZap` also reverts `EmptyPair` when the pair has no reserves. * No recipient argument anywhere. LP and refunds go to `msg.sender`. * Neither zap holds funds between calls, and neither has an owner. ## UniswapZap (deployed) [#uniswapzap-deployed] ```solidity function getSwapAmount(uint256 amountIn, uint256 reserveIn) pure returns (uint256); function zapInToken(address tokenIn, uint256 amountIn, address otherToken, uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline) returns (uint256 lpAmount); function zapInETH(address otherToken, uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline) payable returns (uint256 lpAmount); event Zapped(address indexed sender, address indexed pair, address tokenIn, uint256 amountIn, uint256 swapAmount, uint256 lpAmount); ``` ### ETH into Uniswap V2 LP [#eth-into-uniswap-v2-lp] ```ts import { createPublicClient, createWalletClient, http, parseAbi, parseEther } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { mainnet } from "viem/chains"; const RPC = process.env.RPC_URL!; const acct = privateKeyToAccount(process.env.PK as `0x${string}`); const publicClient = createPublicClient({ chain: mainnet, transport: http(RPC) }); const wallet = createWalletClient({ chain: mainnet, account: acct, transport: http(RPC) }); const cfg = await fetch("https://api.motoswap.org/config").then((r) => r.json()); const { uniswapZap, uniswapV2Factory, weth, motoToken } = cfg.addresses; const zapAbi = parseAbi([ "function getSwapAmount(uint256 amountIn, uint256 reserveIn) pure returns (uint256)", "function zapInETH(address otherToken, uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline) payable returns (uint256 lpAmount)", ]); const pairAbi = parseAbi([ "function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)", "function token0() view returns (address)", "function totalSupply() view returns (uint256)", ]); const amountIn = parseEther("0.05"); const otherToken = motoToken; // 1. The pair. const pair = await publicClient.readContract({ address: uniswapV2Factory, abi: parseAbi(["function getPair(address, address) view returns (address)"]), functionName: "getPair", args: [weth, otherToken], }); // 2. Preview the split so the floors mean something. const [[r0, r1], token0, totalSupply] = await Promise.all([ publicClient.readContract({ address: pair, abi: pairAbi, functionName: "getReserves" }), publicClient.readContract({ address: pair, abi: pairAbi, functionName: "token0" }), publicClient.readContract({ address: pair, abi: pairAbi, functionName: "totalSupply" }), ]); const [reserveIn, reserveOut] = token0.toLowerCase() === weth.toLowerCase() ? [r0, r1] : [r1, r0]; const swapAmount = await publicClient.readContract({ address: uniswapZap, abi: zapAbi, functionName: "getSwapAmount", args: [amountIn, reserveIn], }); const swapOut = (swapAmount * 997n * reserveOut) / (reserveIn * 1000n + swapAmount * 997n); // Uniswap V2, 0.30% const lpExpected = ((amountIn - swapAmount) * totalSupply) / (reserveIn + swapAmount); // 3. Floors at 1% slippage, then send. msg.value is the input. const floor = (x: bigint, bps: bigint) => (x * (10_000n - bps)) / 10_000n; const deadline = BigInt(Math.floor(Date.now() / 1000) + 600); await wallet.writeContract({ address: uniswapZap, abi: zapAbi, functionName: "zapInETH", args: [otherToken, floor(swapOut, 100n), 0n, 0n, floor(lpExpected, 100n), deadline], value: amountIn, }); ``` The guide's `fetch` calls are simple GETs, which work from any origin. A request that triggers a CORS preflight (a script-set `If-None-Match`, a JSON `POST`) only passes for the Motoswap app origin, so make those server-side. For a token input, approve `tokenIn` to the zap first and call `zapInToken(tokenIn, amountIn, otherToken, ...)`. ## MotoSwapZap [#motoswapzap] ```solidity function getSwapAmount(uint256 amountIn, uint256 reserveIn, address tokenIn, address otherToken) view returns (uint256); function creatorSkimBps(address tokenIn, address otherToken) view returns (uint256 bps); function zapInToken(address tokenIn, uint256 amountIn, address otherToken, uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline) returns (uint256 lpAmount); function zapInETH(address otherToken, uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline) payable returns (uint256 lpAmount); ``` ```ts import { parseAbi } from "viem"; // 1. Approve tokenIn to the ZAP (not to the router or the FeeRouter). await wallet.writeContract({ address: tokenIn, abi: parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]), functionName: "approve", args: [zap, amountIn], }); // 2. Ask the zap for the split it will use. It reads the live fees itself. const swapAmount = await publicClient.readContract({ address: zap, abi: parseAbi(["function getSwapAmount(uint256 amountIn, uint256 reserveIn, address tokenIn, address otherToken) view returns (uint256)"]), functionName: "getSwapAmount", args: [amountIn, reserveIn, tokenIn, otherToken], }); // 3. Quote the swap leg through the FeeRouter rules, then set the floors. // quoteExactIn is the function from the aggregators page. const blockNumber = await publicClient.getBlockNumber(); const swapOut = await quoteExactIn(publicClient, feeRouter, swapAmount, [tokenIn, otherToken], blockNumber); // 4. Execute. await wallet.writeContract({ address: zap, abi: parseAbi([ "function zapInToken(address tokenIn, uint256 amountIn, address otherToken, uint256 amountSwapOutMin, uint256 amountTokenMin, uint256 amountOtherMin, uint256 amountLpMin, uint256 deadline) returns (uint256 lpAmount)", ]), functionName: "zapInToken", args: [tokenIn, amountIn, otherToken, amountSwapOutMin, amountTokenMin, amountOtherMin, amountLpMin, deadline], }); ``` The swap leg goes through the `FeeRouter`, so it pays the protocol fee and any creator fee on the quote side. See [the quote recipe](/dev/integrations/aggregators#quote) for the quote function. The zap ends at adding liquidity. Liquidity providers on Motoswap earn the 0.30% trading fee on every swap in their pool. ## Slippage floors [#slippage-floors] ### `amountSwapOutMin` [#amountswapoutmin] Floor on the swap-leg output. ### `amountTokenMin` / `amountOtherMin` [#amounttokenmin--amountothermin] Floors passed to `addLiquidity`. Both are opt-in; `0n` accepts whatever ratio the pool takes. ### `amountLpMin` [#amountlpmin] Floor on the final LP minted. **Set this.** It protects the whole zap from being sandwiched by a bot. ## The optimal-swap formula [#the-optimal-swap-formula] For an input amount `A` and pool reserve `R` of the input token, the amount to swap so that the remainder and the output add as balanced liquidity is: ``` f = 10_000 - total fee on the swap leg, in bps fp = f + 10_000 toSwap = (sqrt(R * (R * fp^2 + A * 4 * f * 10_000)) - R * fp) / (2 * f) ``` * `UniswapZap`: `f = 9_970`, the fixed Uniswap V2 0.30%. * `MotoSwapZap`: `f = 10_000 - factory.swapFeeBps() - feeRouter.protocolFeeBps() - creatorSkimBps(tokenIn, otherToken)`, all three read live inside the call. Call `getSwapAmount` on the zap instead of re-implementing this. It is the number the zap will use. ## Common errors [#common-errors] | Error | Zap | Cause | Fix | | ----------------------------- | ------------ | ------------------------------------------- | --------------------------------------------------- | | `PairNotFound()` | both | No pair for `(tokenIn, otherToken)`. | Create it and seed it with a normal `addLiquidity`. | | `EmptyPair()` | `UniswapZap` | The pair exists with zero reserves. | Seed it with a normal `addLiquidity`. | | `IdenticalTokens()` | both | `tokenIn == otherToken`. | Fix your params. | | `ZeroAmount()` | `UniswapZap` | `amountIn` or `msg.value` is zero. | Send an amount. | | `Expired()` | both | `block.timestamp > deadline`. | Later deadline. | | `FeeOnTransferNotSupported()` | both | One of the tokens taxes transfers. | Add liquidity through the router directly. | | `InsufficientLp()` | both | Final LP \< `amountLpMin`. | Re-quote, or widen the floor. | | `EthTransferFailed()` | both | The ETH dust refund to `msg.sender` failed. | Call from an address that can receive ETH. | # Home base (/user/why/home-base) [Paid players come back](./the-flywheel.mdx) is one house on one floor. This chapter is where the house lives, and how many floors it runs. ## The floor with the money [#the-floor-with-the-money] Ethereum mainnet looks quiet if you only watch the launch charts. It is not quiet. It holds the deepest pools, the largest holders, and the communities that have survived every cycle. Depth is where volume settles, and volume is the house's whole business, so the house has to keep its truth on the floor where the money already is. That is why Motoswap's home is mainnet. MOTO's supply lives there. When Motoswap reaches other networks, you will trade a bridged MOTO there, backed one to one by MOTO held on Ethereum. Motocats live and stake there. And [moto.fun](../moto-fun.mdx) runs there, beside the DEX, on the one chain where a launchpad has never had a home. ## One house, every floor [#one-house-every-floor] The second half of the design is that the same simple house is built to run on every EVM chain, with the floors talking to each other. There is no separate product per network, with its own rules and website. One set of rules, every chain that speaks the EVM's language, and messages passing between them where a floor needs to know what happened on another. Motoswap opens on Ethereum. As more floors open, this is what the design means at the table: * Your Motocats stake once, on Ethereum, and the Points boost will follow you to every floor you trade on. * Points will stay one season across every network. A swap anywhere counts toward the same SZN 1 airdrop. * [Rakeback](../rakeback.mdx) will pay you on the floor you played. Fees you pay on a network will come back to you from that network's pot, in cash, with nothing to bridge. * Swaps, launches, and MOTO staking will work the same way on every network. A product built for one chain cannot do this; the chain is the product. Motoswap is built for the EVM, and the EVM is a family of chains that share a language. ## Why Uniswap will not follow [#why-uniswap-will-not-follow] Uniswap could have done any of this. It has the liquidity, the users, and a deployment on every chain that matters. It has not, and it will not, because its house keeps the rake for the tables. A house built for market makers does not need to pay its players or its dealers, and it cannot be retrofitted to. [Uniswap built the NYSE and nobody came](./the-uniswap-problem.mdx) has the mechanics. Motoswap opens with the players paid, on the floor with the deepest liquidity, and is built to carry the same rules to every floor the EVM reaches. # Nobody paid the players (/user/why/the-fix) Two chapters ago the players were the revenue. One chapter ago the dealers got paid and the players still footed the bill. Nobody, on any chain, had ever paid the people doing the trading. Motoswap is a DEX on Ethereum built from the two halves that work: Uniswap v2's math and PumpSwap's plumbing. The math is the constant-product pool, the design that carried SHIB and PEPE. The plumbing is the PumpSwap decision: fees come out of the trade in cash, the quote asset, and leave the pool, so they can be routed to people instead of dissolving into reserves. Then Motoswap does the thing neither of them did. It routes the cash to you. ## The players get paid [#the-players-get-paid] [Rakeback](../rakeback.mdx) needs nothing staked and nothing supplied: a fifth of the fee, back to the person who paid it, every trade. The rest of the casino sits on the same plumbing: [Points](../points.mdx) toward the SZN 1 airdrop, [Motocats](../earn.mdx#stake-motocats) multiplying them, [MOTO staking](../earn.mdx#stake-moto) paid in the actual fee assets, and [moto.fun](../moto-fun.mdx), a bonding curve that opens a live market in seconds. The full fee split and the venue-by-venue comparison live on [The 1%](../fee.mdx). moto.fun also pays creators the way Solana does: [creator fees](/fun/creator-fees) of `0.5%` of every trade while your coin is on the curve and `0.3%` after it graduates, in cash, claimable without selling. ## Why this counts as a challenge [#why-this-counts-as-a-challenge] Nobody has seriously gone after Uniswap since SushiSwap pulled its liquidity away in the summer of 2020. Motoswap's launch farming, in September 2026, paid Uniswap LPs in MOTO before the DEX opened. Everything since has been forks, aggregators, and interfaces, all of them accepting Uniswap's incentive model as a given and competing on everything except the thing that matters. Motoswap competes on the thing that matters. Same math, opposite loyalties: the fee is treated as the players' money, and the model is aimed at the people Uniswap never built for, on the chain Uniswap calls home. Ethereum mainnet is the home base because the deepest liquidity lives there, and depth is where volume settles. Other high-volume EVM chains come after. You already pay casino prices everywhere. [What you pay elsewhere](../fee.mdx#what-you-pay-elsewhere) puts the numbers side by side; the short version is that no other venue pays you back in cash, and on Motoswap a fifth of the fee comes back to you and the rest funds the game. You were always the house's revenue. Here, you are also on the payroll. Motoswap is the casino Ethereum was always supposed to be, with the players paid. The machine this builds over time is the next chapter: [Paid players come back](./the-flywheel.mdx). # Paid players come back (/user/why/the-flywheel) [The last chapter](./the-fix.mdx) put the players on the payroll. This one is what a house that pays its players does over time. ## Liquidity that cannot leave [#liquidity-that-cannot-leave] Every moto.fun coin that graduates opens two pools, TOKEN/ETH and TOKEN/MOTO, and the graduation burns the LP. Nobody holds the claim ticket, so nobody can ever pull the liquidity. Not the creator, not Motoswap, not next month's farm come fishing. It sits under the token forever. Burned LP does two jobs at once. It welds the token's home market in place, depth that can never migrate, so the trading settles here and every trade pays [the 1%](../fee.mdx) that pays the players. And it takes MOTO off the market for good, because half the TOKEN/MOTO pool is MOTO. Demand for new tokens is demand for MOTO. Mechanically, not as a narrative. ## One turn of the wheel [#one-turn-of-the-wheel] The payout leg is the whole trick: [Rakeback](../rakeback.mdx) back to the trader in cash, the staker cut to [MOTO stakers](../earn.mdx#stake-moto) in real fee assets, `0.10%` buying MOTO off the market and burning it. Paid traders trade more, paid stakers stay staked, and [Points](../points.mdx) count all of it toward the SZN 1 airdrop. Activity is what deployers shop for. The next launch lands here, and the wheel gains mass. Trades on the MOTO side of a pair pay their fees in MOTO, so stakers and the buyback and burn partly collect in MOTO itself. ## What it is not [#what-it-is-not] A flywheel is momentum, not a floor. When launches slow, every arrow above weakens at once; a machine like this amplifies both directions. What never unwinds is the liquidity already burned: MOTO under a market whose pool tokens are burned stays there whether the wheel is spinning or not. Where the machine lives, and how it reaches every other chain, is the last chapter: [Home base](./home-base.mdx). # You are the house's revenue (/user/why/the-question) A casino has a house and it has players. The house takes a cut of every hand, and the only question that matters is where the cut goes. In every DEX you have ever used it went to the house and to the people who supplied the tables. It never went to you. You were the revenue. You know this in your hands. On Solana a token launches, runs, and the whole casino moves with it. On Ethereum and its L2s the same energy never quite arrives. The standard explanation is that EVM never got its pump.fun. It sounds right and it explains nothing. Base has had cheap blocks for years. It has the users, the wallets, and the liquidity. If a launchpad were the missing piece, one would have won by now. It is 2026 and Ethereum mainnet, the chain with the deepest liquidity in crypto, still has no main launchpad. Nobody can name one, and that is not an accident. Why does the model that runs Solana keep failing to transplant onto EVM chains that have every other ingredient? The answer is not the launchpad. A launchpad is the front door; the economics happen at the DEX underneath. From mainnet to Base to Robinhood Chain, that DEX is Uniswap. And Uniswap's incentive model was built for institutions and liquidity providers, not for the people actually trading. That is the claim these chapters earn. The next one opens the books on the house every EVM chain plays in: [Uniswap built the NYSE and nobody came](./the-uniswap-problem.mdx). # Uniswap built the NYSE and nobody came (/user/why/the-uniswap-problem) Uniswap is the EVM's default DEX, and it is the cleanest example of a house built for the tables rather than the players. It was built for liquidity providers and the funds that make markets for them. Look at what the fee does in each version and you can see who it was built for. The crowd that showed up was never the one it was built for. Retail came, the institutions did not, and the fee kept flowing to the tables anyway. When Robinhood launched its own chain, Uniswap was aboard as the primary AMM from day one, and within two weeks the chain was top five by DEX volume. Whatever Uniswap's incentives are, they are the incentives of the entire EVM. So look at them closely. ## v2: fees the pool keeps [#v2-fees-the-pool-keeps] Uniswap v2 charges 0.30% per swap, taken inside the pool, in both tokens of the pair. The fee compounds into the reserves. A liquidity provider realizes it one way only: by removing liquidity. For a memecoin, that design works in exactly one case: when the LP is burned or locked. Then nobody can pull the fees out, the pool gets deeper as volume flows, and price and liquidity grow together. It is the case that matters. SHIB launched on a v2 pair in August 2020. PEPE launched in April 2023 with its LP tokens burned. Notice what the token's creator gets in this model: nothing. The fees belong to the pool. If the dev wants to get paid, the dev sells the token, on their own chart, in front of everyone. ## v3: fees for whoever can leave [#v3-fees-for-whoever-can-leave] v3 rebuilt liquidity around positions. An LP position became an NFT, its fees became collectable at any time without touching the principal, and concentrated ranges made single-sided liquidity possible. For professional market makers, all upside. For a memecoin, each change points the wrong way. Fees that used to deepen the pool now get extracted as they accrue, so the pool never thickens as the token grows. Single-sided ranges let anyone park a wall of supply across a price range, which is a sell wall by construction. And fees accrue in whichever token the trade pays, so every sell pays its fee in the memecoin itself. The record matches the mechanics. No memecoin launched on a v3 pool on Ethereum mainnet has reached a billion dollars. ## A square peg in a round hole [#a-square-peg-in-a-round-hole] The peg is the Solana creator fee: a cash cut of every trade, paid to the person who made the token. The hole is Uniswap v3. EVM launchpads want to offer the same deal, so they force the one into the other: they build the creator fee out of the only material Uniswap gives them, a locked v3 position whose fees route to the creator. The next chapter covers how Solana does it natively. This section is what happens when you hammer. That construction inherits everything above. The "creator fee" accrues half in the memecoin, the pool bleeds instead of deepening, and the launch itself is a single-sided position, the sell wall as a business model. Uniswap, for its part, cut its interface fee to zero by governance vote in December 2025. Trading on Uniswap costs the pool fee plus gas, and returns nothing: no rakeback, no creator fees, no launch surface. The cheapest possible version of a model built for someone other than you. That is the house on every EVM chain today: the rake stays with the tables, the creator sells on their own chart to get paid, and the player is the revenue. Now look at the one place that changed half of it: [Solana paid the dealers](./what-solana-figured-out.mdx). # Solana paid the dealers (/user/why/what-solana-figured-out) Solana is the one floor where the house changed its rules, and it changed them for the dealers. The people who make the tokens got paid. Here is the mechanism. PumpSwap, the DEX pump.fun tokens graduate onto, runs the same constant-product math as Uniswap v2. Same curve, same pools. The difference is one plumbing decision: the protocol cut and the creator cut come out of the cash side of the trade and leave the pool. Follow what that does to the deployer's life. A pump.fun creator earns a cut of every trade on their token, in cash, and getting paid never touches the chart. That is the flywheel, and it is an incentive loop, not a technology: deployers get paid in cash, so they launch where the cash is, so the launches are on Solana, so the volume is on Solana, so the traders are on Solana, so the next deployer launches there too. Pump.fun made cash creator fees the standard on Solana, and that standard has never been copied on top of Uniswap. Now the other half of the ledger, because Solana solved the deployer's side and left yours alone. Buy a pump.fun token through Phantom's in-wallet swap and you pay Phantom's flat 0.85% app fee on top of a route that already includes the pool's own fees. The stack runs fees on fees on fees, and none of it returns to the person generating the volume. So the scoreboard reads: Solana pays the house and the dealers in cash, and the players pay for all of it. EVM pays nobody in cash at all. Nobody, anywhere, pays the players. That open seat is the point of the next chapter: [Nobody paid the players](./the-fix.mdx).