AXIS Docs

DevelopersSmart contract

Failure modes

What happens when the book moves before inclusion, a counterparty cannot settle or an input is out of bounds, and how clients should react.

Most AXIS failures are not errors. A listed order that no longer qualifies is skipped, and the trade executes with what is left. The tables below list each situation with the contract's behavior (v0.6.0) and the recommended client reaction. Error codes are described in Errors.

Who is at fault#

Failure What happens Who is at fault What clients should do
skip event The call succeeds. The listed order is left unchanged and its ID is reported The order's owner: the backing left does not cover the fill, the owner cannot receive the taker's asset, or the maker's asset could not be collected into the contract (trade and crossfill) Stop proposing that maker until its backing has been read again
Token error on the payment to a maker The whole call fails with the token's own error, for a Stellar Asset Contract AllowanceError (9), BalanceError (10), BalanceDeauthorizedError (11) or TrustlineMissingError (13). Nothing moves and no maker is flagged Ambiguous: the payer cannot pay (balance or allowance), or the maker cannot be credited although authorized, typically a full trustline. In crossfill the payer is the taker order's owner, so the caller is never at fault for this leg Check the payer's balance and allowance. If they cover the call, leave out the maker whose line has no room and retry. Penalize neither side
Token error on a maker leg in a swap The swap fails with the token's own error, since swaps settle strictly The maker Re-quote without that maker
CannotReceive (708) The whole call fails, before any transfer or on the final forward The receiving side: the trader (or the crossfill caller) has no authorized trustline for the bought asset, or the forward to the trader does not fit, for example a full trustline Fix the trustline or make room under its limit
IntermediaryCannotReceive (712) The whole call fails before any transfer Nobody's trading fault: the issuer of the bought asset requires authorization and has not authorized the AXIS contract Wait for the issuer, see Assets that require authorization
crossfill taker order skipped The call succeeds and returns (0, 0, 0). A skip event carries the taker order's ID and nothing moves The taker order's owner, who cannot back the order or cannot receive its buying asset Leave that taker order out until its owner's backing changes
Silent skip The call succeeds without an event for that order Nobody: the order is missing, expired, overpriced, listed twice or on another pair List a few spare orders

Only a skip event points at a party, the order's owner. A failed transaction changes no state, and indexers and routers must not attribute it to anyone. The AXIS API keeps failed AXIS calls for diagnostics only (see REST API): a failed token transfer is recorded with both its sender and its receiver, neither of them blamed, and failures never affect routing.

Races between simulation and inclusion#

A taker signs the call arguments only, never the amounts paid to makers (see Authorization), so a signed trade stays valid whatever happens to the listed orders in the meantime.

Situation What happens What clients should do
A listed order was filled or removed Its entry is gone and matching skips it without an event. The trade succeeds with fewer fills List a few spare orders after the ones the quote needs, within the event budget. Read sold and bought from the result
A listed order was partially filled The taker fills what is left of it Same as above. Use Fill-or-Kill when partial execution is not acceptable
A listed order expired Skipped without an event. The entry is only read Avoid orders that expire within seconds of the quote
A listed order was repriced with update A price beyond the taker's threshold is skipped without an event. Any other price fills at the order's new price Set the limit to the worst price you accept, such as the quote's worstPrice, not to the best price you saw
The owner reused a listed ID for an order on another pair Skipped without an event, the other order is untouched Nothing to do. A reused ID on the same pair is matched as a new order, under the normal price check
A listed order skipped or not reached in simulation matches at execution (backing restored, price moved back into range, an earlier order shrank or vanished) Execution writes that order and its maker's balances and allowance. If the transaction did not declare those entries read-write, it fails Declare every listed order and, per maker, the balance of the asset sold, the allowance on it and the balance of the asset received, plus the contract's own balance of every asset passing through it and the trader's balance of the bought asset, all read-write. The JS client does this by default for Stellar Asset Contract tokens
A Limit trade is retried after an uncertain submission With the same nonce, the retry fails with OrderExists (711) if the first attempt stored a remainder that is still live. If the first attempt filled completely, the retry trades again Look up the first transaction, or the order by its ID, before retrying

