EquifoldV4StockSwapRouter

0x55c6e42a4826e311463fda203c99fe949a1f5319

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
9,635 bytes
ABI entries
23
3 read · 7 write
License
none

Contract information

Address
0x55c6e42a4826e311463fda203c99fe949a1f5319
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0xea3b0ecdc0…d5dbddf1e4

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (3)

moduleFactory()address
poolManager()address
stockRegistry()address

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "moduleFactory_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "stockRegistry_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AmountOverflow",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DeadlineExpired",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "IncorrectCaller",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidRoute",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SlippageExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnknownToken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroOutput",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "buyWithEth",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "buyWithStock",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "buyWithUsdg",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "moduleFactory",
    "outputs": [
      {
        "internalType": "contract IEquifoldStockModuleFactory",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "sellToEth",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "sellToStock",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "sellToUsdg",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "stockRegistry",
    "outputs": [
      {
        "internalType": "contract IEquifoldStockQuoteRegistry",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "rawData",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { IPoolManager } from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import { IUnlockCallback } from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import { PoolKey } from "@uniswap/v4-core/src/types/PoolKey.sol";
import { Currency, CurrencyLibrary } from "@uniswap/v4-core/src/types/Currency.sol";
import { BalanceDelta, BalanceDeltaLibrary } from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import { SwapParams } from "@uniswap/v4-core/src/types/PoolOperation.sol";
import { TickMath } from "@uniswap/v4-core/src/libraries/TickMath.sol";

/// @dev The Task-3 launch-module factory surface this router reads to resolve a launched token's pool
///      and quote stock. Both are zero-valued until the token's pool actually exists.
interface IEquifoldStockModuleFactory {
    function poolKeyOf(
        address token
    ) external view returns (PoolKey memory);
    function stockOf(
        address token
    ) external view returns (address);
}

/// @dev The Task-1 stock quote registry surface this router reads to resolve routing keys. `getStock`
///      reverts {StockNotEnabled} for a stock that is not allowlisted; `StockConfig` is declared
///      identically to the registry's so its ABI return decodes here. `usdg` is the shared quote
///      asset, and `ethUsdgKey` the shared native-ETH/USDG routing pool.
interface IEquifoldStockQuoteRegistry {
    struct StockConfig {
        bool enabled;
        address feed;
        PoolKey stockUsdgKey;
    }

    function getStock(
        address stock
    ) external view returns (StockConfig memory);
    function ethUsdgKey() external view returns (PoolKey memory);
    function usdg() external view returns (address);
}

/// @title Equifold Uniswap v4 Stock-Paired Swap Router
/// @author Equifold
/// @notice The atomic multihop entry/exit router for Equifold's "stock-paired v4 launch" vertical.
///         Launch pools are quoted in a Robinhood tokenized stock, so a user buying with ETH or USDG
///         (or exiting to them) must route ETH <-> USDG <-> stock <-> launch-token across up to three
///         v4 pools. This router plans the hops in the entrypoint and executes them in a single
///         {IPoolManager.unlock}: v4 flash accounting nets every intermediate currency, so only the
///         first input is ever settled and only the final output is ever taken — no intermediate ERC-20
///         ever touches this contract's balance.
/// @dev Exact-input only: `amountSpecified` is always the negated input, and each hop's exact output
///      becomes the next hop's exact input, so a trade fully fills against the permanently-locked
///      single-sided launch liquidity (and the deep vanilla routing pools) with no refund path. Unlike
///      the graduated-pool {EquifoldV4SwapRouter}, the ETH leg here is NATIVE — never wrapped — so the
///      ETH input is settled with `settle{value}` and the ETH output is `take`n straight to the
///      recipient.
///
///      Binding launch-window invariants (enforced by the Task-2 anti-snipe hook, checked by review):
///      1. The recipient encoded in the launch-pool hop's `hookData` is structurally the SAME variable
///         this router `take`s the final launch-token output to (`recipient`), so the hook's per-wallet
///         caps can never be keyed on an address that differs from where the tokens land.
///      2. ETH is never refunded or sent to the caller (the launch module has no `receive()`): the ETH
///         input is exactly `msg.value` (reverting on zero) and every ETH output goes to `recipient`.
contract EquifoldV4StockSwapRouter is IUnlockCallback, ReentrancyGuard {
    using SafeERC20 for IERC20;
    using BalanceDeltaLibrary for BalanceDelta;

    error AmountOverflow();
    error DeadlineExpired();
    error IncorrectCaller();
    error InvalidAddress();
    error InvalidRoute();
    error SlippageExceeded();
    error UnknownToken();
    error ZeroAmount();
    error ZeroOutput();

    /// @dev Largest amount whose `int256` reinterpretation is non-negative (2**255 - 1).
    uint256 private constant MAX_EXACT_IN_AMOUNT = (1 << 255) - 1;

    IPoolManager public immutable poolManager;
    IEquifoldStockModuleFactory public immutable moduleFactory;
    IEquifoldStockQuoteRegistry public immutable stockRegistry;

    /// @dev One planned leg of a route: the pool, the swap direction (from the leg's input currency's
    ///      sort position), and the hookData to forward (recipient on the launch hop, empty elsewhere).
    struct Hop {
        PoolKey key;
        bool zeroForOne;
        bytes hookData;
    }

    /// @dev The full route handed to {unlockCallback}: the ordered hops, the exact first input, the
    ///      first-input and final-output currencies (for settle/take), where the output goes, and
    ///      whether the input is native ETH (settled with `settle{value}` rather than sync/transfer).
    struct CallbackData {
        Hop[] hops;
        uint256 amountIn;
        Currency input;
        Currency output;
        address outputRecipient;
        bool nativeIn;
    }

    constructor(
        IPoolManager poolManager_,
        address moduleFactory_,
        address stockRegistry_
    ) {
        if (
            address(poolManager_) == address(0) || moduleFactory_ == address(0)
                || stockRegistry_ == address(0)
        ) revert InvalidAddress();
        poolManager = poolManager_;
        moduleFactory = IEquifoldStockModuleFactory(moduleFactory_);
        stockRegistry = IEquifoldStockQuoteRegistry(stockRegistry_);
    }

    /// @notice Accepts native ETH only from the pool manager (the ETH leg of a native-in/out route).
    receive() external payable {
        if (msg.sender != address(poolManager)) revert IncorrectCaller();
    }

    // --- Buys -----------------------------------------------------------------------------------

    /// @notice Buys `token` with its quote stock, exact-in (single hop: stock -> token). Pulls
    ///         `amountIn` stock from the caller and takes the launch token to `recipient`.
    function buyWithStock(
        address token,
        uint256 amountIn,
        uint256 minOut,
        address recipient,
        uint256 deadline
    ) external nonReentrant returns (uint256 amountOut) {
        _guard(deadline, recipient, amountIn);
        (PoolKey memory launchKey, address stock) = _launch(token);

        IERC20(stock).safeTransferFrom(msg.sender, address(this), amountIn);

        PoolKey[] memory keys = new PoolKey[](1);
        keys[0] = launchKey;
        amountOut = _run(keys, keys.length - 1, Currency.wrap(stock), amountIn, recipient, false);
        if (amountOut < minOut) revert SlippageExceeded();
    }

    /// @notice Buys `token` with USDG, exact-in (two hops: USDG -> stock -> token). Pulls `amountIn`
    ///         USDG from the caller and takes the launch token to `recipient`.
    function buyWithUsdg(
        address token,
        uint256 amountIn,
        uint256 minOut,
        address recipient,
        uint256 deadline
    ) external nonReentrant returns (uint256 amountOut) {
        _guard(deadline, recipient, amountIn);
        (PoolKey memory launchKey, address stock) = _launch(token);
        address usdg = stockRegistry.usdg();

        IERC20(usdg).safeTransferFrom(msg.sender, address(this), amountIn);

        PoolKey[] memory keys = new PoolKey[](2);
        keys[0] = _stockUsdgKey(stock);
        keys[1] = launchKey;
        amountOut = _run(keys, keys.length - 1, Currency.wrap(usdg), amountIn, recipient, false);
        if (amountOut < minOut) revert SlippageExceeded();
    }

    /// @notice Buys `token` with native ETH, exact-in (three hops: ETH -> USDG -> stock -> token).
    ///         `msg.value` is the exact input (reverts on zero); takes the launch token to `recipient`.
    function buyWithEth(
        address token,
        uint256 minOut,
        address recipient,
        uint256 deadline
    ) external payable nonReentrant returns (uint256 amountOut) {
        _guard(deadline, recipient, msg.value);
        (PoolKey memory launchKey, address stock) = _launch(token);

        PoolKey[] memory keys = new PoolKey[](3);
        keys[0] = stockRegistry.ethUsdgKey();
        keys[1] = _stockUsdgKey(stock);
        keys[2] = launchKey;
        amountOut =
            _run(keys, keys.length - 1, CurrencyLibrary.ADDRESS_ZERO, msg.value, recipient, true);
        if (amountOut < minOut) revert SlippageExceeded();
    }

    // --- Sells ----------------------------------------------------------------------------------

    /// @notice Sells `token` for its quote stock, exact-in (single hop: token -> stock). Pulls
    ///         `amountIn` token from the caller and takes the stock to `recipient`.
    function sellToStock(
        address token,
        uint256 amountIn,
        uint256 minOut,
        address recipient,
        uint256 deadline
    ) external nonReentrant returns (uint256 amountOut) {
        _guard(deadline, recipient, amountIn);
        (PoolKey memory launchKey,) = _launch(token);

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

        PoolKey[] memory keys = new PoolKey[](1);
        keys[0] = launchKey;
        amountOut = _run(keys, 0, Currency.wrap(token), amountIn, recipient, false);
        if (amountOut < minOut) revert SlippageExceeded();
    }

    /// @notice Sells `token` for USDG, exact-in (two hops: token -> stock -> USDG). Pulls `amountIn`
    ///         token from the caller and takes the USDG to `recipient`.
    function sellToUsdg(
        address token,
        uint256 amountIn,
        uint256 minOut,
        address recipient,
        uint256 deadline
    ) external nonReentrant returns (uint256 amountOut) {
        _guard(deadline, recipient, amountIn);
        (PoolKey memory launchKey, address stock) = _launch(token);

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

        PoolKey[] memory keys = new PoolKey[](2);
        keys[0] = launchKey;
        keys[1] = _stockUsdgKey(stock);
        amountOut = _run(keys, 0, Currency.wrap(token), amountIn, recipient, false);
        if (amountOut < minOut) revert SlippageExceeded();
    }

    /// @notice Sells `token` for native ETH, exact-in (three hops: token -> stock -> USDG -> ETH).
    ///         Pulls `amountIn` token from the caller and takes native ETH straight to `recipient`.
    function sellToEth(
        address token,
        uint256 amountIn,
        uint256 minOut,
        address recipient,
        uint256 deadline
    ) external nonReentrant returns (uint256 amountOut) {
        _guard(deadline, recipient, amountIn);
        (PoolKey memory launchKey, address stock) = _launch(token);

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

        PoolKey[] memory keys = new PoolKey[](3);
        keys[0] = launchKey;
        keys[1] = _stockUsdgKey(stock);
        keys[2] = stockRegistry.ethUsdgKey();
        amountOut = _run(keys, 0, Currency.wrap(token), amountIn, recipient, false);
        if (amountOut < minOut) revert SlippageExceeded();
    }

    // --- v4 unlock callback ---------------------------------------------------------------------

    /// @notice v4 unlock callback: swaps every planned hop exact-in (each hop's output funds the next
    ///         hop's input via flash accounting), then settles the single first input and takes the
    ///         single final output to the recipient.
    function unlockCallback(
        bytes calldata rawData
    ) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert IncorrectCaller();
        CallbackData memory d = abi.decode(rawData, (CallbackData));

        uint256 amount = d.amountIn;
        for (uint256 i = 0; i < d.hops.length; ++i) {
            Hop memory hop = d.hops[i];
            BalanceDelta delta = poolManager.swap(
                hop.key,
                SwapParams({
                    zeroForOne: hop.zeroForOne,
                    amountSpecified: _negatedAmountSpecified(amount),
                    sqrtPriceLimitX96: hop.zeroForOne
                        ? TickMath.MIN_SQRT_PRICE + 1
                        : TickMath.MAX_SQRT_PRICE - 1
                }),
                hop.hookData
            );
            // The exact-in output is the swap's positive delta on the hop's output side.
            int128 outDelta = hop.zeroForOne ? delta.amount1() : delta.amount0();
            if (outDelta <= 0) revert ZeroOutput();
            // forge-lint: disable-next-line(unsafe-typecast)
            amount = uint256(uint128(outDelta));
        }

        _settleInput(d.input, d.amountIn, d.nativeIn);
        // The final take recipient IS the launch-hop hookData recipient (single `outputRecipient`
        // variable), so the hook's window caps and the token destination can never diverge.
        poolManager.take(d.output, d.outputRecipient, amount);
        return abi.encode(amount);
    }

    // --- Internal -------------------------------------------------------------------------------

    /// @dev Shared entrypoint guards: unexpired deadline, non-zero recipient, non-zero exact input.
    function _guard(
        uint256 deadline,
        address recipient,
        uint256 amountIn
    ) private view {
        if (block.timestamp > deadline) revert DeadlineExpired();
        if (recipient == address(0)) revert InvalidAddress();
        if (amountIn == 0) revert ZeroAmount();
    }

    /// @dev Resolves a launched token's pool key and quote stock, reverting if the pool doesn't exist.
    function _launch(
        address token
    ) private view returns (PoolKey memory key, address stock) {
        key = moduleFactory.poolKeyOf(token);
        stock = moduleFactory.stockOf(token);
        if (stock == address(0)) revert UnknownToken();
    }

    /// @dev The stock's allowlisted stock/USDG routing key (reverts {StockNotEnabled} if not enabled).
    function _stockUsdgKey(
        address stock
    ) private view returns (PoolKey memory) {
        return stockRegistry.getStock(stock).stockUsdgKey;
    }

    /// @dev Plans the route and executes it in one unlock. `launchHopIndex` marks the launch-pool hop
    ///      (the only one carrying `abi.encode(recipient)` as hookData). Guards the caller-controlled
    ///      first-input cast up front; every later hop's input is a `uint128` delta and always fits.
    function _run(
        PoolKey[] memory keys,
        uint256 launchHopIndex,
        Currency inputCurrency,
        uint256 amountIn,
        address recipient,
        bool nativeIn
    ) private returns (uint256 amountOut) {
        if (amountIn > MAX_EXACT_IN_AMOUNT) revert AmountOverflow();
        (Hop[] memory hops, Currency outputCurrency) =
            _plan(keys, launchHopIndex, inputCurrency, recipient);
        amountOut = abi.decode(
            poolManager.unlock(
                abi.encode(
                    CallbackData({
                        hops: hops,
                        amountIn: amountIn,
                        input: inputCurrency,
                        output: outputCurrency,
                        outputRecipient: recipient,
                        nativeIn: nativeIn
                    })
                )
            ),
            (uint256)
        );
    }

    /// @dev Threads the input currency through the ordered keys: each hop's direction is its input
    ///      currency's sort position, and its output currency is the next hop's input. The launch hop
    ///      carries `abi.encode(recipient)`; routing hops carry empty hookData. Reverts if a key does
    ///      not contain the threaded input currency (a misconfigured route).
    function _plan(
        PoolKey[] memory keys,
        uint256 launchHopIndex,
        Currency inputCurrency,
        address recipient
    ) private pure returns (Hop[] memory hops, Currency outputCurrency) {
        hops = new Hop[](keys.length);
        Currency cur = inputCurrency;
        for (uint256 i = 0; i < keys.length; ++i) {
            bool zeroForOne;
            Currency out;
            if (Currency.unwrap(keys[i].currency0) == Currency.unwrap(cur)) {
                zeroForOne = true;
                out = keys[i].currency1;
            } else if (Currency.unwrap(keys[i].currency1) == Currency.unwrap(cur)) {
                zeroForOne = false;
                out = keys[i].currency0;
            } else {
                revert InvalidRoute();
            }
            hops[i] = Hop({
                key: keys[i],
                zeroForOne: zeroForOne,
                hookData: i == launchHopIndex ? abi.encode(recipient) : bytes("")
            });
            cur = out;
        }
        outputCurrency = cur;
    }

    /// @dev Pays the pool manager the single first input. Native ETH is settled with `settle{value}`
    ///      (no sync/transfer); an ERC-20 is synced, transferred, then settled.
    function _settleInput(
        Currency currency,
        uint256 amount,
        bool nativeIn
    ) private {
        if (nativeIn) {
            poolManager.settle{ value: amount }();
        } else {
            poolManager.sync(currency);
            IERC20(Currency.unwrap(currency)).safeTransfer(address(poolManager), amount);
            poolManager.settle();
        }
    }

    /// @dev Exact-in swaps always pass a negative `amountSpecified`. Guards the uint->int
    ///      reinterpretation, since the first hop's amount is caller-controlled.
    function _negatedAmountSpecified(
        uint256 amountIn
    ) private pure returns (int256) {
        if (amountIn > MAX_EXACT_IN_AMOUNT) revert AmountOverflow();
        // forge-lint: disable-next-line(unsafe-typecast)
        return -int256(amountIn);
    }
}
Chain explorer3365msChain node78ms