AXIS Docs

DevelopersContract reference

Functions

Every public entry point of the AXIS contract with its signature, arguments, authorization, events and errors.

The contract source is at github.com/axis-markets/orderbook (version 0.6.0, soroban-sdk 28). The Testnet deployment is CA6P26K4QNNIMTYP22ILTSCXQQDEKNWJIZJPC34YEZYOLBNT7YUPX7XS. See Networks and deployments for the other addresses.

Conventions used on this page:

  • Signatures omit the Env argument that every Soroban entry point receives.
  • Amounts are i128 integer base units of a token. Prices are i128 fixed point numbers with 18 decimals: buying base units per 1 selling base unit, times 10^18. See price and amount units.
  • Order IDs are u128 values derived from the owner and a client nonce. See order ID derivation.
  • Every state-changing entry point extends the contract instance and code TTL to 3 days when fewer than 3 days are left. There is no lifetime entry point: keepers extend both with the ExtendFootprintTTL operation, see Contract lifetime.
  • Error codes link to the error table. A failed call reverts everything it did, including in-call approvals and events.

At a glance#

Function Who calls it Changes state While frozen
trade trader, signs yes fails with 730
swap trader, signs yes fails with 730
crossfill anyone, signs as trader yes fails with 730
update order owner, signs yes removals and approvals only
subsidize sponsor, signs yes fails with 730
requote anyone, no signature yes fails with 730
order, market, config, frozen anyone no, except that market extends the market entry TTL works
freeze, delegate, set_oracle, set_floor, set_listing_min_days, set_ledger_time safety admin yes works

There is no upgrade function, no fee switch and no admin access to orders or funds. See Safety admin and invariants.

Trading#

trade#

fn trade(
    direction: TradeDirection,
    kind: OrderKind,
    trader: Address,
    amount: i128,
    selling: Address,
    buying: Address,
    price: i128,
    orders: Vec<u128>,
    nonce: u64,
    expires: u64,
    approve: Option<Approval>,
) -> (i128, i128, Option<u128>)

Matches the trader against the listed maker orders, in the given order, and settles every fill: the trader pays each maker directly, the makers' assets go to the contract, and the contract forwards everything bought to the trader in one transfer at the end of the call. With kind = Limit the unfilled remainder is stored as a new order owned by the trader. Fill executes what it can and stops (immediate-or-cancel). FillOrKill fails unless the whole amount executes.

Name Type Meaning
direction TradeDirection Sell: amount is the exact amount of selling to spend. Buy: amount is the amount of buying to acquire.
kind OrderKind Limit, Fill or FillOrKill.
trader Address Account that pays and receives. Owner of the remainder order.
amount i128 Base units of selling (Sell) or buying (Buy). Must be positive.
selling Address Token the trader pays with.
buying Address Token the trader receives. Must differ from selling.
price i128 Limit price, 18 decimals, 1 to 10^36. Sell: minimum buying per 1 selling. Buy: maximum selling per 1 buying.
orders Vec<u128> Maker order IDs to match, in this order. May be empty.
nonce u64 Client nonce for the ID of the remainder order. Used only when an order is created.
expires u64 Expiration of the remainder order, UNIX seconds, 0 for none. Checked for Limit only.
approve Option<Approval> Allowance granted to the contract before trading, normally on selling.

Returns (sold, bought, id): the amount of selling the trader paid, the amount of buying the trader received, and the ID of the order created for the remainder (None when no order was created).

Authorization: trader signs the root call. With approve, the trader also signs the approve sub-invocation on the token. Makers sign nothing: they are paid and charged through their standing allowances.

When frozen: fails with 730 for every kind.

Events: one trade per fill, one skip per listed order whose maker could not settle, and new when a remainder order is stored. The tokens add their own transfer events (two per maker, plus one for the forward to the trader) and an approve event for an in-call approval.

Errors:

Notes:

  • Missing, expired and duplicate IDs, orders of another pair, orders priced worse than the limit (order.price <= floor(10^36 / price) for Sell, order.price <= price for Buy) and fills that round to zero are skipped silently. A maker who cannot settle is skipped with a skip event, see Settlement and allowances.
  • The taker's output is rounded down and the cost rounded up, and the limit is compared with the order prices, not with the rounded amounts. See Rounding and the taker's limit.
  • A Limit trade checks the expiration, the market and the minimum order value of the whole amount before matching. Fill and FillOrKill need no market and ignore nonce and expires. A FillOrKill Sell counts as executed when the leftover cannot buy one base unit of buying at the highest order price it filled.
  • A Buy remainder is stored sell-equivalent (see Sell-equivalent Buy remainders), and a dust remainder is not stored.
  • Self-trades are allowed by design: the trader decides whether to list their own orders. The JS client wraps this function as sell and buy (see Contract client).