Backing problems#

The contract checks a maker's backing at the moment of each fill (see Admission at match time). It never trims, caps or removes an order because of it.

Situation What happens What clients should do
The maker spent the balance, lowered or revoked the allowance, or the allowance expired A fill the backing left does not cover is skipped with a skip event. The order stays unchanged and the taker's amount moves on to the next listed order Route on effective depth. After a skip, stop proposing the maker until its backing has been read again
The backing covers only part of an order A fill within the backing is admitted, a larger one is skipped whole Size the fill of a partially backed order to its backed amount
The maker's trustline for the asset they sell is deauthorized, or the balance is locked by classic selling liabilities or the XLM reserve Admission passes, the maker leg's try_transfer_from into the contract fails, and every order of that maker in the call is skipped with a skip event. A swap fails instead Subtract known liabilities and reserves from classic balances. React to skip as above
The maker cannot receive the taker's asset (missing or deauthorized trustline) Skipped at admission with a skip event, before any transfer Check the maker's trustline for the asset the taker pays with
The maker's trustline for the asset they receive is full Admission passes, because authorized reports no limit. The payment to the maker fails and the whole call reverts with the token's error, which looks the same as a taker who cannot pay Before listing a maker, check that its line for the taker's asset has room for the payment (headroom in the indexer's backing records). After such a failure, leave the maker out and retry

Archived entries#

Situation What happens What clients should do
A listed maker order archived once its lifetime ran out without an update (about 120 days on mainnet, 7 days after creation on Testnet) Simulation marks it for restore, and the transaction restores it at the submitter's expense, a fresh rent chunk. A spare order the simulation did not reach is declared without the mark, and the transaction fails when applied Do not list orders whose lifetime is about to run out
An owner removes or updates an archived order The owner's transaction restores the entry first and pays its rent Keep active orders fresh with update. An archived order is not canceled: anyone who restores it can fill it while the maker still backs it
The contract instance or code archived Every call has to restore them first Extend the contract lifetime regularly (see Keepers lapsing)

Taker-side problems#

Situation What happens What clients should do
The taker cannot receive the bought asset (missing or deauthorized trustline) trade and crossfill fail with CannotReceive (708) before any transfer, and a Limit trade that stores a remainder checks it as well. swap has no pre-check and fails with the token's error Make sure the trustline exists before trading
The bought asset requires authorization and its issuer has not authorized the AXIS contract IntermediaryCannotReceive (712) before any transfer, in trade, crossfill and for any asset of a swap path Nothing the trader can fix. See Assets that require authorization
In crossfill, the taker order's owner has no room left in the trustline for the asset it buys The owner's payout is a plain transfer made after the makers delivered. The receive check reads only authorized, so the whole call reverts with the token's error. The same goes for the surplus paid to the caller Skip that taker order until its owner's trustline has room
The taker's trustline for the bought asset is full The receiver check only reads authorized, so it passes. The makers settle into the contract, then the forward to the taker fails and the call fails with CannotReceive (708). No maker is skipped or flagged. A swap fails with the token's error instead Keep room under the trustline limit
The taker's balance or allowance does not cover the fills The payment's transfer_from fails with the token's error and the whole call reverts. The error does not say whether the taker or the maker was at fault Pass an approve in the call and check the balance first
The remainder of a Limit trade is not backed after the fills InsufficientBalance (702) or InsufficientAllowance (703), and the fills revert too Approve and hold the whole amount (Sell) or its worst-case cost (Buy). Add what your open orders need if they should stay backed
expires is in the past when the transaction executes InvalidExpiration (707) Leave a margin of more than the expected inclusion time
The remainder's ID belongs to a live order of the trader OrderExists (711) Use a fresh nonce for every new order
The taker lists their own order Allowed: the tokens go through the contract and back to the same account, and the order shrinks Filter out your own orders if self-trades are unwanted

Fill-or-Kill and swap bounds#

Situation What happens What clients should do
A Fill-or-Kill cannot execute in full with the admitted fills NotFilled (709) before any transfer, in simulation or, if the book changed after simulation, on-chain Re-quote, or switch to Fill
A maker of a Fill-or-Kill fails at settlement NotFilled (709) after settlement, and the whole call reverts Re-quote without that maker. Spare orders do not help here, because matching stops before reaching them
A Sell Fill-or-Kill leaves an amount too small to buy one base unit at the highest order price it filled Counts as executed and the trader keeps the leftover Expect sold slightly below amount
The swap route cannot meet buying_amount (Sell) or stay within selling_amount (Buy) NotFilled (709), nothing moves Re-quote and set the bound with explicit slippage
A swap maker's transfer fails The swap reverts with the token's error, since swaps settle strictly Re-quote without that maker
A swap maker lacks backing or cannot receive Skipped in planning and in execution alike, and the hop's other listed orders fill instead List fallback orders in every hop
A Sell swap leaves a rounding remainder of selling_amount unused The trader keeps it and sold is slightly lower. A route that cannot absorb more than such a remainder fails with NotFilled (709) Read sold from the result

Small amounts and dust#

Situation What happens What clients should do
A Limit trade whose whole amount is below the minimum order value OrderSizeTooSmall (720) before any matching, even if the listed orders would fill it Use Fill for small trades
A Limit trade fills nothing and its remainder is dust OrderSizeTooSmall (720) Raise the amount or adjust the price
A partial fill leaves a dust remainder The trade succeeds without storing an order, and returns no ID Do not expect an ID for every Limit trade
An update sets a dust amount or a value below the floor OrderSizeTooSmall (720) for the whole batch Remove the order instead
A fill leaves dust on a maker order The order is removed and the trade event reports left = 0 Treat left = 0 as filled

Prices and oracle access#

Situation What happens What clients should do
No usable cached price: the cached oracle timestamp is more than 72 hours old, or the oracle changed to one with other decimals New Limit orders and order changes fail with AssetPriceOracleFetchFailed (722) while the floor is non-zero. Fills, swaps, crossfills and removals are unaffected Call requote for the market, it is permissionless, then retry. After an oracle change, call subsidize first if the contract has no feed access on the new oracle
The market's oracle feed access lapsed requote cannot read new prices and keeps the old cache, which ages out after 72 hours. Then 722 as above Call subsidize for the market
The oracle stopped quoting both assets and a check recorded it Limit trades, subsidize and every update that changes an order fail with AssetsNotVerifiedByOracle (721). Existing orders stay fillable, and removals and approvals through update still work Trade against existing orders with Fill or remove them
An asset's token decimals plus the oracle decimals exceed 37 The next check records the asset as unlisted, as if the oracle did not quote it, so a market with no other quoted asset behaves as in the row above Pair the asset with one the oracle quotes
The pair has no market Limit trades and order changes fail with AssetsNotVerifiedByOracle (721). Fill, FillOrKill, swap and crossfill need no market Open the market with subsidize

Markets and the price cache are described in Markets and oracle.

Frozen contract#

Situation What happens What clients should do
The safety admin froze the contract trade, swap, crossfill, subsidize, requote and every update that changes an order fail with Frozen (730). Removals and approvals through update work, and so do the views, the admin calls and lifetime extensions. Orders and allowances are untouched and no funds are stranded Check frozen(), or watch for a rejected quote from the AXIS API. Let makers remove orders and revoke allowances with batches that contain no other change

Safety admin and invariants lists the full matrix.

Keepers lapsing#

Situation What happens What clients should do
Nobody extends the contract Every state-changing call extends the contract to 3 days once fewer than 3 days are left, and the trader who triggers it pays that rent, about 2.5 XLM per day since the previous top-up on Testnet Run a keeper, see Running a keeper
Nobody calls requote Cached prices age out after 72 hours (see above) Refresh every active market well within 72 hours
Nobody calls subsidize Feed access lapses and the cache ages out after it (see above) Fund feed access before it expires