ZxoarRouter
0x3c44bd01a23ff89f1ee355827422566507d35a0d
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
7,992 bytes
ABI entries
21
4 read · 5 write
License
none
Contract information
- Address
- 0x3c44bd01a23ff89f1ee355827422566507d35a0d
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xa4B8b0f811…010A0602C9
- Creation tx
- 0xae208956db…a77d543360
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (4)
deployer() → address
launcher() → address
poolManager() → address
weth() → address
Events (2)
LaunchedAndBoughtRefunded
ABI
[
{
"inputs": [
{
"internalType": "contract IPoolManager",
"name": "poolManager_",
"type": "address"
},
{
"internalType": "contract IWETH",
"name": "weth_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "EthSendFailed",
"type": "error"
},
{
"inputs": [],
"name": "Expired",
"type": "error"
},
{
"inputs": [],
"name": "LauncherAlreadySet",
"type": "error"
},
{
"inputs": [],
"name": "LauncherNotSet",
"type": "error"
},
{
"inputs": [],
"name": "NotDeployer",
"type": "error"
},
{
"inputs": [],
"name": "NotPoolManager",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "got",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "want",
"type": "uint256"
}
],
"name": "TooLittleOut",
"type": "error"
},
{
"inputs": [],
"name": "ZeroLimit",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethSpent",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "refund",
"type": "uint256"
},
{
"indexed": false,
"internalType": "bool",
"name": "refundWethFallback",
"type": "bool"
}
],
"name": "LaunchedAndBought",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "bool",
"name": "wethFallback",
"type": "bool"
}
],
"name": "Refunded",
"type": "event"
},
{
"inputs": [
{
"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": "key",
"type": "tuple"
},
{
"internalType": "uint256",
"name": "minAmountOut",
"type": "uint256"
},
{
"internalType": "uint160",
"name": "sqrtPriceLimitX96",
"type": "uint160"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "buyExactIn",
"outputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethSpent",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "deployer",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "launchAndBuy",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethSpent",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "launcher",
"outputs": [
{
"internalType": "contract IZxoarLauncher",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "contract IPoolManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"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": "key",
"type": "tuple"
},
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minAmountOut",
"type": "uint256"
},
{
"internalType": "uint160",
"name": "sqrtPriceLimitX96",
"type": "uint160"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "sellExactIn",
"outputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokensSpent",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IZxoarLauncher",
"name": "launcher_",
"type": "address"
}
],
"name": "setLauncher",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "unlockCallback",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "contract IWETH",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
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 {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {IWETH} from "./interfaces/IWETH.sol";
import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {Currency} from "v4-core/src/types/Currency.sol";
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
/// @dev Minimal interface into the launcher (launch, pool key, tick limits).
interface IZxoarLauncher {
function launchFor(address creator, string calldata name, string calldata symbol, string calldata metadataURI)
external returns (address token);
function poolKeyFor(address token) external view returns (PoolKey memory);
function TICK_UPPER() external view returns (int24);
function buyTickLimit(address token) external view returns (int24);
}
/// @title ZxoarRouter
///
/// pools. Exact-input both directions, hard slippage and deadline
/// checks, explicit price limits, and refunds when a swap partially
/// fills at the edge of the liquidity band.
contract ZxoarRouter is IUnlockCallback {
using SafeERC20 for IERC20;
IPoolManager public immutable poolManager;
IWETH public immutable weth;
/// @notice The address that deployed this router; the only address allowed
/// to wire the launcher. Prevents a deploy-time front-run of
/// setLauncher (which is one-shot and would otherwise let anyone
/// point launchAndBuy at an attacker-controlled launcher).
address public immutable deployer;
IZxoarLauncher public launcher; // Set once after deployment (circular dep with launcher)
error Expired();
error NotPoolManager();
error ZeroLimit();
error TooLittleOut(uint256 got, uint256 want);
error EthSendFailed();
error LauncherAlreadySet();
error LauncherNotSet();
error NotDeployer();
/// @notice Emitted when a partial-fill refund is sent to the buyer.
/// @param recipient The address receiving the refund.
/// @param amount The refund amount in wei.
/// @param wethFallback True if ETH send failed and WETH was sent instead.
event Refunded(address indexed recipient, uint256 amount, bool wethFallback);
/// @notice Emitted on atomic launch-and-buy.
event LaunchedAndBought(
address indexed token,
address indexed creator,
uint256 tokensOut,
uint256 ethSpent,
uint256 refund,
bool refundWethFallback
);
struct SwapData {
PoolKey key;
bool zeroForOne;
uint256 amountIn;
uint160 sqrtPriceLimitX96;
address recipient;
}
constructor(IPoolManager poolManager_, IWETH weth_) {
require(address(poolManager_) != address(0), "zero pool manager");
require(address(weth_) != address(0), "zero weth");
poolManager = poolManager_;
weth = weth_;
deployer = msg.sender;
}
/// @notice Set the launcher address. Deployer-only and one-shot (resolves the
/// circular dependency between router and launcher at deploy time).
/// @dev Restricted to `deployer` so it cannot be front-run between router
/// deployment and wiring — an unrestricted one-shot setter would let an
/// attacker bind a malicious launcher and hijack launchAndBuy.
function setLauncher(IZxoarLauncher launcher_) external {
if (msg.sender != deployer) revert NotDeployer();
if (address(launcher) != address(0)) revert LauncherAlreadySet();
require(address(launcher_) != address(0), "zero launcher");
launcher = launcher_;
}
modifier checkDeadline(uint256 deadline) {
// Intentional: a swap deadline is exactly a block.timestamp comparison.
// Validator second-level drift is immaterial to a user-set deadline.
// forge-lint: disable-next-line(block-timestamp)
if (block.timestamp > deadline) revert Expired();
_;
}
/// @notice Buy `key.currency1` with native ETH (exact input = msg.value).
/// @dev Partial fills at the band edge refund unspent ETH to the caller.
function buyExactIn(
PoolKey calldata key,
uint256 minAmountOut,
uint160 sqrtPriceLimitX96,
address recipient,
uint256 deadline
) external payable checkDeadline(deadline) returns (uint256 amountOut, uint256 ethSpent) {
if (sqrtPriceLimitX96 == 0) revert ZeroLimit();
// Clamp: buys may not walk price past the launcher's per-token floor
// (curve floor pre-graduation, graduated-band floor after). No caller-
// supplied limit can teleport price through empty tick space.
if (address(launcher) != address(0)) {
uint160 floorLimit = TickMath.getSqrtPriceAtTick(launcher.buyTickLimit(Currency.unwrap(key.currency1)));
if (sqrtPriceLimitX96 < floorLimit) sqrtPriceLimitX96 = floorLimit;
}
bytes memory result = poolManager.unlock(
abi.encode(SwapData(key, true, msg.value, sqrtPriceLimitX96, recipient))
);
(amountOut, ethSpent) = abi.decode(result, (uint256, uint256));
if (amountOut < minAmountOut) revert TooLittleOut(amountOut, minAmountOut);
uint256 refund = msg.value - ethSpent;
if (refund > 0) {
bool wethFallback = _sendEth(msg.sender, refund);
emit Refunded(msg.sender, refund, wethFallback);
}
}
/// @notice Sell `key.currency1` for native ETH (exact input).
/// @dev Requires prior approval of `amountIn` to this router. Unspent
/// tokens on a partial fill are refunded to the caller.
function sellExactIn(
PoolKey calldata key,
uint256 amountIn,
uint256 minAmountOut,
uint160 sqrtPriceLimitX96,
address recipient,
uint256 deadline
) external checkDeadline(deadline) returns (uint256 amountOut, uint256 tokensSpent) {
if (sqrtPriceLimitX96 == 0) revert ZeroLimit();
// Clamp: sells may not walk price above the launch tick (empty space).
if (address(launcher) != address(0)) {
uint160 ceilLimit = TickMath.getSqrtPriceAtTick(launcher.TICK_UPPER());
if (sqrtPriceLimitX96 > ceilLimit) sqrtPriceLimitX96 = ceilLimit;
}
address token = Currency.unwrap(key.currency1);
IERC20(token).safeTransferFrom(msg.sender, address(this), amountIn);
bytes memory result = poolManager.unlock(
abi.encode(SwapData(key, false, amountIn, sqrtPriceLimitX96, recipient))
);
(amountOut, tokensSpent) = abi.decode(result, (uint256, uint256));
if (amountOut < minAmountOut) revert TooLittleOut(amountOut, minAmountOut);
uint256 refund = amountIn - tokensSpent;
if (refund > 0) IERC20(token).safeTransfer(msg.sender, refund);
}
/// @notice Atomic launch + first buy in ONE transaction.
/// @dev Guaranteed first position: nothing can intervene between launch and buy.
/// The creator's buy pays the guard fee (no creator privilege). Exact tokens
/// out is deterministic and computable before signing.
/// @param name Token name
/// @param symbol Token ticker (2-6 chars)
/// @param metadataURI Optional URI to JSON metadata
/// @param deadline Revert if block.timestamp > deadline
/// @return token The deployed token address
/// @return tokensOut Tokens received by creator
/// @return ethSpent ETH consumed by the swap (msg.value - refund)
function launchAndBuy(
string calldata name,
string calldata symbol,
string calldata metadataURI,
uint256 deadline
) external payable checkDeadline(deadline) returns (address token, uint256 tokensOut, uint256 ethSpent) {
if (address(launcher) == address(0)) revert LauncherNotSet();
// Launch with creator = msg.sender (router is msg.sender to launcher, so we use launchFor)
token = launcher.launchFor(msg.sender, name, symbol, metadataURI);
// Execute immediate first buy at guard max (50% fee)
PoolKey memory key = launcher.poolKeyFor(token);
uint160 sqrtPriceLimitX96 = TickMath.getSqrtPriceAtTick(launcher.buyTickLimit(token));
bytes memory result = poolManager.unlock(
abi.encode(SwapData(key, true, msg.value, sqrtPriceLimitX96, msg.sender))
);
(tokensOut, ethSpent) = abi.decode(result, (uint256, uint256));
// Refund unspent ETH (partial fill at band edge)
uint256 refund = msg.value - ethSpent;
bool wethFallback;
if (refund > 0) {
wethFallback = _sendEth(msg.sender, refund);
emit Refunded(msg.sender, refund, wethFallback);
}
emit LaunchedAndBought(token, msg.sender, tokensOut, ethSpent, refund, wethFallback);
}
function unlockCallback(bytes calldata data) external returns (bytes memory) {
if (msg.sender != address(poolManager)) revert NotPoolManager();
SwapData memory d = abi.decode(data, (SwapData));
BalanceDelta delta = poolManager.swap(
d.key,
IPoolManager.SwapParams({
zeroForOne: d.zeroForOne,
amountSpecified: -int256(d.amountIn),
sqrtPriceLimitX96: d.sqrtPriceLimitX96
}),
""
);
if (d.zeroForOne) {
// ETH in (amount0 negative), token out (amount1 positive).
uint256 ethSpent = uint256(uint128(-delta.amount0()));
uint256 tokenOut = uint256(uint128(delta.amount1()));
poolManager.settle{value: ethSpent}();
if (tokenOut > 0) poolManager.take(d.key.currency1, d.recipient, tokenOut);
return abi.encode(tokenOut, ethSpent);
} else {
uint256 tokensSpent = uint256(uint128(-delta.amount1()));
uint256 ethOut = uint256(uint128(delta.amount0()));
poolManager.sync(d.key.currency1);
IERC20(Currency.unwrap(d.key.currency1)).safeTransfer(address(poolManager), tokensSpent);
poolManager.settle();
if (ethOut > 0) poolManager.take(d.key.currency0, d.recipient, ethOut);
return abi.encode(ethOut, tokensSpent);
}
}
/// @dev Send ETH to `to`. If raw send fails (e.g. recipient is a contract
/// without payable receive), wrap to WETH and transfer instead.
/// @return usedWethFallback True if raw send failed and WETH was used.
function _sendEth(address to, uint256 amount) internal returns (bool usedWethFallback) {
(bool ok,) = to.call{value: amount}("");
if (!ok) {
// Wrap to WETH and transfer — cannot be reverted by recipient
weth.deposit{value: amount}();
require(weth.transfer(to, amount), "weth transfer failed");
return true;
}
return false;
}
receive() external payable {}
}
Chain explorer3868msChain node73ms