swap#

fn swap(
    direction: TradeDirection,
    trader: Address,
    selling: Address,
    selling_amount: i128,
    buying_amount: i128,
    path: Vec<TradeStep>,
    approve: Option<Approval>,
) -> (i128, i128)

Trades along a route of one or more markets with all-or-nothing bounds. The contract plans every hop read-only first, then executes front to back with exact amounts. Every hop's output passes through the contract, which forwards the last hop's output to the trader in one transfer. It holds assets only inside the call.

Name Type Meaning
direction TradeDirection Sell: exact input. Buy: exact output.
trader Address Pays the first hop and receives the output of the last hop.
selling Address Token the trader pays with.
selling_amount i128 Sell: amount to sell. Buy: maximum amount to spend. Must be positive.
buying_amount i128 Sell: minimum amount to receive. Buy: exact amount to receive. Must be positive.
path Vec<TradeStep> Hops in order. Each step names the asset bought at that hop and the maker orders to match. The first hop sells selling, every later hop sells the previous step's asset.
approve Option<Approval> Allowance granted to the contract before trading, normally on selling.

Returns (sold, bought): the amount of selling the trader paid and the amount of the last step's asset the trader received.

Authorization: trader signs the root call, plus the approve sub-invocation when approve is set.

When frozen: fails with 730.

Events: per hop, one trade per fill and one skip per listed order whose maker has no backing or cannot receive. Then one swap for the whole route. The taker of the trade events is the trader on the first hop and the AXIS contract on later hops. The tokens add two transfer events per maker and one for the forward of the output to the trader.

Errors:

  • 730 Frozen: the contract is frozen.
  • 704 InvalidMatch: path is empty, or selling_amount or buying_amount is not positive.
  • 712 IntermediaryCannotReceive: the contract cannot hold an asset of the path, because its issuer requires authorization and has not authorized the contract. Checked for every hop before matching.
  • 709 NotFilled: the route cannot meet the bounds. A hop yields nothing or leaves usable input, the Sell output is below buying_amount, the Buy cost exceeds selling_amount, or execution differs from the plan.
  • 740 Overflow: a price calculation does not fit i128.
  • A token error when the trader cannot pay or a maker cannot be credited (a full trustline), a maker transfer fails, or the trader cannot receive the output. swap is strict and has no 708 pre-check (see token errors).

