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
- Creator
- 0xd03543d97B…F446Cd1418
- 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