StonkStockRouterV2
0xa88f54239be5124affea441eddf423bb138b00c4
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
4,537 bytes
ABI entries
26
3 read · 8 write
License
mit
Contract information
- Address
- 0xa88f54239be5124affea441eddf423bb138b00c4
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xb668382cF4…A809787CDa
- Creation tx
- 0x019ad3b8c4…73bf5a524d
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (3)
owner() → address
routes(address) → address, address, address, uint8, bool, bool
weth() → address
Events (4)
OwnershipTransferredRouteClearedRouteSetSwappedToStock
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "weth_",
"type": "address"
},
{
"internalType": "address",
"name": "owner_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "BadCallback",
"type": "error"
},
{
"inputs": [],
"name": "Expired",
"type": "error"
},
{
"inputs": [],
"name": "InvalidConfig",
"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": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [],
"name": "RouteNotSet",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "SlippageExceeded",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"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": "stock",
"type": "address"
}
],
"name": "RouteCleared",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "stock",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "midPool",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "midToken",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "stockPool",
"type": "address"
},
{
"indexed": false,
"internalType": "enum StonkStockRouterV2.PoolKind",
"name": "stockPoolKind",
"type": "uint8"
},
{
"indexed": false,
"internalType": "bool",
"name": "midZeroForOne",
"type": "bool"
},
{
"indexed": false,
"internalType": "bool",
"name": "stockZeroForOne",
"type": "bool"
}
],
"name": "RouteSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "stock",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "midOut",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "stockOut",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "SwappedToStock",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "stock",
"type": "address"
}
],
"name": "clearRoute",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "rescueEth",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "rescueToken",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "routes",
"outputs": [
{
"internalType": "address",
"name": "midPool",
"type": "address"
},
{
"internalType": "address",
"name": "midToken",
"type": "address"
},
{
"internalType": "address",
"name": "stockPool",
"type": "address"
},
{
"internalType": "enum StonkStockRouterV2.PoolKind",
"name": "stockPoolKind",
"type": "uint8"
},
{
"internalType": "bool",
"name": "midZeroForOne",
"type": "bool"
},
{
"internalType": "bool",
"name": "stockZeroForOne",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "stock",
"type": "address"
},
{
"internalType": "address",
"name": "midPool",
"type": "address"
},
{
"internalType": "address",
"name": "midToken",
"type": "address"
},
{
"internalType": "address",
"name": "stockPool",
"type": "address"
},
{
"internalType": "enum StonkStockRouterV2.PoolKind",
"name": "kind",
"type": "uint8"
}
],
"name": "setRoute",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenOut",
"type": "address"
},
{
"internalType": "uint256",
"name": "minOut",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapExactETHForTokens",
"outputs": [
{
"internalType": "uint256",
"name": "amountOut",
"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": "",
"type": "bytes"
}
],
"name": "uniswapV3SwapCallback",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "contract IWETHMinimal",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
import {IStonkSwapRouter} from "../interfaces/IStonkSwapRouter.sol";
import {ZeroAddress, InvalidConfig, SlippageExceeded} from "../libs/Errors.sol";
interface IWETHMinimal {
function deposit() external payable;
function transfer(address to, uint256 amount) external returns (bool);
function withdraw(uint256 amount) external;
}
/// @dev Swap surface of a canonical Uniswap v3 pool (WETH/USDG mid pool AND the newer
/// v3-style stock pools like USDG/SLV on Robinhood Chain).
interface IUniswapV3PoolMinimal {
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);
}
/// @dev The original Rialto stock pools (USDG <-> tokenized stock) expose swapExactIn —
/// decoded from RialtoSwap's live buys (selector 0x2203d44a). The pool pulls tokenIn via
/// transferFrom and pushes tokenOut to `to`.
interface IRialtoStockPool {
function token0() external view returns (address);
function token1() external view returns (address);
function swapExactIn(bool zeroForOne, uint256 amountIn, uint256 minAmountOut, address to, uint256 deadline)
external
returns (uint256 amountOut);
}
/// @title StonkStockRouterV2
/// @notice Allowlisted swap adapter for the StonkBrokers boosters on Robinhood Chain, superseding
/// StonkStockRouter. Same fixed IStonkSwapRouter surface and the same two-hop path
///
/// ETH -> WETH (wrap) -> mid token (Uniswap v3 pool) -> stock (stock pool)
///
/// but the SECOND hop now supports BOTH stock-pool kinds live on Robinhood Chain:
///
/// PoolKind.Rialto — the original swapExactIn pools (AAPL / AMZN / NVDA, …)
/// PoolKind.V3 — canonical Uniswap-v3-style pools (SLV, and newer listings)
///
/// so new stocks route through whichever venue actually holds their liquidity, with one
/// owner setRoute() call and no booster changes. Routes stay fully owner-adjustable.
contract StonkStockRouterV2 is IStonkSwapRouter, Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
/// @dev Uniswap v3 sqrt price bounds (TickMath.MIN_SQRT_RATIO / MAX_SQRT_RATIO).
uint160 private constant MIN_SQRT_RATIO_PLUS_ONE = 4295128740;
uint160 private constant MAX_SQRT_RATIO_MINUS_ONE = 1461446703485210103287273052203988822378723970341;
enum PoolKind {
Rialto,
V3
}
struct Route {
address midPool; // Uniswap v3 pool holding WETH + midToken
address midToken; // intermediate token (USDG today)
address stockPool; // pool holding midToken + the stock
PoolKind stockPoolKind; // which swap interface the stock pool speaks
bool midZeroForOne; // hop-1 direction: WETH is midPool.token0
bool stockZeroForOne; // hop-2 direction: midToken is stockPool.token0
}
IWETHMinimal public immutable weth;
/// @notice stock token => its two-hop route. Fully owner-adjustable at any time.
mapping(address => Route) public routes;
/// @dev The only v3 pool allowed to hit the swap callback, and the token this router owes
/// it — set transiently around each v3 hop (WETH for hop 1, midToken for a V3 hop 2).
address private _expectedPool;
address private _payToken;
error RouteNotSet();
error Expired();
error BadCallback();
event RouteSet(
address indexed stock,
address midPool,
address midToken,
address stockPool,
PoolKind stockPoolKind,
bool midZeroForOne,
bool stockZeroForOne
);
event RouteCleared(address indexed stock);
event SwappedToStock(address indexed stock, uint256 ethIn, uint256 midOut, uint256 stockOut, address indexed to);
constructor(address weth_, address owner_) Ownable(owner_) {
if (weth_ == address(0)) revert ZeroAddress();
weth = IWETHMinimal(weth_);
}
// ------------------------------------------------------------------
// Owner config — the adjustable path
// ------------------------------------------------------------------
/// @notice Points `stock` at a two-hop route ending in a `kind` pool. Both hops are
/// validated against the pools' actual token pairs (both pool kinds expose token0/token1)
/// so a fat-fingered address can never silently mis-route funds; swap directions are
/// derived here once instead of on every swap.
function setRoute(address stock, address midPool, address midToken, address stockPool, PoolKind kind)
external
onlyOwner
{
if (stock == address(0) || midPool == address(0) || midToken == address(0) || stockPool == address(0)) {
revert ZeroAddress();
}
address m0 = IUniswapV3PoolMinimal(midPool).token0();
address m1 = IUniswapV3PoolMinimal(midPool).token1();
bool midZeroForOne;
if (m0 == address(weth) && m1 == midToken) midZeroForOne = true;
else if (m1 == address(weth) && m0 == midToken) midZeroForOne = false;
else revert InvalidConfig();
address s0 = IUniswapV3PoolMinimal(stockPool).token0();
address s1 = IUniswapV3PoolMinimal(stockPool).token1();
bool stockZeroForOne;
if (s0 == midToken && s1 == stock) stockZeroForOne = true;
else if (s1 == midToken && s0 == stock) stockZeroForOne = false;
else revert InvalidConfig();
routes[stock] = Route({
midPool: midPool,
midToken: midToken,
stockPool: stockPool,
stockPoolKind: kind,
midZeroForOne: midZeroForOne,
stockZeroForOne: stockZeroForOne
});
emit RouteSet(stock, midPool, midToken, stockPool, kind, midZeroForOne, stockZeroForOne);
}
function clearRoute(address stock) external onlyOwner {
delete routes[stock];
emit RouteCleared(stock);
}
/// @notice Owner rescue for any dust stranded here (partial fills, direct sends). The
/// router holds nothing between swaps by design.
function rescueToken(address token, address to, uint256 amount) external onlyOwner {
if (to == address(0)) revert ZeroAddress();
IERC20(token).safeTransfer(to, amount);
}
function rescueEth(address to, uint256 amount) external onlyOwner {
if (to == address(0)) revert ZeroAddress();
(bool ok,) = payable(to).call{value: amount}("");
require(ok, "eth send failed");
}
// ------------------------------------------------------------------
// IStonkSwapRouter
// ------------------------------------------------------------------
/// @inheritdoc IStonkSwapRouter
function swapExactETHForTokens(address tokenOut, uint256 minOut, address to, uint256 deadline)
external
payable
nonReentrant
returns (uint256 amountOut)
{
if (to == address(0)) revert ZeroAddress();
if (block.timestamp > deadline) revert Expired();
Route memory route = routes[tokenOut];
if (route.stockPool == address(0)) revert RouteNotSet();
// Hop 0: wrap.
weth.deposit{value: msg.value}();
// Hop 1: WETH -> midToken directly against the v3 pool; we pay WETH in the callback.
uint256 midOut = _v3Swap(route.midPool, address(weth), route.midZeroForOne, msg.value);
// Hop 2: midToken -> stock via whichever pool kind holds the liquidity. Output is
// delivered to this router and measured by balance delta (stock tokens on Robinhood
// use scaled-UI proxies, so trust balances over return values), then forwarded —
// guaranteeing "at least minOut to `to`".
uint256 balBefore = IERC20(tokenOut).balanceOf(address(this));
if (route.stockPoolKind == PoolKind.Rialto) {
IERC20(route.midToken).forceApprove(route.stockPool, midOut);
IRialtoStockPool(route.stockPool).swapExactIn(route.stockZeroForOne, midOut, minOut, address(this), deadline);
} else if (route.stockPoolKind == PoolKind.V3) {
_v3Swap(route.stockPool, route.midToken, route.stockZeroForOne, midOut);
} else {
revert InvalidConfig(); // unreachable; exhaustive over PoolKind
}
amountOut = IERC20(tokenOut).balanceOf(address(this)) - balBefore;
if (amountOut < minOut) revert SlippageExceeded();
IERC20(tokenOut).safeTransfer(to, amountOut);
emit SwappedToStock(tokenOut, msg.value, midOut, amountOut, to);
}
/// @dev Exact-in swap against a canonical v3 pool, paying `payToken` in the callback.
/// Returns the output amount reported by the pool's deltas.
function _v3Swap(address pool, address payToken, bool zeroForOne, uint256 amountIn)
internal
returns (uint256 amountOut)
{
_expectedPool = pool;
_payToken = payToken;
(int256 amount0, int256 amount1) = IUniswapV3PoolMinimal(pool).swap(
address(this),
zeroForOne,
int256(amountIn),
zeroForOne ? MIN_SQRT_RATIO_PLUS_ONE : MAX_SQRT_RATIO_MINUS_ONE,
""
);
_expectedPool = address(0);
_payToken = address(0);
amountOut = uint256(-(zeroForOne ? amount1 : amount0));
}
// ------------------------------------------------------------------
// Uniswap v3 callback (hop 1 + V3 hop 2 settlement)
// ------------------------------------------------------------------
function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external {
if (msg.sender != _expectedPool || _expectedPool == address(0)) revert BadCallback();
uint256 owed = uint256(amount0Delta > 0 ? amount0Delta : amount1Delta);
IERC20(_payToken).safeTransfer(msg.sender, owed);
}
}
Chain explorer1769msChain node77ms