> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mobula.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Smart Contract Integration

> Integrate the MobulaRouter EVM contract directly from Solidity. Covers both entry points, struct ABI, custom errors, fee model, msg.value rules, pause/upgrade policy and a Foundry test snippet.

## Overview

`MobulaRouter` is the EVM smart contract that powers every swap returned by the [Swap Quoting](/rest-api-reference/endpoint/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.

<Tip>
  If you only sign-and-send what the API returns, you do **not** need this page — the API encodes the calldata for you. Read [Swap Quoting](/rest-api-reference/endpoint/swap-quoting) instead.
</Tip>

### Two entry points

| Entry point             | Use when…                                                                                                        | Pool universe                                                                                    | Pausable |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | -------- |
| `executeAggregatorSwap` | You already have aggregator calldata (1inch, 0x, KyberSwap, LiFi, …) and want fee + slippage enforcement on top. | Whatever the aggregator routes through.                                                          | **No**   |
| `executeRoute`          | You build the route yourself and want explicit, factory-validated pools with no third-party code.                | UniswapV2, UniswapV3, SolidlyV2, LiquidityBookV2, UniswapV4, SolidlyV3, FourMeme, PCSInfinityCL. | **Yes**  |

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.

| Chain        | Chain ID | Router (proxy)                               | Explorer                                                                                                   |
| ------------ | -------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| Ethereum     | `1`      | `0x0005ea683517b3a4463bfd798c9850Ad2d586795` | [Etherscan](https://etherscan.io/address/0x0005ea683517b3a4463bfd798c9850Ad2d586795)                       |
| Optimism     | `10`     | `0x0005ea38EB0a69D1253508EBDBdB9eA8Cb26B5Ef` | [Optimistic Etherscan](https://optimistic.etherscan.io/address/0x0005ea38EB0a69D1253508EBDBdB9eA8Cb26B5Ef) |
| BNB Chain    | `56`     | `0x0005ea70fa6dbd2efd17697d2351301adb6318b2` | [BscScan](https://bscscan.com/address/0x0005ea70fa6dbd2efd17697d2351301adb6318b2)                          |
| Gnosis       | `100`    | `0x40A9849E3bfcf0f3d3be9dfaE3C997aCa19c19ED` | [GnosisScan](https://gnosisscan.io/address/0x40A9849E3bfcf0f3d3be9dfaE3C997aCa19c19ED)                     |
| Polygon      | `137`    | `0x0005ea38EB0a69D1253508EBDBdB9eA8Cb26B5Ef` | [PolygonScan](https://polygonscan.com/address/0x0005ea38EB0a69D1253508EBDBdB9eA8Cb26B5Ef)                  |
| Monad        | `143`    | `0x40A9849E3bfcf0f3d3be9dfaE3C997aCa19c19ED` | [Monvision](https://mainnet-beta.monvision.io/address/0x40A9849E3bfcf0f3d3be9dfaE3C997aCa19c19ED)          |
| Mantle       | `5000`   | `0x40A9849E3bfcf0f3d3be9dfaE3C997aCa19c19ED` | [Mantle Explorer](https://explorer.mantle.xyz/address/0x40A9849E3bfcf0f3d3be9dfaE3C997aCa19c19ED)          |
| MegaETH      | `4326`   | `0x40A9849E3bfcf0f3d3be9dfaE3C997aCa19c19ED` | [MegaETH Blockscout](https://megaeth.blockscout.com/address/0x40A9849E3bfcf0f3d3be9dfaE3C997aCa19c19ED)    |
| Base         | `8453`   | `0x0005ea38EB0a69D1253508EBDBdB9eA8Cb26B5Ef` | [BaseScan](https://basescan.org/address/0x0005ea38EB0a69D1253508EBDBdB9eA8Cb26B5Ef)                        |
| Arbitrum One | `42161`  | `0x0005ea38EB0a69D1253508EBDBdB9eA8Cb26B5Ef` | [Arbiscan](https://arbiscan.io/address/0x0005ea38EB0a69D1253508EBDBdB9eA8Cb26B5Ef)                         |
| Avalanche C  | `43114`  | `0x0005ea38EB0a69D1253508EBDBdB9eA8Cb26B5Ef` | [SnowTrace](https://snowtrace.io/address/0x0005ea38EB0a69D1253508EBDBdB9eA8Cb26B5Ef)                       |
| Linea        | `59144`  | `0x40A9849E3bfcf0f3d3be9dfaE3C997aCa19c19ED` | [LineaScan](https://lineascan.build/address/0x40A9849E3bfcf0f3d3be9dfaE3C997aCa19c19ED)                    |
| Blast        | `81457`  | `0x40A9849E3bfcf0f3d3be9dfaE3C997aCa19c19ED` | [BlastScan](https://blastscan.io/address/0x40A9849E3bfcf0f3d3be9dfaE3C997aCa19c19ED)                       |
| Scroll       | `534352` | `0x40A9849E3bfcf0f3d3be9dfaE3C997aCa19c19ED` | [Scrollscan](https://scrollscan.com/address/0x40A9849E3bfcf0f3d3be9dfaE3C997aCa19c19ED)                    |

<Note>
  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.
</Note>

## Solidity interface & ABI

Two artifacts are published — pick whichever your toolchain expects:

| Artifact                                                             | Hosted in docs                                   | Hosted on GitHub Gist (raw, hot-linkable)                                                                          |
| -------------------------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
| **`IMobulaRouter.sol`** — drop-in Solidity interface                 | [`IMobulaRouter.sol`](IMobulaRouter.sol)         | [gist raw](https://gist.githubusercontent.com/NBMSacha/97628523b02dc9d4f05c0b79adf623a3/raw/IMobulaRouter.sol)     |
| **`MobulaRouter.abi.json`** — full ABI (functions + events + errors) | [`MobulaRouter.abi.json`](MobulaRouter.abi.json) | [gist raw](https://gist.githubusercontent.com/NBMSacha/97628523b02dc9d4f05c0b79adf623a3/raw/MobulaRouter.abi.json) |

Gist URL (browse both files): [gist.github.com/NBMSacha/97628523b02dc9d4f05c0b79adf623a3](https://gist.github.com/NBMSacha/97628523b02dc9d4f05c0b79adf623a3).

To wire the interface into a Foundry / Hardhat project:

```solidity theme={null}
import { IMobulaRouter } from "./IMobulaRouter.sol";
```

To load the ABI in a TypeScript front-end with viem / ethers:

```ts theme={null}
import abi from "./MobulaRouter.abi.json" assert { type: "json" };
// or fetch from the gist:
// const abi = await fetch("https://gist.githubusercontent.com/NBMSacha/97628523b02dc9d4f05c0b79adf623a3/raw/MobulaRouter.abi.json").then(r => r.json());
```

Highlights — full file in the linked artifacts:

```solidity theme={null}
interface IMobulaRouter {
    enum ProtocolType {
        UniswapV2, UniswapV3, SolidlyV2, LiquidityBookV2,
        UniswapV4, SolidlyV3, FourMeme, PCSInfinityCL
    }

    struct AggregatorSwapParams {
        address target;
        address sellToken;
        address buyToken;
        uint256 sellAmount;
        uint256 minBuyAmountToUser;
        address beneficiary;
        uint256 deadline;
        uint16  feeBps;
        address wethOverride;
        bytes   data;
    }

    struct RouteStep {
        address pool;
        address tokenIn;
        address tokenOut;
        ProtocolType protocol;
        uint24 v3Fee;
        int24  tickSpacing;
        address hooks;
    }

    struct SecureRoute {
        RouteStep[] steps;
        uint256 minBuyAmountToUser;
        address beneficiary;
        uint256 deadline;
        uint16  feeBps;
        address wethOverride;
        bool    unwrapOutput;
    }

    function executeAggregatorSwap(AggregatorSwapParams calldata params) external payable;
    function executeRoute(address sellToken, uint256 sellAmount, SecureRoute calldata route) external payable;
}
```

The runtime ABI JSON is committed in the contract artifacts at `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:

| Sell side                   | `sellToken` value | `msg.value`                 |
| --------------------------- | ----------------- | --------------------------- |
| Native (ETH/BNB/MATIC/AVAX) | `address(0)`      | **MUST equal `sellAmount`** |
| ERC-20                      | token address     | **MUST be `0`**             |

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()`.

<Note>
  **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.
</Note>

## `executeAggregatorSwap` — call an aggregator with safety rails

```solidity theme={null}
function executeAggregatorSwap(AggregatorSwapParams calldata params) external payable;
```

What the contract does, in order:

1. `block.timestamp <= deadline`, else `DeadlineExpired()`.
2. `isAllowedTarget[target] == true`, else `TargetNotAllowed()`.
3. `beneficiary != 0`, else `ZeroBeneficiary()`.
4. Pull funds (`msg.value` for native, `transferFrom` + `approve(target)` for ERC-20).
5. Snapshot `buyToken` balance, `target.call{value: msg.value}(data)`.
6. If the call reverts, bubble the message via `CallFailed(string)`.
7. Compute output delta. If 0 → `NoOutputReceived()`.
8. Apply `feeBps` (≤ 500), enforce `output - fee >= minBuyAmountToUser`, else `NetOutputTooLow()`.
9. Send fee to `feeAddress` (owner-controlled), remainder to `beneficiary`.

### `AggregatorSwapParams` reference

| Field                | Type      | Notes                                                                                   |
| -------------------- | --------- | --------------------------------------------------------------------------------------- |
| `target`             | `address` | Aggregator router. Must be in `isAllowedTarget`.                                        |
| `sellToken`          | `address` | `address(0)` for native.                                                                |
| `buyToken`           | `address` | `address(0)` for native. Output is unwrapped from WETH if the aggregator returned WETH. |
| `sellAmount`         | `uint256` | Amount of `sellToken` to spend.                                                         |
| `minBuyAmountToUser` | `uint256` | **Post-fee** minimum. See [Slippage semantics](#slippage-semantics-post-fee-minimum).   |
| `beneficiary`        | `address` | Receives the net output. Cannot be zero.                                                |
| `deadline`           | `uint256` | Unix seconds.                                                                           |
| `feeBps`             | `uint16`  | ≤ `MAX_FEE_BPS = 500` (5%).                                                             |
| `wethOverride`       | `address` | Optional — defaults to the stored `WETH` if zero.                                       |
| `data`               | `bytes`   | Calldata forwarded to `target`.                                                         |

## `executeRoute` — direct, factory-validated swaps

```solidity theme={null}
function executeRoute(
    address sellToken,
    uint256 sellAmount,
    SecureRoute calldata route
) external payable;
```

Each `RouteStep` references a pool. Before swapping, the router:

1. Resolves the pool's factory (via `factory()` for V2/V3/Solidly, `getFactory()` for LB, or `factoryRegistry[step.pool]` for V4 / PCSInfinityCL / FourMeme).
2. Confirms the factory is registered with the matching `ProtocolType`.
3. Confirms the pool's tokens match `tokenIn` / `tokenOut`.
4. Confirms the factory advertises this exact pool for the (tokenIn, tokenOut, fee/stable/tickSpacing) tuple.

Any mismatch reverts with `PoolNotFromFactory()`, `FactoryNotRegistered()`, `InvalidProtocolType()` or `PoolTokenMismatch()`. Steps must chain — `route.steps[i].tokenOut == route.steps[i+1].tokenIn`, else `StepChainBroken()`.

### `RouteStep` reference

| Field         | Type           | Notes                                                                                                                 |
| ------------- | -------------- | --------------------------------------------------------------------------------------------------------------------- |
| `pool`        | `address`      | Pool / TokenManager / PoolManager.                                                                                    |
| `tokenIn`     | `address`      | Input token.                                                                                                          |
| `tokenOut`    | `address`      | Output token.                                                                                                         |
| `protocol`    | `ProtocolType` | See enum below.                                                                                                       |
| `v3Fee`       | `uint24`       | Multi-purpose: V3 fee tier; `1`=stable for SolidlyV2; LB binStep; FourMeme `1`=buy / `0`=sell. Ignored for UniswapV2. |
| `tickSpacing` | `int24`        | V4 / SolidlyV3 / PCSInfinityCL.                                                                                       |
| `hooks`       | `address`      | V4 / PCSInfinityCL hook contract.                                                                                     |

### `SecureRoute` reference

| Field                | Type          | Notes                                                                    |
| -------------------- | ------------- | ------------------------------------------------------------------------ |
| `steps`              | `RouteStep[]` | At least one. Empty → `NoSteps()`.                                       |
| `minBuyAmountToUser` | `uint256`     | **Post-fee** minimum.                                                    |
| `beneficiary`        | `address`     | Receives the net output.                                                 |
| `deadline`           | `uint256`     | Unix seconds.                                                            |
| `feeBps`             | `uint16`      | ≤ 500.                                                                   |
| `wethOverride`       | `address`     | Optional WETH override.                                                  |
| `unwrapOutput`       | `bool`        | If true and the final tokenOut is WETH, unwrap to native before sending. |

### `ProtocolType` enum

| Value             | Index | Notes                                                      |
| ----------------- | ----- | ---------------------------------------------------------- |
| `UniswapV2`       | 0     | Standard 0.3% V2 (Uniswap, SushiSwap, PancakeSwap V2, …).  |
| `UniswapV3`       | 1     | Concentrated liquidity. `v3Fee` = pool fee tier.           |
| `SolidlyV2`       | 2     | Velodrome / Aerodrome / etc. `v3Fee` = stable flag (0/1).  |
| `LiquidityBookV2` | 3     | Trader Joe LB. `v3Fee` = binStep.                          |
| `UniswapV4`       | 4     | Singleton PoolManager. Requires registered StateView.      |
| `SolidlyV3`       | 5     | Aerodrome CL / Slipstream. Same swap surface as V3.        |
| `FourMeme`        | 6     | BNB-chain bonding curve. `pool` is the TokenManager.       |
| `PCSInfinityCL`   | 7     | PancakeSwap Infinity CL on BSC. Requires registered Vault. |

## Fee model

| Field                    | Limit                        | Where it comes from                                               |
| ------------------------ | ---------------------------- | ----------------------------------------------------------------- |
| `feeBps` (per call)      | 0 – 500 (5%) — `MAX_FEE_BPS` | Caller-supplied.                                                  |
| `feeAddress` (recipient) | Owner-controlled.            | Stored on the contract; updated only by `setFeeAddress(address)`. |

```solidity theme={null}
uint256 fee   = (amountOut * feeBps) / 10_000;
uint256 toUser = amountOut - fee;
require(toUser >= minBuyAmountToUser, NetOutputTooLow());
```

<Warning>
  **Third parties cannot route fees to their own wallet on-chain.** `feeAddress` is a single owner-controlled storage slot. If you need partner-revenue routing on-chain, you build it on top — typically by post-processing the output yourself, or by using the API's higher-level fee plumbing. The router only knows one recipient.
</Warning>

`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`:

```solidity theme={null}
uint256 fee   = (amountOut * feeBps) / BPS_DENOMINATOR;
uint256 toUser = amountOut - fee;
if (toUser < minToUser) revert NetOutputTooLow();
```

Set `minBuyAmountToUser = expectedOut * (1 - slippageTolerance) * (1 - feeBps / 10_000)`, not just `expectedOut * (1 - slippage)`.

## Custom error reference

Custom error names live in `MobulaRouter.sol:168-192`. Selectors are `bytes4(keccak256(signature))`.

| Error                    | Selector     | When it reverts                                                           |
| ------------------------ | ------------ | ------------------------------------------------------------------------- |
| `DeadlineExpired()`      | `0x1ab7da6b` | `block.timestamp > deadline`.                                             |
| `TargetNotAllowed()`     | `0x48cbf26d` | Aggregator target not in whitelist.                                       |
| `ZeroBeneficiary()`      | `0x776cceeb` | `beneficiary == address(0)`.                                              |
| `ZeroAmount()`           | `0x1f2a2005` | `sellAmount == 0` or zero net output.                                     |
| `TransferFromFailed()`   | `0x7939f424` | `transferFrom` / `transfer` returned false or reverted.                   |
| `ApprovalFailed()`       | `0x8164f842` | Internal `approve` to a spender failed.                                   |
| `NoOutputReceived()`     | `0xbc35435a` | Swap produced 0 of `buyToken`.                                            |
| `NetOutputTooLow()`      | `0x12091c42` | Post-fee output below `minBuyAmountToUser`.                               |
| `FeeTooHigh()`           | `0xcd4e6167` | `feeBps > 500`.                                                           |
| `NoSteps()`              | `0x8fd6f281` | `route.steps.length == 0`.                                                |
| `FirstSellMismatch()`    | `0xaf08899e` | `steps[0].tokenIn` doesn't match `sellToken` (or WETH for native).        |
| `CallFailed(string)`     | `0xb5e1dc2d` | Aggregator target reverted; reason bubbled.                               |
| `ZeroAddress()`          | `0xd92e233d` | Setter / recovery received the zero address.                              |
| `IncorrectETHAmount()`   | `0x201c04ab` | `msg.value` doesn't match the rule above.                                 |
| `ETHTransferFailed()`    | `0xb12d13eb` | Native send to fee or beneficiary failed.                                 |
| `ArrayLengthMismatch()`  | `0xa24a13a6` | `revokeApprovals` length mismatch.                                        |
| `EmptyArray()`           | `0x521299a9` | `revokeApprovals` empty.                                                  |
| `FactoryNotRegistered()` | `0x6bf0bd6a` | Pool's factory (or pool itself for V4/PCS-CL/FourMeme) not registered.    |
| `PoolNotFromFactory()`   | `0x3a19268b` | Factory does not advertise this pool for the (tokens, fee/spacing) tuple. |
| `InvalidProtocolType()`  | `0xa87da298` | Registered factory doesn't match `step.protocol`.                         |
| `PoolTokenMismatch()`    | `0x04a6a570` | Pool tokens don't match `tokenIn` / `tokenOut`.                           |
| `StepChainBroken()`      | `0xfc05df26` | `steps[i].tokenOut != steps[i+1].tokenIn`.                                |
| `UnsupportedProtocol()`  | `0x743f26e1` | Protocol enum value not handled.                                          |
| `InvalidCallback()`      | `0xf7a632f5` | A swap callback came from an unexpected pool.                             |
| `VaultNotRegistered()`   | `0xeeb4f612` | PCSInfinityCL pool with no vault registered.                              |

## Pause and upgrade policy

* **`executeRoute`** is wrapped in `whenNotPaused`. The owner can call `pauseExecuteRoute()` / `unpauseExecuteRoute()` and emits `ExecuteRoutePaused` / `ExecuteRouteUnpaused`. While paused, every call reverts with the standard OpenZeppelin `EnforcedPause()` error.
* **`executeAggregatorSwap`** is **NOT** behind the pause modifier. It only checks the per-call deadline + target whitelist. If you want to coordinate a stop, the owner removes `target` from `isAllowedTarget` (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 check `version()` (currently `"2.15.0"`) before relying on a feature in production.

## Reentrancy and callback guarantees

Both entry points hold a `nonReentrant` 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

```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import { IMobulaRouter } from "./IMobulaRouter.sol";

interface IERC20 {
    function approve(address, uint256) external returns (bool);
}

contract Caller {
    IMobulaRouter immutable ROUTER;

    constructor(address router) { ROUTER = IMobulaRouter(router); }

    /// Native ETH → ERC-20 via a pre-built aggregator quote.
    function buy(
        address aggregator,    // 1inch / 0x / KyberSwap router
        address buyToken,
        uint256 minOut,        // post-fee minimum
        bytes calldata aggData
    ) external payable {
        IMobulaRouter.AggregatorSwapParams memory p = IMobulaRouter.AggregatorSwapParams({
            target:              aggregator,
            sellToken:           address(0),          // native
            buyToken:            buyToken,
            sellAmount:          msg.value,
            minBuyAmountToUser:  minOut,
            beneficiary:         msg.sender,
            deadline:            block.timestamp + 300,
            feeBps:              0,                    // no partner fee
            wethOverride:        address(0),           // default WETH
            data:                aggData
        });

        ROUTER.executeAggregatorSwap{ value: msg.value }(p);
    }

    /// ERC-20 → ERC-20 single-hop V3 via executeRoute.
    /// Caller MUST approve ROUTER for `amountIn` of `tokenIn` first.
    function swapV3(
        address pool,
        address tokenIn,
        address tokenOut,
        uint24  feeTier,
        uint256 amountIn,
        uint256 minOut
    ) external {
        IMobulaRouter.RouteStep[] memory steps = new IMobulaRouter.RouteStep[](1);
        steps[0] = IMobulaRouter.RouteStep({
            pool:        pool,
            tokenIn:     tokenIn,
            tokenOut:    tokenOut,
            protocol:    IMobulaRouter.ProtocolType.UniswapV3,
            v3Fee:       feeTier,
            tickSpacing: int24(0),
            hooks:       address(0)
        });

        IMobulaRouter.SecureRoute memory route = IMobulaRouter.SecureRoute({
            steps:               steps,
            minBuyAmountToUser:  minOut,
            beneficiary:         msg.sender,
            deadline:            block.timestamp + 300,
            feeBps:              0,
            wethOverride:        address(0),
            unwrapOutput:        false
        });

        ROUTER.executeRoute(tokenIn, amountIn, route);
    }
}
```

## Foundry test snippet

A self-contained fork test, mirroring `contracts/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.

```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import "forge-std/Test.sol";
import { MobulaRouter } from "contracts/evm/src/MobulaRouter.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

interface IERC20 { function balanceOf(address) external view returns (uint256); }

contract MobulaRouterIntegrationFork is Test {
    MobulaRouter router;

    address constant WETH                = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1;
    address constant USDC                = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831;
    address constant UNI_V3_ROUTER       = 0xE592427A0AEce92De3Edee1F18E0157C05861564;

    address user        = makeAddr("user");
    address feeRecipient = makeAddr("fees");

    function setUp() public {
        vm.createSelectFork(vm.envString("ARBITRUM_RPC"));

        MobulaRouter impl = new MobulaRouter();
        bytes memory init = abi.encodeWithSelector(
            MobulaRouter.initialize.selector, feeRecipient, address(this), WETH
        );
        router = MobulaRouter(payable(address(new ERC1967Proxy(address(impl), init))));

        router.setAllowedTarget(UNI_V3_ROUTER, true);
    }

    function test_AggregatorSwap_ETH_to_USDC() public {
        uint256 ethIn = 0.1 ether;
        vm.deal(user, ethIn);

        bytes memory data = abi.encodeWithSignature(
            "exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))",
            WETH, USDC, uint24(500), address(router),
            block.timestamp, ethIn, uint256(0), uint160(0)
        );

        MobulaRouter.AggregatorSwapParams memory p = MobulaRouter.AggregatorSwapParams({
            target:              UNI_V3_ROUTER,
            sellToken:           address(0),
            buyToken:            USDC,
            sellAmount:          ethIn,
            minBuyAmountToUser:  0,
            beneficiary:         user,
            deadline:            block.timestamp + 300,
            feeBps:              50,            // 0.5%
            wethOverride:        address(0),
            data:                data
        });

        vm.prank(user);
        router.executeAggregatorSwap{ value: ethIn }(p);

        assertGt(IERC20(USDC).balanceOf(user),         0, "user got USDC");
        assertGt(IERC20(USDC).balanceOf(feeRecipient), 0, "fees collected");
    }
}
```

Run it:

```bash theme={null}
forge test --match-contract MobulaRouterIntegrationFork --fork-url $ARBITRUM_RPC -vvv
```

For the full set of fork tests (V2, V3, Solidly, LB, V4, multi-hop, fee-on-transfer) see `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 `minBuyAmountToUser` as a **post-fee** value.
* [ ] Approve `sellAmount` (not `max`) if you do not want to leave a standing approval.

## See also

* [Swap Quoting REST endpoint](/rest-api-reference/endpoint/swap-quoting) — the high-level API that produces ready-to-sign calldata.
* [Complete Swap Guide](/guides/complete-swap-guide) — end-to-end TypeScript walkthrough.
* Canonical artifacts:
  * Interface: [`IMobulaRouter.sol`](IMobulaRouter.sol) — also on [GitHub Gist](https://gist.github.com/NBMSacha/97628523b02dc9d4f05c0b79adf623a3) for hot-linking.
  * Full ABI: [`MobulaRouter.abi.json`](MobulaRouter.abi.json) — same Gist.
