Documentation

TradeManager reference

The full external ABI of ArmoryTradeManager, covering quoting, buys, sells, structs, events, access control and every revert.

On this page

Everything below is part of the external ArmoryTradeManager surface.

The Route tuple

struct Route {
    uint8 dex;
    bytes v3Path;
    address[] v2Path;
    uint24 fee;
    int24 tickSpacing;
    address hooks;
}
  • dex is the dexId, resolved through the registry.
  • v3Path is the packed path for a V3-shaped venue. Uniswap-encoded on dexId 1, Algebra-encoded (no fee bytes) on dexId 3. 0x on a V2 venue.
  • v2Path is the hop list for a V2 venue. Empty on a V3 venue.
  • fee, tickSpacing and hooks are reserved. No shipped adapter reads any of them. V3 pool tiers live inside v3Path, and Algebra fees are dynamic. Off-chain consumers may populate fee and tickSpacing for display only. Set unused fields, including hooks, to zero.

Field meaning is owned by the adapter a dexId routes to. A future venue that needs different parameters ABI-encodes them into v3Path and ignores the rest. The struct itself is fixed.

Quoting

There is no quote function. Clients eth_call-simulate the real trade with a zero floor and read the return value:

  • buy returns tokenOut, the tokens delivered to the recipient.
  • sell returns ethToUser, the post-fee APE sent to the seller.

Simulate with from set to the trading address and value set to the real trade size, because the fee comes off msg.value before routing and the result depends on it.

Fee ordering differs between the two sides

On a buy the fee is taken from msg.value first and only the remainder is routed, so a buy is quoted on size - fee. On a sell the whole token amount is routed and the fee is taken from the APE that came back, so a sell's output is grossOut - fee.

Applying the buy ordering to a sell overstates it by roughly fee squared, and a sell's output is exactly the number minEthOut is derived from.

Fee arithmetic, both sides:

// buy
uint256 fee = (msg.value * feeBpsFor(route.dex)) / MAX_BPS;
uint256 ethIn = msg.value - fee;
 
// sell
uint256 fee = (ethOut * feeBpsFor(route.dex)) / MAX_BPS;
uint256 ethToUser = ethOut - fee;

MAX_BPS is the basis-point denominator and is a public constant. Read it rather than assuming it, and read feeBpsFor(route.dex) in the same batch you quote with.

buy

function buy(
    address token,
    Route calldata route,
    uint256 minAmountOut,
    uint256 deadline,
    address ref,
    bytes calldata refData
) external payable nonReentrant returns (uint256 tokenOut);
  • Native APE in, via msg.value. There is no ERC-20 buy leg.
  • The fee is deducted from msg.value, and the remainder is forwarded to the adapter as value.
  • Tokens are delivered directly to msg.sender by the adapter. They are never staged in the manager, and there is no recipient parameter. The buyer is always the caller, so a contract calling buy receives the tokens itself.
  • minAmountOut is passed to the adapter as a venue-level floor and re-checked by the manager against the returned tokenOut.
  • Any APE the venue did not consume is refunded to msg.sender and emitted as EthRefunded. The refund is computed as a balance delta around the adapter call, so it is exactly what the adapter handed back.
  • ref credits a referrer, and address(0) means none. Self-referral (ref == msg.sender) produces no referral share. refData is forwarded to the referral manager.

You pay the fee on the full msg.value, refund included

The fee is computed on msg.value before routing, and the refund is returned after. Sending more APE than you intend to spend and relying on the refund therefore costs you fee on the unspent portion. Send the exact size.

sell

function sell(
    address token,
    uint256 amountIn,
    Route calldata route,
    uint256 minEthOut,
    uint256 deadline,
    address ref,
    bytes calldata refData
) external nonReentrant returns (uint256 ethToUser);
  • The caller must have approved the manager for amountIn of token.
  • The manager pulls the tokens with safeTransferFrom, measures what actually arrived (a balance delta, so fee-on-transfer tokens cannot desync accounting), and transfers that amount to the adapter.
  • APE proceeds are measured as the manager's own balance delta across the adapter call. The adapter's return value is not used.
  • minEthOut is checked against ethToUser, which is post-fee, and is what the seller actually receives.
  • APE is sent to msg.sender with a raw call. A contract seller must have a payable receive or fallback that succeeds, or the trade reverts with EthTransferFailed.

Sell paths run token → WAPE

The path in a sell Route must be encoded from the token to WAPE, not from WAPE to the token. Both shipped adapters validate first == token, last == WAPE on a sell and first == WAPE, last == token on a buy. Reusing a buy path for a sell reverts with TokenMismatch before any router is touched. See Path encoding.

Slippage, deadlines and recipients

  • Deadline is Unix seconds, checked as block.timestamp > deadline at the top of both functions. The reference terminal uses a ten-minute window.
  • Slippage is applied by the caller to the simulated output, not to a mark: minOut = simulatedOut * (MAX_BPS - slippageBps) / MAX_BPS. The simulation already includes the platform fee and the price impact, so the floor means "how much worse than quoted am I willing to fill".
  • Recipient is always msg.sender on both sides. Neither function takes a recipient argument.

Views

