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;
}dexis the dexId, resolved through the registry.v3Pathis the packed path for a V3-shaped venue. Uniswap-encoded on dexId 1, Algebra-encoded (no fee bytes) on dexId 3.0xon a V2 venue.v2Pathis the hop list for a V2 venue. Empty on a V3 venue.fee,tickSpacingandhooksare reserved. No shipped adapter reads any of them. V3 pool tiers live insidev3Path, and Algebra fees are dynamic. Off-chain consumers may populatefeeandtickSpacingfor display only. Set unused fields, includinghooks, 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:
buyreturnstokenOut, the tokens delivered to the recipient.sellreturnsethToUser, 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 asvalue. - Tokens are delivered directly to
msg.senderby the adapter. They are never staged in the manager, and there is norecipientparameter. The buyer is always the caller, so a contract callingbuyreceives the tokens itself. minAmountOutis passed to the adapter as a venue-level floor and re-checked by the manager against the returnedtokenOut.- Any APE the venue did not consume is refunded to
msg.senderand emitted asEthRefunded. The refund is computed as a balance delta around the adapter call, so it is exactly what the adapter handed back. refcredits a referrer, andaddress(0)means none. Self-referral (ref == msg.sender) produces no referral share.refDatais 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
amountInoftoken. - 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.
minEthOutis checked againstethToUser, which is post-fee, and is what the seller actually receives.- APE is sent to
msg.senderwith a rawcall. A contract seller must have a payablereceiveorfallbackthat succeeds, or the trade reverts withEthTransferFailed.
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 > deadlineat 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.senderon both sides. Neither function takes a recipient argument.
Views
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.
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:
Inherited and library errors that surface through the same call:
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.