MotionSwapRouter

0x33bb3226531fa18a6253dfd01538c76fee5acc37

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
12,179 bytes
ABI entries
57
8 read · 12 write
License
none

Contract information

Address
0x33bb3226531fa18a6253dfd01538c76fee5acc37
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xc8aadcf69a…649106a7cf

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (8)

ROUTER_VERSION()uint256
launchManager()address
motionHook()address
owner()address
pendingOwner()address
poolManager()address
routeOf(address)tuple[]
weth()address

Events (7)

BuyWithEthExactInputSwapOwnershipTransferStartedOwnershipTransferredRouteClearedRouteSetSellToEth

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "contract ILaunchManager",
        "name": "launchManager_",
        "type": "address"
      },
      {
        "internalType": "contract IWETH",
        "name": "weth_",
        "type": "address"
      },
      {
        "internalType": "contract IHooks",
        "name": "motionHook_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "DeadlineExpired",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EthTransferFailed",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "paid",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "expected",
        "type": "uint256"
      }
    ],
    "name": "IncorrectSettlement",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidAmountIn",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidCallback",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidManager",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidNativeValue",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidPoolManager",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidRecipient",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidSwapDelta",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidWeth",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyPoolManager",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlySelf",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amountInUsed",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "expectedAmountIn",
        "type": "uint256"
      }
    ],
    "name": "PartialFillNotAllowed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "hopIndex",
        "type": "uint256"
      }
    ],
    "name": "RouteDisconnected",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "hopIndex",
        "type": "uint256"
      }
    ],
    "name": "RouteHopHasHook",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      }
    ],
    "name": "RouteNotConfigured",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RouteTooLong",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RouteWrongEnd",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RouteWrongStart",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amountOutMinimum",
        "type": "uint256"
      }
    ],
    "name": "TooLittleOutput",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnexpectedNativeValue",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnexpectedV3Callback",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnknownToken",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "payer",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethRefunded",
        "type": "uint256"
      }
    ],
    "name": "BuyWithEth",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "payer",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "currencyIn",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "currencyOut",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      }
    ],
    "name": "ExactInputSwap",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "quote",
        "type": "address"
      }
    ],
    "name": "RouteCleared",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "hops",
        "type": "uint256"
      }
    ],
    "name": "RouteSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "payer",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      }
    ],
    "name": "SellToEth",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "ROUTER_VERSION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "buyWithEth",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      }
    ],
    "name": "clearRoute",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "components": [
              {
                "internalType": "Currency",
                "name": "currency0",
                "type": "address"
              },
              {
                "internalType": "Currency",
                "name": "currency1",
                "type": "address"
              },
              {
                "internalType": "uint24",
                "name": "fee",
                "type": "uint24"
              },
              {
                "internalType": "int24",
                "name": "tickSpacing",
                "type": "int24"
              },
              {
                "internalType": "contract IHooks",
                "name": "hooks",
                "type": "address"
              }
            ],
            "internalType": "struct PoolKey",
            "name": "key",
            "type": "tuple"
          },
          {
            "internalType": "bool",
            "name": "zeroForOne",
            "type": "bool"
          },
          {
            "internalType": "uint256",
            "name": "amountIn",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "minAmountOut",
            "type": "uint256"
          },
          {
            "internalType": "uint160",
            "name": "sqrtPriceLimitX96",
            "type": "uint160"
          },
          {
            "internalType": "address",
            "name": "recipient",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "deadline",
            "type": "uint256"
          },
          {
            "internalType": "bytes",
            "name": "hookData",
            "type": "bytes"
          }
        ],
        "internalType": "struct MotionSwapRouter.ExactInputSingleParams",
        "name": "params",
        "type": "tuple"
      }
    ],
    "name": "exactInputSingle",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amountInUsed",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchManager",
    "outputs": [
      {
        "internalType": "contract ILaunchManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "motionHook",
    "outputs": [
      {
        "internalType": "contract IHooks",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      }
    ],
    "name": "routeOf",
    "outputs": [
      {
        "components": [
          {
            "internalType": "enum MotionSwapRouter.Venue",
            "name": "venue",
            "type": "uint8"
          },
          {
            "internalType": "address",
            "name": "pool",
            "type": "address"
          },
          {
            "components": [
              {
                "internalType": "Currency",
                "name": "currency0",
                "type": "address"
              },
              {
                "internalType": "Currency",
                "name": "currency1",
                "type": "address"
              },
              {
                "internalType": "uint24",
                "name": "fee",
                "type": "uint24"
              },
              {
                "internalType": "int24",
                "name": "tickSpacing",
                "type": "int24"
              },
              {
                "internalType": "contract IHooks",
                "name": "hooks",
                "type": "address"
              }
            ],
            "internalType": "struct PoolKey",
            "name": "key",
            "type": "tuple"
          }
        ],
        "internalType": "struct MotionSwapRouter.Hop[]",
        "name": "",
        "type": "tuple[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "routeQuoteToEthSelf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokensIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "sellToEth",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "components": [
          {
            "internalType": "enum MotionSwapRouter.Venue",
            "name": "venue",
            "type": "uint8"
          },
          {
            "internalType": "address",
            "name": "pool",
            "type": "address"
          },
          {
            "components": [
              {
                "internalType": "Currency",
                "name": "currency0",
                "type": "address"
              },
              {
                "internalType": "Currency",
                "name": "currency1",
                "type": "address"
              },
              {
                "internalType": "uint24",
                "name": "fee",
                "type": "uint24"
              },
              {
                "internalType": "int24",
                "name": "tickSpacing",
                "type": "int24"
              },
              {
                "internalType": "contract IHooks",
                "name": "hooks",
                "type": "address"
              }
            ],
            "internalType": "struct PoolKey",
            "name": "key",
            "type": "tuple"
          }
        ],
        "internalType": "struct MotionSwapRouter.Hop[]",
        "name": "route",
        "type": "tuple[]"
      }
    ],
    "name": "setRoute",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minQuoteOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "swapExactEthForQuote",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "quoteOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "int256",
        "name": "amount0Delta",
        "type": "int256"
      },
      {
        "internalType": "int256",
        "name": "amount1Delta",
        "type": "int256"
      },
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "uniswapV3SwapCallback",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "rawData",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "contract IWETH",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.26;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
import {TransientSlot} from "@openzeppelin/contracts/utils/TransientSlot.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";

import {ILaunchManager} from "./interfaces/ILaunchManager.sol";
import {TokenState} from "./types/LaunchTypes.sol";

interface IWETH is IERC20 {
    function deposit() external payable;
    function withdraw(uint256 amount) external;
}

interface IUniswapV3Pool {
    function token0() external view returns (address);
    function token1() external view returns (address);
    function swap(
        address recipient,
        bool zeroForOne,
        int256 amountSpecified,
        uint160 sqrtPriceLimitX96,
        bytes calldata data
    ) external returns (int256 amount0, int256 amount1);
}

/// @title MotionSwapRouter
/// @notice Production router (version 2). Keeps V1's single-pool exact-input
/// V4 swap and adds owner-pinned multi-venue routes so any launch coin can be
/// bought or sold with native ETH in one transaction, across both lifecycle
/// phases: the bonding curve (via LaunchManager) and the graduated V4 pool.
/// @dev Routes exist because this chain carries parasitic quote pools with
/// confiscatory LP fees; the router only ever crosses pools the owner has
/// pinned. A route walks from native ETH to a quote asset over Uniswap V4 or
/// V3 pools and is reversed on exit. The owner may retarget a route at any
/// time but can never move user funds: the user's single min-output bound is
/// enforced on the final leg, route hops are hookless, and the contract holds
/// no balance between transactions.
contract MotionSwapRouter is IUnlockCallback, ReentrancyGuardTransient, Ownable2Step {
    using BalanceDeltaLibrary for BalanceDelta;
    using CurrencyLibrary for Currency;
    using SafeERC20 for IERC20;
    using TransientSlot for bytes32;
    using TransientSlot for TransientSlot.Bytes32Slot;
    using TransientSlot for TransientSlot.AddressSlot;

    // -------------------------------------------------------------------
    // Types
    // -------------------------------------------------------------------

    enum Venue {
        UniV4,
        UniV3
    }

    struct Hop {
        Venue venue;
        /// @dev V3 pool address; ignored for V4 hops.
        address pool;
        /// @dev V4 pool key; ignored for V3 hops.
        PoolKey key;
    }

    struct ExactInputSingleParams {
        PoolKey key;
        bool zeroForOne;
        uint256 amountIn;
        uint256 minAmountOut;
        uint160 sqrtPriceLimitX96;
        address recipient;
        uint256 deadline;
        bytes hookData;
    }

    struct V4SwapData {
        PoolKey key;
        bool zeroForOne;
        uint256 amountIn;
        uint160 sqrtPriceLimitX96;
        address takeTo;
        address payer;
        bytes hookData;
    }

    // -------------------------------------------------------------------
    // Constants and immutables
    // -------------------------------------------------------------------

    uint256 public constant ROUTER_VERSION = 2;

    /// @dev Transient slots for the callback guards, cleared automatically at
    /// the end of every transaction.
    bytes32 private constant _CALLBACK_HASH_SLOT =
        keccak256("motion.swaprouter.v2.expectedCallbackHash");
    bytes32 private constant _V3_POOL_SLOT = keccak256("motion.swaprouter.v2.expectedV3Pool");

    IPoolManager public immutable poolManager;
    ILaunchManager public immutable launchManager;
    IWETH public immutable weth;
    IHooks public immutable motionHook;

    // -------------------------------------------------------------------
    // Storage
    // -------------------------------------------------------------------

    mapping(address quote => Hop[]) private _routes;

    // -------------------------------------------------------------------
    // Events
    // -------------------------------------------------------------------

    event ExactInputSwap(
        address indexed payer,
        address indexed recipient,
        address indexed currencyIn,
        address currencyOut,
        uint256 amountIn,
        uint256 amountOut
    );
    event RouteSet(address indexed quote, uint256 hops);
    event RouteCleared(address indexed quote);
    event BuyWithEth(
        address indexed payer, address indexed token, uint256 ethIn, uint256 tokensOut, uint256 ethRefunded
    );
    event SellToEth(address indexed payer, address indexed token, uint256 tokensIn, uint256 ethOut);

    // -------------------------------------------------------------------
    // Errors
    // -------------------------------------------------------------------

    error InvalidPoolManager();
    error InvalidManager();
    error InvalidWeth();
    error OnlyPoolManager();
    error OnlySelf();
    error InvalidRecipient();
    error InvalidAmountIn();
    error DeadlineExpired();
    error InvalidNativeValue();
    error UnexpectedNativeValue();
    error InvalidCallback();
    error InvalidSwapDelta();
    error PartialFillNotAllowed(uint256 amountInUsed, uint256 expectedAmountIn);
    error TooLittleOutput(uint256 amountOut, uint256 amountOutMinimum);
    error IncorrectSettlement(uint256 paid, uint256 expected);
    error RouteNotConfigured(address quote);
    error RouteTooLong();
    error RouteDisconnected(uint256 hopIndex);
    error RouteWrongStart();
    error RouteWrongEnd();
    error RouteHopHasHook(uint256 hopIndex);
    error UnknownToken();
    error UnexpectedV3Callback();
    error EthTransferFailed();

    // -------------------------------------------------------------------
    // Constructor
    // -------------------------------------------------------------------

    constructor(IPoolManager poolManager_, ILaunchManager launchManager_, IWETH weth_, IHooks motionHook_, address owner_)
        Ownable(owner_)
    {
        if (address(poolManager_) == address(0) || address(poolManager_).code.length == 0) {
            revert InvalidPoolManager();
        }
        // A code-bearing typo must not silently pass: this view is part of the
        // canonical V4 PoolManager ABI.
        try poolManager_.protocolFeeController() returns (address) {}
        catch {
            revert InvalidPoolManager();
        }
        if (address(launchManager_) == address(0) || address(launchManager_).code.length == 0) revert InvalidManager();
        if (address(weth_) == address(0) || address(weth_).code.length == 0) revert InvalidWeth();
        poolManager = poolManager_;
        launchManager = launchManager_;
        weth = weth_;
        motionHook = motionHook_;
    }

    /// @dev Accepts ETH from WETH withdrawals, PoolManager takes, and
    /// LaunchManager refunds. Every leg forwards its ETH before the call ends,
    /// so the router holds no balance at rest; direct transfers are lost.
    receive() external payable {}

    // -------------------------------------------------------------------
    // Route registry (owner)
    // -------------------------------------------------------------------

    /// @notice Returns the hop list that carries native ETH into `quote`.
    function routeOf(address quote) external view returns (Hop[] memory) {
        return _routes[quote];
    }

    /// @notice Pins the hop list that carries native ETH into `quote`.
    /// @dev Validated hop by hop: the first hop must take native ETH (V4) or
    /// WETH (V3), each hop must consume the previous hop's output token, and
    /// the last hop must emit the quote asset. V4 hops must be hookless: a
    /// hooked bridge pool could confiscate value on the reverse (refund or
    /// sell) leg, which carries no user min-output bound.
    function setRoute(address quote, Hop[] calldata route) external onlyOwner {
        if (route.length == 0 || quote == address(0)) revert RouteNotConfigured(quote);
        if (route.length > 4) revert RouteTooLong();

        delete _routes[quote];
        address current = address(0);
        for (uint256 i = 0; i < route.length; i++) {
            (address tokenA, address tokenB) = _hopTokens(route[i]);
            // A V3 hop fed by native ETH implicitly consumes WETH (auto-wrap).
            address input = current == address(0) && route[i].venue != Venue.UniV4 ? address(weth) : current;
            if (input == tokenA) {
                current = tokenB;
            } else if (input == tokenB) {
                current = tokenA;
            } else {
                revert RouteDisconnected(i);
            }
            if (route[i].venue == Venue.UniV4) {
                if (address(route[i].key.hooks) != address(0)) revert RouteHopHasHook(i);
                if (i == 0 && !route[i].key.currency0.isAddressZero()) revert RouteWrongStart();
            }
            _routes[quote].push(route[i]);
        }
        if (current != quote) revert RouteWrongEnd();
        emit RouteSet(quote, route.length);
    }

    /// @notice Removes the route for `quote`.
    function clearRoute(address quote) external onlyOwner {
        delete _routes[quote];
        emit RouteCleared(quote);
    }

    // -------------------------------------------------------------------
    // Trading (external)
    // -------------------------------------------------------------------

    /// @notice Swaps exactly `amountIn` of one pool currency for the other
    /// through a single V4 pool. Behaviour is identical to router V1.
    /// @dev Fail-closed on partial fills: the quote-input hook fee is assessed
    /// before the pool executes, so accepting a partial fill would charge that
    /// fee against unfilled input.
    function exactInputSingle(ExactInputSingleParams calldata params)
        external
        payable
        nonReentrant
        returns (uint256 amountInUsed, uint256 amountOut)
    {
        if (params.recipient == address(0)) revert InvalidRecipient();
        if (block.timestamp > params.deadline) revert DeadlineExpired();

        Currency input = params.zeroForOne ? params.key.currency0 : params.key.currency1;
        if (input.isAddressZero()) {
            if (msg.value != params.amountIn) revert InvalidNativeValue();
        } else if (msg.value != 0) {
            revert UnexpectedNativeValue();
        }

        (amountInUsed, amountOut) = _v4Swap(
            V4SwapData({
                key: params.key,
                zeroForOne: params.zeroForOne,
                amountIn: params.amountIn,
                sqrtPriceLimitX96: params.sqrtPriceLimitX96,
                takeTo: params.recipient,
                payer: msg.sender,
                hookData: params.hookData
            })
        );
        if (amountOut < params.minAmountOut) revert TooLittleOutput(amountOut, params.minAmountOut);

        Currency output = params.zeroForOne ? params.key.currency1 : params.key.currency0;
        emit ExactInputSwap(
            msg.sender, params.recipient, Currency.unwrap(input), Currency.unwrap(output), amountInUsed, amountOut
        );
    }

    /// @notice Buys `token` with native ETH in one transaction, whatever the
    /// coin's quote asset and lifecycle phase.
    /// @param minTokensOut Lower bound on the coin amount delivered.
    /// @dev A graduating curve buy refunds excess quote; that refund is
    /// converted back to ETH (or returned in the quote asset if routing dust
    /// would revert) so the caller never holds the quote asset.
    function buyWithEth(address token, uint256 minTokensOut, address recipient, uint256 deadline)
        external
        payable
        nonReentrant
        returns (uint256 tokensOut)
    {
        if (recipient == address(0)) recipient = msg.sender;
        if (block.timestamp > deadline) revert DeadlineExpired();
        if (msg.value == 0) revert InvalidAmountIn();

        TokenState memory state = launchManager.tokenStateOf(token);
        if (state.creator == address(0)) revert UnknownToken();
        address quote = state.quote;

        uint256 quoteAmount = quote == address(0) ? msg.value : _routeEthToQuote(quote, msg.value);

        uint256 ethRefunded;
        if (state.migrated) {
            (, tokensOut) = _swapCoinPool(token, quote, quoteAmount, recipient, true);
        } else {
            // Measure the manager's refund as a balance delta around the buy,
            // never as the router's absolute balance: a donated balance must
            // not be attributed to this caller, nor be able to make the refund
            // path revert and brick buys. Only a graduating buy refunds.
            uint256 balanceBefore = _selfBalance(quote);
            if (quote == address(0)) {
                tokensOut = launchManager.buy{value: quoteAmount}(token, quoteAmount, 0, recipient, deadline);
            } else {
                IERC20(quote).forceApprove(address(launchManager), quoteAmount);
                tokensOut = launchManager.buy(token, quoteAmount, 0, recipient, deadline);
                IERC20(quote).forceApprove(address(launchManager), 0);
            }
            uint256 refund = _selfBalance(quote) + quoteAmount - balanceBefore;
            if (refund > 0) ethRefunded = _returnRefundAsEth(quote, refund);
        }
        if (tokensOut < minTokensOut) revert TooLittleOutput(tokensOut, minTokensOut);
        emit BuyWithEth(msg.sender, token, msg.value, tokensOut, ethRefunded);
    }

    /// @notice Swaps native ETH into `quote` through the pinned route and sends
    /// it to `recipient`. Used by LaunchManager to fund an ETH-paid dev buy on a
    /// non-ETH-quoted launch, but callable by anyone as a plain swap.
    /// @param minQuoteOut Lower bound on the quote delivered.
    function swapExactEthForQuote(address quote, uint256 minQuoteOut, address recipient)
        external
        payable
        nonReentrant
        returns (uint256 quoteOut)
    {
        if (recipient == address(0)) revert InvalidRecipient();
        if (msg.value == 0) revert InvalidAmountIn();
        quoteOut = _routeEthToQuote(quote, msg.value);
        if (quoteOut < minQuoteOut) revert TooLittleOutput(quoteOut, minQuoteOut);
        IERC20(quote).safeTransfer(recipient, quoteOut);
    }

    /// @notice Sells `tokensIn` of `token` and pays the caller native ETH in
    /// one transaction, whatever the coin's quote asset and lifecycle phase.
    /// @param minEthOut Lower bound on the ETH delivered.
    function sellToEth(address token, uint256 tokensIn, uint256 minEthOut, address recipient, uint256 deadline)
        external
        nonReentrant
        returns (uint256 ethOut)
    {
        if (recipient == address(0)) recipient = msg.sender;
        if (block.timestamp > deadline) revert DeadlineExpired();
        if (tokensIn == 0) revert InvalidAmountIn();

        TokenState memory state = launchManager.tokenStateOf(token);
        if (state.creator == address(0)) revert UnknownToken();
        address quote = state.quote;

        IERC20(token).safeTransferFrom(msg.sender, address(this), tokensIn);

        uint256 quoteOut;
        if (state.migrated) {
            (, quoteOut) = _swapCoinPool(token, quote, tokensIn, address(this), false);
        } else {
            IERC20(token).forceApprove(address(launchManager), tokensIn);
            quoteOut = launchManager.sell(token, tokensIn, 0, address(this), deadline);
            IERC20(token).forceApprove(address(launchManager), 0);
        }

        ethOut = quote == address(0) ? quoteOut : _routeQuoteToEth(quote, quoteOut);
        if (ethOut < minEthOut) revert TooLittleOutput(ethOut, minEthOut);
        _sendEth(recipient, ethOut);
        emit SellToEth(msg.sender, token, tokensIn, ethOut);
    }

    // -------------------------------------------------------------------
    // Callbacks (external, guarded)
    // -------------------------------------------------------------------

    /// @inheritdoc IUnlockCallback
    function unlockCallback(bytes calldata rawData) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert OnlyPoolManager();
        bytes32 expected = _CALLBACK_HASH_SLOT.asBytes32().tload();
        if (expected == bytes32(0) || keccak256(rawData) != expected) revert InvalidCallback();

        V4SwapData memory data = abi.decode(rawData, (V4SwapData));
        BalanceDelta delta = poolManager.swap(
            data.key,
            IPoolManager.SwapParams({
                zeroForOne: data.zeroForOne,
                amountSpecified: -int256(data.amountIn),
                sqrtPriceLimitX96: data.sqrtPriceLimitX96
            }),
            data.hookData
        );

        int128 inputDelta = data.zeroForOne ? delta.amount0() : delta.amount1();
        int128 outputDelta = data.zeroForOne ? delta.amount1() : delta.amount0();
        if (inputDelta >= 0 || outputDelta <= 0) revert InvalidSwapDelta();

        uint256 amountInUsed = uint256(-int256(inputDelta));
        uint256 amountOut = uint256(uint128(outputDelta));
        if (amountInUsed != data.amountIn) revert PartialFillNotAllowed(amountInUsed, data.amountIn);

        Currency input = data.zeroForOne ? data.key.currency0 : data.key.currency1;
        if (input.isAddressZero()) {
            uint256 paid = poolManager.settle{value: amountInUsed}();
            if (paid != amountInUsed) revert IncorrectSettlement(paid, amountInUsed);
        } else {
            poolManager.sync(input);
            if (data.payer == address(this)) {
                IERC20(Currency.unwrap(input)).safeTransfer(address(poolManager), amountInUsed);
            } else {
                IERC20(Currency.unwrap(input)).safeTransferFrom(data.payer, address(poolManager), amountInUsed);
            }
            uint256 paid = poolManager.settle();
            if (paid != amountInUsed) revert IncorrectSettlement(paid, amountInUsed);
        }

        Currency output = data.zeroForOne ? data.key.currency1 : data.key.currency0;
        poolManager.take(output, data.takeTo, amountOut);

        return abi.encode(amountInUsed, amountOut);
    }

    /// @notice Pays the V3 pool during a router-initiated swap.
    /// @dev Guarded to the pool the router is currently swapping through.
    function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external {
        address expected = _V3_POOL_SLOT.asAddress().tload();
        if (msg.sender != expected || expected == address(0)) revert UnexpectedV3Callback();
        address tokenIn = abi.decode(data, (address));
        uint256 owed = uint256(amount0Delta > 0 ? amount0Delta : amount1Delta);
        IERC20(tokenIn).safeTransfer(msg.sender, owed);
    }

    /// @notice Self-only wrapper so the refund path can try/catch a route
    /// failure and fall back to returning the quote asset.
    function routeQuoteToEthSelf(address quote, uint256 amount) external returns (uint256) {
        if (msg.sender != address(this)) revert OnlySelf();
        return _routeQuoteToEth(quote, amount);
    }

    // -------------------------------------------------------------------
    // Route execution (internal)
    // -------------------------------------------------------------------

    function _routeEthToQuote(address quote, uint256 amountIn) private returns (uint256 amountOut) {
        Hop[] memory route = _routes[quote];
        if (route.length == 0) revert RouteNotConfigured(quote);
        address current = address(0);
        amountOut = amountIn;
        for (uint256 i = 0; i < route.length; i++) {
            (current, amountOut) = _executeHop(route[i], current, amountOut);
        }
    }

    function _routeQuoteToEth(address quote, uint256 amountIn) private returns (uint256 amountOut) {
        Hop[] memory route = _routes[quote];
        if (route.length == 0) revert RouteNotConfigured(quote);
        address current = quote;
        amountOut = amountIn;
        for (uint256 i = route.length; i > 0; i--) {
            (current, amountOut) = _executeHop(route[i - 1], current, amountOut);
        }
        if (current == address(weth)) {
            weth.withdraw(amountOut);
            current = address(0);
        }
        // setRoute guarantees the reversed walk lands on native ETH or WETH.
        assert(current == address(0));
    }

    /// @dev Swaps `amountIn` of `tokenIn` through `hop`, returning the other
    /// pool token and the amount received. Wraps ETH when a V3 hop needs WETH;
    /// V4 hops spend native ETH directly.
    function _executeHop(Hop memory hop, address tokenIn, uint256 amountIn)
        private
        returns (address tokenOut, uint256 amountOut)
    {
        if (tokenIn == address(0) && hop.venue != Venue.UniV4) {
            weth.deposit{value: amountIn}();
            tokenIn = address(weth);
        }
        if (hop.venue == Venue.UniV4) {
            bool zeroForOne = Currency.unwrap(hop.key.currency0) == tokenIn;
            tokenOut = Currency.unwrap(zeroForOne ? hop.key.currency1 : hop.key.currency0);
            (, amountOut) = _v4Swap(
                V4SwapData({
                    key: hop.key,
                    zeroForOne: zeroForOne,
                    amountIn: amountIn,
                    sqrtPriceLimitX96: 0,
                    takeTo: address(this),
                    payer: address(this),
                    hookData: ""
                })
            );
        } else {
            (tokenOut, amountOut) = _v3Swap(hop.pool, tokenIn, amountIn);
        }
    }

    /// @dev Swaps through the coin's own graduated pool. `quoteIn` true means
    /// quote -> coin (buy), false means coin -> quote (sell). The pool key is
    /// reconstructed from the quote config so a non-default tick spacing works.
    function _swapCoinPool(address token, address quote, uint256 amountIn, address takeTo, bool quoteIn)
        private
        returns (uint256 amountInUsed, uint256 amountOut)
    {
        (Currency c0, Currency c1) = quote < token
            ? (Currency.wrap(quote), Currency.wrap(token))
            : (Currency.wrap(token), Currency.wrap(quote));
        PoolKey memory key = PoolKey({
            currency0: c0,
            currency1: c1,
            fee: 0,
            tickSpacing: launchManager.quoteConfigOf(quote).poolTickSpacing,
            hooks: motionHook
        });
        bool zeroForOne = quoteIn ? quote < token : token < quote;
        (amountInUsed, amountOut) = _v4Swap(
            V4SwapData({
                key: key,
                zeroForOne: zeroForOne,
                amountIn: amountIn,
                sqrtPriceLimitX96: 0,
                takeTo: takeTo,
                payer: address(this),
                hookData: ""
            })
        );
    }

    // -------------------------------------------------------------------
    // Venue swaps (internal)
    // -------------------------------------------------------------------

    function _v4Swap(V4SwapData memory data) private returns (uint256 amountInUsed, uint256 amountOut) {
        if (data.amountIn == 0 || data.amountIn > uint256(uint128(type(int128).max))) revert InvalidAmountIn();
        if (data.sqrtPriceLimitX96 == 0) {
            data.sqrtPriceLimitX96 = data.zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1;
        }
        bytes memory callbackData = abi.encode(data);
        _CALLBACK_HASH_SLOT.asBytes32().tstore(keccak256(callbackData));
        (amountInUsed, amountOut) = abi.decode(poolManager.unlock(callbackData), (uint256, uint256));
        _CALLBACK_HASH_SLOT.asBytes32().tstore(bytes32(0));
    }

    function _v3Swap(address pool, address tokenIn, uint256 amountIn)
        private
        returns (address tokenOut, uint256 amountOut)
    {
        IUniswapV3Pool p = IUniswapV3Pool(pool);
        address token0 = p.token0();
        bool zeroForOne = tokenIn == token0;
        tokenOut = zeroForOne ? p.token1() : token0;

        _V3_POOL_SLOT.asAddress().tstore(pool);
        (int256 amount0, int256 amount1) = p.swap(
            address(this),
            zeroForOne,
            int256(amountIn),
            zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1,
            abi.encode(tokenIn)
        );
        _V3_POOL_SLOT.asAddress().tstore(address(0));

        int256 inputDelta = zeroForOne ? amount0 : amount1;
        int256 outputDelta = zeroForOne ? amount1 : amount0;
        if (inputDelta <= 0 || outputDelta >= 0) revert InvalidSwapDelta();
        if (uint256(inputDelta) != amountIn) revert PartialFillNotAllowed(uint256(inputDelta), amountIn);
        amountOut = uint256(-outputDelta);
    }

    // -------------------------------------------------------------------
    // Refunds and helpers (internal)
    // -------------------------------------------------------------------

    /// @dev Returns a graduation refund to the caller as ETH. Native quote is
    /// sent directly; an ERC20 quote is routed back to ETH, but if that routing
    /// reverts (e.g. dust rounding to zero through a hop) the refund is handed
    /// back in the quote asset rather than reverting the whole buy.
    function _returnRefundAsEth(address quote, uint256 refund) private returns (uint256 ethRefunded) {
        if (quote == address(0)) {
            _sendEth(msg.sender, refund);
            return refund;
        }
        try this.routeQuoteToEthSelf(quote, refund) returns (uint256 ethOut) {
            _sendEth(msg.sender, ethOut);
            ethRefunded = ethOut;
        } catch {
            IERC20(quote).safeTransfer(msg.sender, refund);
        }
    }

    function _hopTokens(Hop calldata hop) private view returns (address token0, address token1) {
        if (hop.venue == Venue.UniV4) {
            return (Currency.unwrap(hop.key.currency0), Currency.unwrap(hop.key.currency1));
        }
        return (IUniswapV3Pool(hop.pool).token0(), IUniswapV3Pool(hop.pool).token1());
    }

    function _selfBalance(address token) private view returns (uint256) {
        return token == address(0) ? address(this).balance : IERC20(token).balanceOf(address(this));
    }

    function _sendEth(address to, uint256 amount) private {
        if (amount == 0) return;
        (bool success,) = to.call{value: amount}("");
        if (!success) revert EthTransferFailed();
    }
}
Chain explorer693msChain node73ms