FunctionReturns
adapters(uint8) → addressthe adapter bound to a dexId, or zero
dexPaused(uint8) → boolper-venue kill switch
isAdapter(address) → boolthe receive() allowlist. True for any address ever registered
feeBpsFor(uint8) → uint16the rate this venue pays right now. The authority
platformFeeBps() → uint16the default rate used when a venue has no override
referralBps() → uint16the referrer's share of the fee, in bps of the fee
feeReceiver() → addressthe ITradeFeeReceiver fees are pushed to
tradeTracker() → addressthe ITradeTracker hook, or zero when disabled
referralManager() → addressthe IReferralManager, or zero when referrals are off
manager() → addressthe IGovernanceManager that onlyOwner reads from
MAX_BPS, MAX_PLATFORM_FEE_BPS, MAX_REFERRAL_BPSuint16 constants

The per-venue override itself is stored privately as bps plus one, so that an explicit zero-bps override is distinguishable from "unset". Do not try to read it directly. feeBpsFor is the accessor.

Owner-only functions

onlyOwner resolves to manager.owner() on the hardcoded IGovernanceManager constant in ApeOwnable. It reverts with the string "Only Owner", not a custom error, which is worth knowing when you decode a revert.

FunctionEffect
addAdapter(uint8 dexId, address adapter)bind a dexId, permanently
setDexPaused(uint8 dexId, bool paused)gate a venue
setDexFeeBps(uint8 dexId, uint16 bps)set a per-venue fee override
clearDexFeeBps(uint8 dexId)drop the override, so the venue falls back to the default
setPlatformFeeBps(uint16 bps)set the default rate
setFeeReceiver(address receiver)change the fee destination. Zero is rejected
setTradeTracker(address tracker)set or disable (zero) the tracker hook
setReferralManager(address)set or disable (zero) referrals
setReferralBps(uint16 bps)set the referrer's share of the fee

Fee setters are bounded by MAX_PLATFORM_FEE_BPS, and setReferralBps by MAX_REFERRAL_BPS. Both revert FeeTooHigh above their cap. There is no removeAdapter, no rescue, and no upgrade path.

Events

event TradeExecuted(
    address indexed user,
    address indexed token,
    bool indexed isBuy,
    uint8 dex,
    uint256 ethAmount,   // buys: msg.value; sells: APE out, pre-fee
    uint256 tokenAmount,
    uint256 fee,         // TOTAL platform fee (protocol + referral)
    address ref,         // address(0) if none
    uint256 refFee       // referral portion of `fee`
);
event EthRefunded(address indexed user, uint256 amount);
event AdapterAdded(uint8 indexed dexId, address indexed adapter);
event DexPausedUpdated(uint8 indexed dexId, bool paused);
event PlatformFeeUpdated(uint16 previousBps, uint16 newBps);
event DexFeeOverrideUpdated(uint8 indexed dexId, bool set, uint16 bps);
event FeeReceiverUpdated(address indexed previousReceiver, address indexed newReceiver);
event TradeTrackerUpdated(address indexed previousTracker, address indexed newTracker);
event ReferralManagerUpdated(address indexed previousManager, address indexed newManager);
event ReferralBpsUpdated(uint16 previousBps, uint16 newBps);

Note the asymmetry in TradeExecuted. On a buy, ethAmount is the gross msg.value including any APE that was later refunded. On a sell it is the APE the venue produced before the fee. tokenAmount on a sell is the amount that actually arrived at the manager, not the requested amountIn.

isBuy is indexed, so it is a topic and not a data field. Filter on it rather than decoding.

Every revert an integrator can hit

From ArmoryTradeManager:

ErrorWhen
Expired()block.timestamp > deadline
ZeroAmount()msg.value == 0 on a buy. amountIn == 0 or nothing received on a sell. Also if the fee consumes the entire msg.value
UnknownDex(uint8 dexId)no adapter is registered for this dexId
DexIsPaused(uint8 dexId)the dexId exists but is paused
SlippageExceeded()tokenOut < minAmountOut, or ethToUser < minEthOut
NoSwapOutput()a sell produced zero APE at the manager
FeeReceiverNotSet()a non-zero protocol fee with feeReceiver == address(0)
EthTransferFailed()the refund, or the sell payout, was rejected by the caller
UnexpectedEth()plain APE sent to the manager from an address that is not a registered adapter
FeeTooHigh()an owner-only setter, above its cap
AdapterExists(uint8 dexId)addAdapter on a bound dexId
ZeroAddress()addAdapter(_, 0) or setFeeReceiver(0)

Inherited and library errors that surface through the same call:

ErrorSource
ReentrancyGuardReentrantCall()OpenZeppelin ReentrancyGuard on buy and sell
SafeERC20FailedOperation(address token)the transferFrom on a sell
BadPath()SwapPaths. The path bytes are the wrong length or shape
TokenMismatch()SwapPaths. First or last token is wrong, so wrong direction or wrong token
OnlyManager()TradeAdapterBase. You called an adapter directly
NoSwapOutput()the adapter's own zero-output guard
"Only Owner" (string)ApeOwnable

Plus whatever the underlying router reverts with. A two-hop path whose middle hop names a pool that does not exist passes SwapPaths, which only checks the ends, and reverts inside the router instead.