Overview
MobulaRouter is the EVM smart contract that powers every swap returned by the Swap Quoting API. Most integrators sign and send the calldata that the API returns and never touch the contract directly.
This page is for the smaller set of integrators that build directly on top of the router from Solidity — market makers, MEV/arb teams, custom routing front-ends, on-chain bots. It documents the ABI, struct layout, error codes, and operational rules required to call the contract safely without reading the implementation.
Two entry points
Both functions are
nonReentrant and payable. executeAggregatorSwap forwards calldata to a target address that must be in the owner-managed whitelist (isAllowedTarget). executeRoute validates every pool through its factory before swapping — no arbitrary code execution.
Deployed addresses
The router is a UUPS-upgradeable proxy. Always interact with the proxy address — implementation addresses change across upgrades.The router proxy address differs across chains. Don’t hardcode a single address — always look it up from the table above, from
getMobulaRouterAddress(chainId) in @mobula/execution-engine, or from the evm.transaction.to field of every quote response.Solidity interface & ABI
Two artifacts are published — pick whichever your toolchain expects:
Gist URL (browse both files): gist.github.com/NBMSacha/97628523b02dc9d4f05c0b79adf623a3.
To wire the interface into a Foundry / Hardhat project:
contracts/evm/out/MobulaRouter.sol/MobulaRouter.json after running forge build. Copy the abi field directly into your client.
Approvals and msg.value
There is exactly one rule for every entry point:
ERC-20 sellers MUST
approve(router, sellAmount) (or type(uint256).max) on sellToken before calling. The router uses a SafeERC20-style transferFrom (_pullTokenFromUser) that handles non-standard tokens (USDT, etc.) and supports fee-on-transfer (the actual received amount is used downstream).
Mismatches revert with IncorrectETHAmount(). Insufficient approvals revert with TransferFromFailed().
Pre-approval pattern. Approve once for
type(uint256).max and reuse — the router itself never persists approvals to external targets without zeroing first (see _ensureAllowance), so a stale aggregator approval can’t be exploited.executeAggregatorSwap — call an aggregator with safety rails
block.timestamp <= deadline, elseDeadlineExpired().isAllowedTarget[target] == true, elseTargetNotAllowed().beneficiary != 0, elseZeroBeneficiary().- Pull funds (
msg.valuefor native,transferFrom+approve(target)for ERC-20). - Snapshot
buyTokenbalance,target.call{value: msg.value}(data). - If the call reverts, bubble the message via
CallFailed(string). - Compute output delta. If 0 →
NoOutputReceived(). - Apply
feeBps(≤ 500), enforceoutput - fee >= minBuyAmountToUser, elseNetOutputTooLow(). - Send fee to
feeAddress(owner-controlled), remainder tobeneficiary.
AggregatorSwapParams reference
executeRoute — direct, factory-validated swaps
RouteStep references a pool. Before swapping, the router:
- Resolves the pool’s factory (via
factory()for V2/V3/Solidly,getFactory()for LB, orfactoryRegistry[step.pool]for V4 / PCSInfinityCL / FourMeme). - Confirms the factory is registered with the matching
ProtocolType. - Confirms the pool’s tokens match
tokenIn/tokenOut. - Confirms the factory advertises this exact pool for the (tokenIn, tokenOut, fee/stable/tickSpacing) tuple.
PoolNotFromFactory(), FactoryNotRegistered(), InvalidProtocolType() or PoolTokenMismatch(). Steps must chain — route.steps[i].tokenOut == route.steps[i+1].tokenIn, else StepChainBroken().
RouteStep reference
SecureRoute reference
ProtocolType enum
Fee model
FeeTooHigh() reverts when feeBps > 500.
Slippage semantics (post-fee minimum)
minBuyAmountToUser is the amount the beneficiary actually receives, after the fee is taken — not the gross output of the swap. From _distributeFeesAndSend:
minBuyAmountToUser = expectedOut * (1 - slippageTolerance) * (1 - feeBps / 10_000), not just expectedOut * (1 - slippage).
Custom error reference
Custom error names live inMobulaRouter.sol:168-192. Selectors are bytes4(keccak256(signature)).
Pause and upgrade policy
executeRouteis wrapped inwhenNotPaused. The owner can callpauseExecuteRoute()/unpauseExecuteRoute()and emitsExecuteRoutePaused/ExecuteRouteUnpaused. While paused, every call reverts with the standard OpenZeppelinEnforcedPause()error.executeAggregatorSwapis NOT behind the pause modifier. It only checks the per-call deadline + target whitelist. If you want to coordinate a stop, the owner removestargetfromisAllowedTarget(one-call kill switch).- Upgradeability: UUPS proxy (
UUPSUpgradeable+ owner-gated_authorizeUpgrade). Storage layout is documented in the implementation header. New chains and new protocol types ship as upgrades — always checkversion()(currently"2.15.0") before relying on a feature in production.
Reentrancy and callback guarantees
Both entry points hold anonReentrant guard for the entire swap lifecycle. Pool callbacks (uniswapV3SwapCallback, pancakeV3SwapCallback, unlockCallback for V4, lockAcquired for PCS Infinity) check msg.sender == _activeCallbackPool and revert with InvalidCallback() otherwise — random pools cannot trigger them.
If you fork or wrap the router in your own contract, do not call back into the router from your own callbacks; you’ll hit the reentrancy guard.
Solidity caller example
Foundry test snippet
A self-contained fork test, mirroringcontracts/evm/test/MobulaRouter.fork.t.sol. Deploys a fresh proxy on an Arbitrum fork, whitelists Uniswap V3, and runs a native → USDC swap end-to-end.
contracts/evm/test/MobulaRouter.fork.t.sol in the monorepo.
Operational checklist
Before sending a transaction in production:- Hit
version()— confirm you’re on the expected major. - Read
feeAddress()if you display fee disclosures. - For aggregator swaps: confirm
isAllowedTarget(target) == true. - For routes: confirm every pool’s factory is registered (off-chain check via
factoryRegistry). - Set
deadline = block.timestamp + N— never far in the future. - Compute
minBuyAmountToUseras a post-fee value. - Approve
sellAmount(notmax) if you do not want to leave a standing approval.
See also
- Swap Quoting REST endpoint — the high-level API that produces ready-to-sign calldata.
- Complete Swap Guide — end-to-end TypeScript walkthrough.
- Canonical artifacts:
- Interface:
IMobulaRouter.sol— also on GitHub Gist for hot-linking. - Full ABI:
MobulaRouter.abi.json— same Gist.
- Interface: