StockFeeConverter
0xe3b703626df7ebca5ac944d38d7ca02c09a93df2
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
4,161 bytes
ABI entries
12
3 read · 2 write
License
none
Contract information
- Address
- 0xe3b703626df7ebca5ac944d38d7ca02c09a93df2
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xd51778f623…DED13626F9
- Creation tx
- 0xf4fa173014…659c704b7d
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (3)
poolManager() → address
usdg() → address
weth() → address
Events (1)
Converted
ABI
[
{
"inputs": [
{
"internalType": "contract IPoolManager",
"name": "poolManager_",
"type": "address"
},
{
"internalType": "address",
"name": "usdg_",
"type": "address"
},
{
"internalType": "address",
"name": "weth_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "BadHop",
"type": "error"
},
{
"inputs": [],
"name": "OnlyPoolManager",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "Slippage",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "stock",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "stockIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wethOut",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "Converted",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "stock",
"type": "address"
},
{
"internalType": "uint256",
"name": "stockAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minWethOut",
"type": "uint256"
},
{
"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": "hop1",
"type": "tuple"
},
{
"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": "hop2",
"type": "tuple"
},
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "convert",
"outputs": [
{
"internalType": "uint256",
"name": "wethOut",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "contract IPoolManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "unlockCallback",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "usdg",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
}
]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 {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "v4-core/interfaces/callback/IUnlockCallback.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {Currency} from "v4-core/types/Currency.sol";
import {BalanceDelta} from "v4-core/types/BalanceDelta.sol";
import {SwapParams} from "v4-core/types/PoolOperation.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
/// @notice Converts a Stock-Mode fee (skimmed in a tokenized-stock ERC-20) into WETH by swapping two hops
/// DIRECTLY against Uniswap V4 pools: stock → USDG → WETH. Robinhood stocks have no direct WETH pool; their
/// native liquidity is against USDG, and USDG has deep WETH pools, so this two-hop route is the on-chain
/// path. Both hop pools are supplied by the caller (the keeper picks the deepest HOOKLESS pool per hop) and
/// validated here for membership + hooklessness. Stateless / holds no funds between calls; unspent stock is
/// refunded and any USDG dust from an imperfect middle leg is forwarded to `to`. Modeled on SqueezBuyback.
contract StockFeeConverter is IUnlockCallback, ReentrancyGuard {
using SafeERC20 for IERC20;
IPoolManager public immutable poolManager;
/// USDG — the middle hop currency (6 decimals). WETH — the output.
address public immutable usdg;
address public immutable weth;
error OnlyPoolManager();
error Slippage();
error BadHop();
/// @param stock the input stock; `stockIn` actually spent; `wethOut` delivered to `to`.
event Converted(address indexed stock, uint256 stockIn, uint256 wethOut, address indexed to);
constructor(IPoolManager poolManager_, address usdg_, address weth_) {
poolManager = poolManager_;
usdg = usdg_;
weth = weth_;
}
/// @notice Swap `stockAmount` of `stock` → WETH via `hop1` (stock/USDG) then `hop2` (USDG/WETH), and
/// send the WETH to `to`. `minWethOut` bounds total slippage. Pulls `stockAmount` from the caller
/// (needs approval); refunds whatever the first hop can't spend. Both hops must be hookless and contain
/// the expected currencies. Returns the WETH delivered.
function convert(
address stock,
uint256 stockAmount,
uint256 minWethOut,
PoolKey calldata hop1,
PoolKey calldata hop2,
address to
) external nonReentrant returns (uint256 wethOut) {
// Validate hops: hop1 == {stock, USDG} hookless; hop2 == {USDG, WETH} hookless. Reverting here
// (rather than trusting the caller) means a mis-supplied pool can't silently route through a
// malicious hook or the wrong pair.
if (address(hop1.hooks) != address(0) || address(hop2.hooks) != address(0)) revert BadHop();
if (!_isPair(hop1, stock, usdg) || !_isPair(hop2, usdg, weth)) revert BadHop();
IERC20(stock).safeTransferFrom(msg.sender, address(this), stockAmount);
(uint256 stockSpent, uint256 out) =
abi.decode(poolManager.unlock(abi.encode(stock, stockAmount, minWethOut, hop1, hop2, to)), (uint256, uint256));
wethOut = out;
uint256 refund = stockAmount - stockSpent;
if (refund != 0) IERC20(stock).safeTransfer(msg.sender, refund);
emit Converted(stock, stockSpent, wethOut, to);
}
function unlockCallback(bytes calldata data) external returns (bytes memory) {
if (msg.sender != address(poolManager)) revert OnlyPoolManager();
(address stock, uint256 stockAmount, uint256 minWethOut, PoolKey memory hop1, PoolKey memory hop2, address to) =
abi.decode(data, (address, uint256, uint256, PoolKey, PoolKey, address));
// Hop 1: exact-in stock → USDG.
(uint256 stockSpent, uint256 usdgOut) = _swapExactIn(hop1, stock, stockAmount);
// Hop 2: exact-in that USDG → WETH.
(uint256 usdgSpent, uint256 wethOut) = _swapExactIn(hop2, usdg, usdgOut);
if (wethOut < minWethOut) revert Slippage();
// Settle what we owe (the stock spent in hop1) and take what we're owed (WETH out; plus any USDG the
// middle leg couldn't consume — sent to `to` rather than stranded).
poolManager.sync(Currency.wrap(stock));
if (stockSpent != 0) IERC20(stock).safeTransfer(address(poolManager), stockSpent);
poolManager.settle();
uint256 usdgDust = usdgOut - usdgSpent; // >= 0 (exact-in can't spend more than supplied)
if (usdgDust != 0) poolManager.take(Currency.wrap(usdg), to, usdgDust);
if (wethOut != 0) poolManager.take(Currency.wrap(weth), to, wethOut);
return abi.encode(stockSpent, wethOut);
}
/// @dev Exact-input swap of `tokenIn` through `key`; returns (input spent, output received). Direction is
/// derived from which currency `tokenIn` is, so callers don't pass zeroForOne.
function _swapExactIn(PoolKey memory key, address tokenIn, uint256 amountIn)
private
returns (uint256 spent, uint256 received)
{
if (amountIn == 0) return (0, 0);
bool zeroForOne = Currency.unwrap(key.currency0) == tokenIn;
BalanceDelta d = poolManager.swap(
key,
SwapParams({
zeroForOne: zeroForOne,
amountSpecified: -int256(amountIn),
sqrtPriceLimitX96: zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1
}),
""
);
// input is the negative leg, output the positive leg.
if (zeroForOne) {
spent = uint256(uint128(-d.amount0()));
received = uint256(uint128(d.amount1()));
} else {
spent = uint256(uint128(-d.amount1()));
received = uint256(uint128(d.amount0()));
}
}
/// @dev True if `key`'s two currencies are exactly {a, b} (order-independent).
function _isPair(PoolKey memory key, address a, address b) private pure returns (bool) {
address c0 = Currency.unwrap(key.currency0);
address c1 = Currency.unwrap(key.currency1);
return (c0 == a && c1 == b) || (c0 == b && c1 == a);
}
}
Chain explorer2911msChain node92ms