FlowPadCurveRouter
0x9ebdf5862d696961c9f4f305b2955b8482bbc661
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
4,074 bytes
ABI entries
21
2 read · 7 write
License
none
Contract information
- Address
- 0x9ebdf5862d696961c9f4f305b2955b8482bbc661
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xbFd1DFfd49…435ce0F502
- Creation tx
- 0xf79e49f3b4…56fd2e5f76
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (2)
hook() → address
poolManager() → address
Events (1)
Trade
ABI
[
{
"inputs": [
{
"internalType": "contract IPoolManager",
"name": "poolManager_",
"type": "address"
},
{
"internalType": "contract IHooks",
"name": "hook_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "DeadlineExpired",
"type": "error"
},
{
"inputs": [],
"name": "EthTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientPayment",
"type": "error"
},
{
"inputs": [],
"name": "NotOwner",
"type": "error"
},
{
"inputs": [],
"name": "NotPoolManager",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "SlippageExceeded",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAmount",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "trader",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "isBuy",
"type": "bool"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
}
],
"name": "Trade",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "minTokensOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "buy",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "minTokensOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "buyFor",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "buyTokens",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "buyTokensFor",
"outputs": [
{
"internalType": "uint256",
"name": "ethSpent",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "hook",
"outputs": [
{
"internalType": "contract IHooks",
"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": "tokensIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minEthOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "sell",
"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": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "ethTo",
"type": "address"
}
],
"name": "sellFor",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "data",
"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.26;
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
import {BalanceDelta} 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";
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
/// @title FlowPadCurveRouter (v4 single-sided model)
/// @notice EXPERIMENTAL, LOCAL-ONLY. Our own v4 swap router — the parity
/// counterpart to the production factory's buy/buyFor/buyTokens/sell/sellFor.
/// Because a FlowPad token is a REAL v4 pool from block one, external
/// aggregators (GMGN, Axiom, Universal Router) can already trade it with their
/// OWN routers; this one exists for our frontend and for buyer attribution,
/// adding slippage (minOut), deadlines, recipient delivery (Weg 1) and native
/// refunds. It never custodies tokens: buys `take` directly to the recipient,
/// sells `transferFrom` the seller straight into the PoolManager.
contract FlowPadCurveRouter is IUnlockCallback {
using SafeERC20 for IERC20;
using CurrencyLibrary for Currency;
error NotPoolManager();
error DeadlineExpired();
error ZeroAddress();
error ZeroAmount();
error SlippageExceeded();
error InsufficientPayment();
error EthTransferFailed();
error NotOwner();
/// @notice One clean trade record per swap through this router — the end
/// user (buy recipient / seller), the direction and both amounts — so the
/// indexer can build price / volume / candles / the trade feed for v4 tokens.
event Trade(address indexed token, address indexed trader, bool isBuy, uint256 ethAmount, uint256 tokenAmount);
IPoolManager public immutable poolManager;
IHooks public immutable hook;
enum Kind {
BUY_EXACT_IN,
BUY_EXACT_OUT,
SELL_EXACT_IN
}
struct Call {
Kind kind;
address token;
uint256 amount; // exact-in: ETH/token in; exact-out: tokens out
uint256 limit; // minOut (buy/sell) or maxEthIn (exact-out)
address payer; // who funds ETH / receives ETH refund
address recipient; // who receives tokens (buy) or ETH (sell)
address from; // token source for sells
}
constructor(IPoolManager poolManager_, IHooks hook_) {
poolManager = poolManager_;
hook = hook_;
}
// ------------------------------------------------------------------- buys
function buy(address token, uint256 minTokensOut, uint256 deadline) external payable returns (uint256) {
return buyFor(token, minTokensOut, deadline, msg.sender);
}
/// @notice Buy delivering tokens directly to `recipient`; msg.sender pays
/// and gets any refund (Weg 1 — max-wallet/creator exemption apply to the
/// recipient).
function buyFor(address token, uint256 minTokensOut, uint256 deadline, address recipient)
public
payable
returns (uint256)
{
if (block.timestamp > deadline) revert DeadlineExpired();
if (recipient == address(0)) revert ZeroAddress();
if (msg.value == 0) revert ZeroAmount();
bytes memory ret = poolManager.unlock(
abi.encode(Call(Kind.BUY_EXACT_IN, token, msg.value, minTokensOut, msg.sender, recipient, address(0)))
);
return abi.decode(ret, (uint256));
}
function buyTokens(address token, uint256 tokensOut, uint256 deadline) external payable returns (uint256) {
return buyTokensFor(token, tokensOut, deadline, msg.sender);
}
/// @notice Exact-OUTPUT buy: receive exactly `tokensOut`, pay up to msg.value
/// (the MAX button); the unused remainder is refunded.
function buyTokensFor(address token, uint256 tokensOut, uint256 deadline, address recipient)
public
payable
returns (uint256 ethSpent)
{
if (block.timestamp > deadline) revert DeadlineExpired();
if (recipient == address(0)) revert ZeroAddress();
if (tokensOut == 0) revert ZeroAmount();
bytes memory ret = poolManager.unlock(
abi.encode(Call(Kind.BUY_EXACT_OUT, token, tokensOut, msg.value, msg.sender, recipient, address(0)))
);
return abi.decode(ret, (uint256));
}
// ------------------------------------------------------------------ sells
function sell(address token, uint256 tokensIn, uint256 minEthOut, uint256 deadline) external returns (uint256) {
return sellFor(token, tokensIn, minEthOut, deadline, msg.sender, msg.sender);
}
/// @notice Sell your own `tokensIn` (pulled from `from == msg.sender`) and
/// direct the ETH to `ethTo`. `from` is required to equal the caller so a
/// standing allowance to this router can never be spent by a third party;
/// delegated sells (an aggregator selling for a user) must use the user's
/// OWN router + the pool directly, or a signed permit.
function sellFor(address token, uint256 tokensIn, uint256 minEthOut, uint256 deadline, address from, address ethTo)
public
returns (uint256)
{
if (block.timestamp > deadline) revert DeadlineExpired();
if (from != msg.sender) revert NotOwner();
if (ethTo == address(0)) revert ZeroAddress();
if (tokensIn == 0) revert ZeroAmount();
bytes memory ret =
poolManager.unlock(abi.encode(Call(Kind.SELL_EXACT_IN, token, tokensIn, minEthOut, from, ethTo, from)));
return abi.decode(ret, (uint256));
}
// --------------------------------------------------------------- unlock
function unlockCallback(bytes calldata data) external returns (bytes memory) {
if (msg.sender != address(poolManager)) revert NotPoolManager();
Call memory c = abi.decode(data, (Call));
PoolKey memory key = _key(c.token);
if (c.kind == Kind.SELL_EXACT_IN) {
BalanceDelta ds = poolManager.swap(
key,
SwapParams({
zeroForOne: false,
amountSpecified: -int256(c.amount),
sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1
}),
""
);
uint256 tokIn = ds.amount1() < 0 ? uint256(uint128(-ds.amount1())) : 0;
uint256 ethOut = ds.amount0() > 0 ? uint256(uint128(ds.amount0())) : 0;
if (ethOut < c.limit) revert SlippageExceeded();
// settle tokens straight from the seller into the PoolManager
poolManager.sync(key.currency1);
IERC20(c.token).safeTransferFrom(c.from, address(poolManager), tokIn);
poolManager.settle();
poolManager.take(key.currency0, c.recipient, ethOut);
emit Trade(c.token, c.from, false, ethOut, tokIn);
return abi.encode(ethOut);
}
bool exactOut = c.kind == Kind.BUY_EXACT_OUT;
BalanceDelta d = poolManager.swap(
key,
SwapParams({
zeroForOne: true,
amountSpecified: exactOut ? int256(c.amount) : -int256(c.amount),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
}),
""
);
uint256 spent = d.amount0() < 0 ? uint256(uint128(-d.amount0())) : 0;
uint256 tokensOut = d.amount1() > 0 ? uint256(uint128(d.amount1())) : 0;
if (exactOut) {
if (spent > c.limit) revert InsufficientPayment(); // c.limit == msg.value
if (tokensOut < c.amount) revert SlippageExceeded(); // AMM couldn't fill the exact output
} else {
if (tokensOut < c.limit) revert SlippageExceeded();
}
if (spent > 0) poolManager.settle{value: spent}();
if (tokensOut > 0) poolManager.take(key.currency1, c.recipient, tokensOut);
emit Trade(c.token, c.recipient, true, spent, tokensOut);
uint256 funded = exactOut ? c.limit : c.amount; // msg.value in both cases
uint256 refund = funded - spent;
if (refund > 0) {
(bool ok,) = c.payer.call{value: refund}("");
if (!ok) revert EthTransferFailed();
}
return abi.encode(exactOut ? spent : tokensOut);
}
function _key(address token) internal view returns (PoolKey memory) {
return PoolKey({
currency0: Currency.wrap(address(0)),
currency1: Currency.wrap(token),
fee: 0,
tickSpacing: 60,
hooks: hook
});
}
receive() external payable {}
}
Chain explorer5393msChain node80ms