Hooks
ITradeTracker, ITradeFeeReceiver and IReferralManager, the three swappable seams on the trade manager.
On this page
Three interfaces let the manager's behavior around a trade change without redeploying
the trading contract. All three are set by the owner, and all three can be disabled by
setting the address to zero. The exception is feeReceiver, which rejects the zero
address at the setter and is required whenever a fee is non-zero.
ITradeTracker
interface ITradeTracker {
function onTrade(
address user,
address token,
bool isBuy,
uint256 ethAmount,
uint256 tokenAmount
) external;
}When it is called. Once per trade, near the end of buy and sell, after the
fee has been split and (on a sell) after the seller has been paid, immediately before
TradeExecuted is emitted.
What it receives. ethAmount is the gross APE side: msg.value on a buy, the
pre-fee APE on a sell. tokenAmount is the token side: the delivered amount on a buy,
the amount actually received by the manager on a sell.
Failure semantics. The call is wrapped in try/catch with an empty handler. A
reverting tracker is silently swallowed:
try ITradeTracker(tracker).onTrade(user, token, isBuy, ethAmount, tokenAmount) {} catch {}Skipped entirely when tradeTracker == address(0).
A tracker must never be load-bearing
Because reverts are swallowed, a tracker that runs out of gas or hits a bad state
loses that trade's notification with no on-chain signal. Do not build settlement or
anything financial on onTrade. Index TradeExecuted for that, because it is
emitted unconditionally. A tracker is for state a missed update cannot corrupt.
ITradeFeeReceiver
interface ITradeFeeReceiver {
function receiveTradeFee(
address user,
address token,
uint256 amount,
bytes calldata data
) external payable;
}When it is called. Inside _payFee, before the trade returns, for the protocol's
share of the fee, meaning the platform fee minus any referral carve-out. Skipped when
that share is zero. If the share is non-zero and feeReceiver is unset, the trade
reverts with FeeReceiverNotSet().
What it receives. Native APE: token == address(0), amount == msg.value,
and empty data.
What a third-party implementation may do. Anything, as long as it does not
revert. It is called synchronously inside the trade, so a reverting receiver bricks
all trading on the manager. It must accept native APE. It must not assume user is
an EOA. It should treat msg.sender == tradeManager as its authentication.
ArmoryTradeCashBackReceiver
The reference ITradeFeeReceiver implementation, and the intended destination for
the protocol share of the fee. For an integrator, the parts that are load-bearing:
- It is manager-authenticated.
receiveTradeFeerevertsOnlyTradeManager()for any other caller,TokenFeesNotSupported()iftoken != address(0), andAmountMismatch()ifmsg.value != amount. - It is deliberately not
nonReentrant, and that is not an oversight. It can re-enter itself throughArmoryTradeManager.buy, and a shared guard would brick that path. It is safe unguarded because it is manager-only and purely additive, and the functions that move value carry the guard instead. A third-partyITradeFeeReceiverthat copies the pattern without the same constraints would be reentrant. - Its
receive()accepts APE from exactly two senders: the trade manager (a buy refund arriving mid-operation) and WAPE. Everything else revertsUnexpectedEth(). Do not send it APE. - It can itself route trades through the manager, like any other caller. Those trades
pay the platform fee and appear in
TradeExecutedwith the contract's own address asuser. An indexer building per-wallet volume must exclude it.
IReferralManager
interface IReferralManager {
function recordReferralFee(
address referrer,
address trader,
address token,
bytes calldata data
) external payable;
}When it is called. In _splitFee, with the referral share as msg.value, and
before the protocol's share is paid to the fee receiver. A referral share is
computed only when all four of these hold:
ref != address(0) && ref != user && referralBps != 0 && address(referralManager) != address(0)refFee = fee * referralBps / MAX_BPS. The trader pays the same total fee either
way, because the referral share is carved out of the fee rather than added to it.
Self-referral produces no referral share.
Failure semantics. Deliberately not wrapped in try/catch. A lost referral
credit is real value, so a broken referral manager reverts the trade loudly rather
than swallowing APE. That also makes setReferralManager a live-fire change: a
reverting implementation bricks every trade that passes a non-zero ref.
When referralManager is unset, passing a ref is inert: the whole fee goes to
the fee receiver and refFee is zero in TradeExecuted. refData is forwarded
verbatim to recordReferralFee; pass "0x" when no data is required.
referralRouter is not an IReferralManager
The referralRouter address key is ArmoryReferralRouter, a completely separate
contract: an opt-in integrator-fee wrapper around the canonical Armory V2 and V3
routers, which skims a caller-chosen input-side fee in kind and emits ReferralSwap.
It does not implement recordReferralFee and cannot be set as the manager's
referralManager. It is a router-level surface for integrators who want a fee on
direct DEX swaps, not an aggregator hook.