Documentation

Access control

Who owns what across the DEX, what the admin and fee manager contracts can do, what the multisig gates, and what none of it can touch.

On this page
ContractAddressApeScan
ArmoryDexAdmin0x9a8d…2e27
ArmoryDexFeeManager0xb904…49Ad
ArmoryV2Factory0x7AA7…50D5
ArmoryV3Factory0xAb52…434F

The shape of it

The vendored factories run on Solidity 0.5.16 and 0.7.6 and cannot inherit the platform's 0.8.28 ownership base. So instead of editing them, two purpose-built 0.8.28 contracts are installed as their privileged addresses and forward each capability behind a single check.

RoleHeld by
ArmoryV2Factory.feeToSetterArmoryDexAdmin
ArmoryV3Factory.ownerArmoryDexFeeManager
onlyOwner on both of the aboveresolved live from a hardcoded GovernanceManager contract's owner(), which is the platform Safe

Both admin contracts inherit ApeOwnable, whose onlyOwner is:

require(msg.sender == manager.owner(), 'Only Owner');

manager is a constant pointing at the platform GovernanceManager. Two consequences worth internalizing:

  • The Safe address is never stored in the DEX contracts. Rotating the Safe at the governance manager rotates authority over the whole DEX automatically, with no transaction on any DEX contract.
  • The governance manager address is a compile-time constant. It cannot be changed without redeploying every ApeOwnable contract.

ArmoryDexAdmin

function setV2FeeTo(address factory, address feeTo) external onlyOwner;
function setV2FeeToSetter(address factory, address newSetter) external onlyOwner;
function enableV3FeeAmount(address factory, uint24 fee, int24 tickSpacing) external onlyOwner;
function setV3FactoryOwner(address factory, address newOwner) external onlyOwner;
function setV3PoolFeeProtocol(address pool, uint8 feeProtocol0, uint8 feeProtocol1) external onlyOwner;
function collectV3Protocol(address pool, address recipient, uint128 amount0Requested, uint128 amount1Requested)
    external onlyOwner returns (uint128 amount0, uint128 amount1);
function execute(address target, bytes calldata data) external payable onlyOwner returns (bytes memory);

execute is an unrestricted call behind onlyOwner, reverting Execute Failed. It exists because a 78-line adapter cannot anticipate every future admin surface. Treat it as: the Safe can make this contract do anything this contract is authorized to do.

V3 passthroughs require factory ownership

enableV3FeeAmount, setV3FactoryOwner, setV3PoolFeeProtocol and collectV3Protocol all require this contract to be the V3 factory owner. It is not; the fee manager is. Call the corresponding fee-manager surface unless factory ownership changes.

ArmoryDexFeeManager

Owns the V3 factory. It closes a real gap in vanilla Uniswap V3: pools launch with feeProtocol = 0 and the factory has no default hook, so a protocol wanting the fee on has to remember to configure every pool one at a time.

Permissionless. Anyone, including you, can call these:

function applyDefaultFeeProtocol(address pool) public;
function applyDefaultFeeProtocolMany(address[] calldata pools) external;
function collectProtocol(address pool) public returns (uint128 amount0, uint128 amount1);
function collectProtocolMany(address[] calldata pools) external;
function feeReceiver() public view returns (address);

applyDefaultFeeProtocol writes the configured default onto a pool, and is a no-op rather than a revert in every case where it should not act: when the default is zero, when this contract is not the factory owner, when the pool carries a multisig override, or when the pool's feeProtocol is already non-zero. That makes it safe to call unconditionally after creating a pool.

collectProtocol sweeps type(uint128).max of both sides to feeReceiver(). It is permissionless because the destination is fixed by config, not by the caller. There is no recipient parameter to abuse.

Multisig-gated:

function setDefaultFeeProtocol(uint8 feeProtocol0, uint8 feeProtocol1) external onlyOwner;
function setPoolFeeProtocol(address pool, uint8 feeProtocol0, uint8 feeProtocol1) external onlyOwner;
function clearPoolFeeProtocolOverride(address pool) external onlyOwner;
function setFeeReceiver(address newReceiver) external onlyOwner;
function enableFeeAmount(uint24 fee, int24 tickSpacing) external onlyOwner;
function setFactoryOwner(address newOwner) external onlyOwner;

_validateFeeProtocol enforces the pool's own rule on the default and on setDefaultFeeProtocol: each side is 0, or an integer in [4, 10]. Anything else reverts Invalid Fee Protocol.

setPoolFeeProtocol sets feeProtocolOverridden[pool] = true, which the permissionless crank checks first. A deliberate setPoolFeeProtocol(pool, 0, 0) is therefore durable: a keeper cannot undo it. clearPoolFeeProtocolOverride returns the pool to default-tracking.

feeReceiver() returns feeReceiverOverride when set, otherwise manager.owner(). Setting the override to zero resets it to the Safe.

Events: DefaultFeeProtocolUpdated, DefaultApplied, PoolFeeProtocolOverridden, FeeReceiverUpdated, ProtocolFeesCollected.

What the admin layer can and cannot do

CapabilityWhoNotes
Redirect the V2 protocol-fee destinationSafe, via DexAdmin.setV2FeeToStatic pointer. See the rotation note below.
Rotate the V2 feeToSetterSafe, via DexAdmin.setV2FeeToSetterOne-way if handed to a contract with no forwarder
Enable a new V3 fee tierSafe, via FeeManager.enableFeeAmountAdditive only; a tier can never be removed
Set a pool's feeProtocolSafe, via FeeManager.setPoolFeeProtocol; keeper, via the default crankBounded to 0 or [4, 10] per side by the pool itself
Sweep accrued V3 protocol feesanyoneDestination is feeReceiver(), not the caller
Hand the V3 factory to a successorSafe, via FeeManager.setFactoryOwnerIrreversible from the fee manager's side
Pause a pool or a routernobodyNo pause exists anywhere in the DEX
Upgrade a pool, pair, or routernobodyNo proxies; every deployment is immutable code
Move LP funds or positionsnobodycollectProtocol reaches only the protocolFees accumulator
Change the V2 swap feenobody0.3% is compiled into the pair's K check
Change an existing V3 pool's tiernobodyfee and tickSpacing are pool immutables
Blocklist a token or an addressnobodyNo allowlist in any vendored contract

That last block is the substantive answer for anyone deciding whether to build on these contracts. The admin layer's entire reach is fee routing and fee-tier enablement. It cannot stop a swap, seize liquidity, or change pricing on a pool that already exists.

What a Safe rotation does and does not follow

Two protocol-fee destinations resolve the multisig differently, and only one follows a rotation on its own.

Resolves viaFollows a Safe rotation?
V3 protocol feesArmoryDexFeeManager.feeReceiver(), which reads manager.owner() live at sweep timeYes, automatically
V2 protocol feesArmoryV2Factory.feeTo, a static address written onceNo

feeTo is the one static pointer in the system. Rotating the Safe without also calling ArmoryDexAdmin.setV2FeeTo(v2Factory, newSafe) leaves V2 protocol LP minting to the old address, with no failed transaction to signal it. The V2 fee is realized lazily on mint and burn, as described on V2 core, so it just accrues quietly to the wrong owner.

Reading the live state

Never infer authority from documentation. These are all plain reads:

const [v2FeeTo, v2FeeToSetter, v3Owner, feeReceiver] = await Promise.all([
  publicClient.readContract({ address: CONTRACTS.v2Factory,    abi: v2FactoryAbi,    functionName: "feeTo" }),
  publicClient.readContract({ address: CONTRACTS.v2Factory,    abi: v2FactoryAbi,    functionName: "feeToSetter" }),
  publicClient.readContract({ address: CONTRACTS.v3Factory,    abi: v3FactoryAbi,    functionName: "owner" }),
  publicClient.readContract({ address: CONTRACTS.dexFeeManager, abi: feeManagerAbi,  functionName: "feeReceiver" }),
]);
 
// per-pool: nibble 0 is token0's side, nibble 1 is token1's
const slot0 = await publicClient.readContract({ address: pool, abi: v3PoolAbi, functionName: "slot0" });
const feeProtocol0 = Number(slot0[5]) % 16;
const feeProtocol1 = Number(slot0[5]) >> 4;

Configured values can change through governance. Read the relevant getter before building a transaction that depends on one.