Documentation

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. receiveTradeFee reverts OnlyTradeManager() for any other caller, TokenFeesNotSupported() if token != address(0), and AmountMismatch() if msg.value != amount.
  • It is deliberately not nonReentrant, and that is not an oversight. It can re-enter itself through ArmoryTradeManager.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-party ITradeFeeReceiver that 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 reverts UnexpectedEth(). 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 TradeExecuted with the contract's own address as user. 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.

Addresses

ContractAddressApeScan
ArmoryTradeManager0x2cea…Dd9F
ArmoryTradeCashBackReceiver0x9B10…4a03
ReferralRouter0xc6be…7E36
The manager, the reference fee receiver, and the unrelated router-level referral wrapper.