Notes:

  • There is no price limit per hop. Slippage is bounded by selling_amount and buying_amount on the whole route.
  • With Sell, input the route cannot use (worth less than one base unit of a hop's output) stays with the trader, so sold can be slightly below selling_amount. With Buy, the trader keeps whatever the route does not spend.
  • Makers without backing are skipped during planning, so a route can list a fallback order after an unbacked one. A maker transfer that fails during execution fails the whole call instead of being skipped.
  • swap needs no market for any pair. See Swaps for the planning and execution passes.

crossfill#

fn crossfill(trader: Address, taker_order_id: u128, orders: Vec<u128>) -> (i128, i128, i128)

Fills an existing order (the taker order) against cheaper orders on the opposite side. The owner of the taker order pays the makers and is paid exactly at the taker order's own limit price. The crossed spread goes to the caller.

Name Type Meaning
trader Address Caller. Receives the surplus. Needs no funds but must be able to receive the taker order's buying asset.
taker_order_id u128 Live order that acts as the taker.
orders Vec<u128> Maker orders that sell the taker order's buying asset for its selling asset.

Returns (paid, received, surplus): the amount of the taker order's selling asset its owner paid to the makers, the amount of its buying asset the makers delivered, and the part of it paid to trader. The owner receives ceil(paid * price / 10^18) at the taker order's price, which is received - surplus.

Authorization: trader signs the root call only. The owner of the taker order signs nothing: their payment uses their standing allowance, like any maker.

When frozen: fails with 730.

Events: one trade per maker fill (taker = owner of the taker order), one skip per listed order whose maker could not settle, then one trade for the taker order itself with taker = trader and reversed asset topics. The tokens add two transfer events per maker, then one from the contract to the owner and one to trader for a positive surplus. When the owner cannot back the taker order or cannot receive, the call emits a single skip with taker_order_id and returns (0, 0, 0) without moving anything.

Errors:

  • 730 Frozen: the contract is frozen.
  • 710 OrderNotFound: the taker order does not exist or has expired.
  • 712 IntermediaryCannotReceive: the contract cannot hold the taker order's buying asset, because its issuer requires authorization and has not authorized the contract.
  • 708 CannotReceive: trader cannot receive the taker order's buying asset.
  • 740 Overflow: a price calculation does not fit i128.
  • A token error when the owner's payment to a maker fails (the owner cannot pay, or the maker cannot be credited although authorized), never the caller's fault, or when the payout to the owner or the surplus to trader does not fit their trustline (see token errors).

Notes:

  • The owner's budget is min(order amount, balance, allowance) in the taker order's selling asset.
  • Makers are matched like a Sell taker at the taker order's price. A fill that would leave the owner short because of rounding is skipped silently.
  • The call returns (0, 0, 0) when nothing matches. It updates the taker order's amount like a fill.
  • The taker order needs no market check and no fresh oracle price. See Crossfill for a worked example.

Order management#

update#

fn update(trader: Address, updates: Vec<OrderUpdate>, approvals: Vec<Approval>) -> Vec<u128>

Changes the amount, price and expiration of the trader's orders in place, removes orders (amount 0) and grants allowances, all in one call. Updating in place keeps the order entry, so it avoids the rent of a new entry.

Name Type Meaning
trader Address Owner of every listed order.
updates Vec<OrderUpdate> New amount, price and expiration per order ID. Amount 0 removes the order.
approvals Vec<Approval> Allowances granted before the updates, in list order. An asset without an approval keeps its current allowance.

Returns the IDs updated or removed, in processing order. Missing IDs are skipped and not returned. An ID listed twice is processed once (the first entry wins).

Authorization: trader signs the root call and one approve sub-invocation per entry in approvals.

When frozen: approvals and removals still work, so makers can pull their quotes and revoke allowances. The first update that changes an order fails the call with 730.

Events: one mod per updated order with the new values, one mod with amount = 0 per removed order. The tokens emit their own approve events for the approvals.

Errors:

Notes:

  • A removal skips every check except ownership. It works on expired orders, while frozen and on a market without quoted assets. The price and expires of a removal entry are ignored.
  • Amounts are absolute, not changes to the current amount, so a fill landing between signing and execution is not taken into account (see Absolute amounts and fills in flight).
  • Updating an expired order revives it under the same ID. The new expiration must be 0 or in the future.
  • Backing is checked per selling asset on the sum of the new amounts in this call, after the approvals, against the balance (702) and then the allowance (703).
  • Approvals are absolute. When several approvals name the same asset, the last one wins. A zero amount revokes.
  • An updated order's entry TTL is extended, never shortened (see TTL policy). Removals do not extend anything.
  • update never calls the oracle. It values orders at the cached prices, like trade.
  • The event limit allows about 110 orders per call, and AxisAccount sends batches of 90. See Resource limits and costs.

Markets#

subsidize#

fn subsidize(sponsor: Address, selling: Address, buying: Address, amount: i128) -> Vec<u64>

Opens the market for a pair when none exists, or extends the oracle price feed access of an existing market, by burning the sponsor's XRF through the oracle. Then caches fresh prices for the market's listed assets.

Name Type Meaning
sponsor Address Account whose XRF is burned.
selling Address One market asset. Either order opens the same market.
buying Address The other market asset.
amount i128 XRF base units to burn. Positive. At least market_listing_fee to open a market.

Returns the new feed access expiration (UNIX seconds) for each oracle-listed asset of the market, in canonical order.

Authorization: sponsor signs the root call, the oracle's track sub-invocations and their XRF burn sub-invocations. Opening a market with more than the fee makes two track calls (the fee, then the rest), and one when the fee is zero. An existing market makes one.

When frozen: fails with 730.

Events: one refresh for the pair. The oracle and the XRF token emit their own events.

Errors:

Notes:

  • market_listing_fee is the oracle's daily per-asset fee times listing_min_days. Anything above the fee is tracked too, and with listing_min_days at 0 any positive amount opens a market. See Listing fee.
  • An asset whose token decimals plus the oracle decimals exceed 37 is recorded as unlisted.
  • On an existing market the whole amount extends access, after re-checking the listing like requote.
  • subsidize is the only way to create a market. trade never opens one.

requote#

fn requote(selling: Address, buying: Address) -> Option<Market>

Re-checks both market assets against the oracle (listing and token decimals) and caches the oracle's latest prices. Permissionless. Keepers call it for every active market so Limit trades and updates can be valued.

Name Type Meaning
selling Address One market asset.
buying Address The other market asset.

Returns the market record after the check, or None when the pair has no market.

Authorization: none.

When frozen: fails with 730.

Events: one refresh for an existing market, whether or not anything changed. Nothing for an unknown pair.

Errors:

Notes:

  • The market record is rewritten only when a side's listing or decimals change, and a cached price only when the oracle's record differs. A failed or empty oracle quote keeps the previous cached price, and lapsed feed access blocks new prices until someone calls subsidize.
  • A cached price is usable for 72 hours after the oracle's record timestamp, see Price cache.
  • A market whose assets are no longer quoted accepts no new Limit orders and no order changes. Its existing orders stay fillable and removable.
  • The JS client returns the simulated record without submitting when the simulation writes nothing. See Running a keeper.

Views#

Views change nothing, apart from the market entry TTL that market extends, and need no signature. Read them with a simulation (simulateTransaction). They work while the contract is frozen.

order#

fn order(id: u128) -> Option<Order>

Returns the order with this ID, or None when it does not exist or has expired. An expired order's entry can still exist in storage until it is removed, overwritten or archived. Only update can see it.

market#

fn market(selling: Address, buying: Address) -> Option<Market>

Returns the market record of the pair, in either asset order, or None. When called inside a submitted transaction it also extends the market entry to 120 days when fewer than 30 days are left.

config#

fn config() -> Config

Returns the safety admin, the oracle address, the days of feeds the listing fee buys, the market listing fee, the minimum trade size and the expected ledger close time. See Config.

frozen#

fn frozen() -> bool

Returns true while the safety admin has frozen the contract.

Safety admin#

The safety admin has exactly these six calls. Each needs the safety admin's signature (require_auth on the address in Config). A missing or wrong signature fails with a host authorization error, not a contract code. All six work while the contract is frozen.

freeze#

fn freeze(blocked: bool)

Sets the frozen flag. true blocks trade, swap, crossfill, subsidize, requote and every update that changes an order. Removals, approvals, views and admin calls keep working. Orders and allowances are not touched. Emits freeze on every call.

delegate#

fn delegate(new_safety_admin: Address)

Hands the safety admin role to another address. The previous admin loses it immediately. Emits config.

set_oracle#

fn set_oracle(oracle: Address)

Points the contract at another Reflector Beam oracle. The oracle must quote prices with at most 24 decimals and charge a positive daily fee, otherwise the call fails with 723 InvalidOracleConfig. The market listing fee is derived again, and cached prices stay usable only when the new oracle quotes with the same decimals. See Changing the oracle. Emits config.

set_floor#

fn set_floor(minimum: i128)

Sets the minimum order value in USD with 7 decimals (1 USD = 10,000,000). 0 disables the check. A negative value fails with 706 InvalidAmount. Emits config.

set_listing_min_days#

fn set_listing_min_days(days: u32)

Sets the days of price feeds the listing fee of a new market buys, and derives the market listing fee again from the oracle's current daily fee (daily fee times days). 0 opens markets without a fee. A value above 255 fails with 706 InvalidAmount, and an oracle that charges no daily fee, or one that overflows i128 at 255 days, with 723 InvalidOracleConfig. Existing markets are not affected. Emits config.

set_ledger_time#

fn set_ledger_time(ledger_time: u32)

Sets the expected ledger close time in seconds, which converts every lifetime the contract sets (contract, markets, orders, cached prices) into ledgers. It is 5 at deployment. 0 or a value above 20 fails with 706 InvalidAmount. TTLs already set are not changed. Emits config.

Constructor#

__constructor#

fn __constructor(safety_admin: Address, oracle: Address, min_trade_size: i128)

Runs once, at deployment. Stores the configuration, with listing_min_days at 90 and ledger_time at 5 seconds, and the oracle's price decimals, and derives the market listing fee from the oracle's daily fee.

Name Type Meaning
safety_admin Address Account allowed to call the six safety admin functions.
oracle Address Reflector Beam oracle contract.
min_trade_size i128 Minimum order value in USD with 7 decimals. 0 disables the check.

Events: config. Errors: 706 InvalidAmount for a negative min_trade_size, 723 InvalidOracleConfig for an oracle with more than 24 decimals, no positive daily fee, or a listing fee that overflows.