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
Envargument that every Soroban entry point receives. - Amounts are
i128integer base units of a token. Prices arei128fixed point numbers with 18 decimals:buyingbase units per 1sellingbase unit, times 10^18. See price and amount units. - Order IDs are
u128values 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
ExtendFootprintTTLoperation, 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:
730 Frozen: the contract is frozen.706 InvalidAmount:amountis zero or negative.705 InvalidPrice:priceis outside 1 to 10^36.704 InvalidMatch:sellingequalsbuying.707 InvalidExpiration: aLimittrade withexpiresnot0and not in the future.721 AssetsNotVerifiedByOracle: aLimittrade on a pair without a market, or with no asset quoted by the oracle.722 AssetPriceOracleFetchFailed: aLimittrade with no usable cached price to value it, whilemin_trade_sizeis not 0.720 OrderSizeTooSmall: aLimittrade below the minimum order value, or one whose remainder is dust while nothing was filled.709 NotFilled: aFillOrKilltrade that does not execute in full.708 CannotReceive: the trader cannot receivebuying, checked before any transfer, or the final forward to the trader fails, for example on a full trustline.712 IntermediaryCannotReceive: the contract cannot holdbuyingon its way to the trader, because the issuer requires authorization and has not authorized the contract.711 OrderExists: the remainder ID belongs to a live order of the trader.702 InsufficientBalanceand703 InsufficientAllowance: the remainder order is not backed.740 Overflow: a price calculation does not fiti128.- A token error when the payment to a maker fails: the trader cannot pay (balance or allowance), or the maker cannot be credited although authorized (a full trustline). The error does not tell which (see token errors).
Notes:
- Missing, expired and duplicate IDs, orders of another pair, orders priced worse than the limit
(
order.price <= floor(10^36 / price)forSell,order.price <= priceforBuy) and fills that round to zero are skipped silently. A maker who cannot settle is skipped with askipevent, 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
Limittrade checks the expiration, the market and the minimum order value of the wholeamountbefore matching.FillandFillOrKillneed no market and ignorenonceandexpires. AFillOrKillSellcounts as executed when the leftover cannot buy one base unit ofbuyingat the highest order price it filled. - A
Buyremainder 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
sellandbuy(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:pathis empty, orselling_amountorbuying_amountis 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, theSelloutput is belowbuying_amount, theBuycost exceedsselling_amount, or execution differs from the plan.740 Overflow: a price calculation does not fiti128.- 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.
swapis strict and has no708pre-check (see token errors).
Notes:
- There is no price limit per hop. Slippage is bounded by
selling_amountandbuying_amounton 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, sosoldcan be slightly belowselling_amount. WithBuy, 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.
swapneeds 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'sbuyingasset, because its issuer requires authorization and has not authorized the contract.708 CannotReceive:tradercannot receive the taker order'sbuyingasset.740 Overflow: a price calculation does not fiti128.- 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
traderdoes not fit their trustline (see token errors).
Notes:
- The owner's budget is
min(order amount, balance, allowance)in the taker order'ssellingasset. - Makers are matched like a
Selltaker 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:
701 NotAuthorized:traderdoes not own a listed order (removals included).730 Frozen: an update that changes an order while frozen.706 InvalidAmount: a negative amount.705 InvalidPrice: a price outside 1 to 10^36.707 InvalidExpiration:expiresnot0and not in the future.720 OrderSizeTooSmall: the new amount is dust or below the minimum order value.722 AssetPriceOracleFetchFailed: no usable cached price to value the order, whilemin_trade_sizeis not 0.721 AssetsNotVerifiedByOracle: an update that changes an order whose pair has no market, or whose market has no asset quoted by the oracle.708 CannotReceive:tradercannot receive the order'sbuyingasset.702 InsufficientBalanceand703 InsufficientAllowance: the new amounts are not backed.740 Overflow: a price calculation does not fiti128.
Notes:
- A removal skips every check except ownership. It works on expired orders, while frozen and on a market without quoted
assets. The
priceandexpiresof 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
0or 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.
updatenever calls the oracle. It values orders at the cached prices, liketrade.- The event limit allows about 110 orders per call, and
AxisAccountsends 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:
730 Frozen: the contract is frozen.706 InvalidAmount:amountis not positive, or the pair has no market andamountis belowmarket_listing_fee.704 InvalidMatch:sellingequalsbuying.721 AssetsNotVerifiedByOracle: neither asset is quoted by the oracle.- A token error when the sponsor lacks XRF (see token errors).
Notes:
market_listing_feeis the oracle's daily per-asset fee timeslisting_min_days. Anything above the fee is tracked too, and withlisting_min_daysat 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
amountextends access, after re-checking the listing likerequote. subsidizeis the only way to create a market.tradenever 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:
730 Frozen: the contract is frozen.
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
Limitorders 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.