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
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