ZxoarToken

0x78fa48fa750cf9b1d916f9c8ba732df6e2542080

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
2,074 bytes
ABI entries
14
6 read · 5 write
License
none

Contract information

Address
0x78fa48fa750cf9b1d916f9c8ba732df6e2542080
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x6772fe77d5…a7d4581c74

Token

Name
ZXOAR Official Token
Symbol
ZXOAR
Decimals
18
Holders
73

Read contract (6)

allowance(address, address)uint256
balanceOf(address)uint256
decimals()uint8
name()string
symbol()string
totalSupply()uint256

Events (2)

ApprovalTransfer

ABI

[
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "n",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "s",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "supply",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "Approval",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "Transfer",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      }
    ],
    "name": "allowance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "approve",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "decimals",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "subtractedValue",
        "type": "uint256"
      }
    ],
    "name": "decreaseAllowance",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "addedValue",
        "type": "uint256"
      }
    ],
    "name": "increaseAllowance",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "name",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "symbol",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "transfer",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "transferFrom",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.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 {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.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";
import {FullMath} from "v4-core/src/libraries/FullMath.sol";
import {FixedPoint96} from "v4-core/src/libraries/FixedPoint96.sol";
import {LPFeeLibrary} from "v4-core/src/libraries/LPFeeLibrary.sol";
import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol";

/// @dev Fixed-supply token; the entire supply is minted to the launcher and
///      immediately deposited into the locked pool position. No owner, no
///      mint, no hooks — nothing a creator can rug with.
contract ZxoarToken is ERC20 {
    constructor(string memory n, string memory s, uint256 supply, address to) ERC20(n, s) {
        _mint(to, supply);
    }
}

/// @title ZxoarLauncher
/// @notice Agent-first fair-launch factory for Uniswap V4.
///
///         launch() in one transaction:
///           1. deploys a fixed-supply token,
///           2. initializes a native-ETH/token V4 pool wired to SnipeGuardHook
///              (dynamic fee: 50% -> 1% over 3 minutes),
///           3. deposits 95% of supply as a single-sided position spanning
///              [TICK_LOWER, TICK_UPPER] — the pool's own math is the bonding
///              curve — and escrows the remaining 5% as the graduation
///              reserve, deployed by permissionless graduate() as locked
///              open-market liquidity the moment the curve fills. There is
///              no migration, ever: 100% of supply is locked pool liquidity.
///
///         The position is owned by this contract and there is deliberately
///         no code path that removes liquidity: it is locked forever. Swap
///         fees accrued to the position are claimable via collectFees(),
///         split 50/50 between the token's creator and the protocol.
contract ZxoarLauncher is IUnlockCallback, ReentrancyGuard {
    using SafeERC20 for IERC20;
    using PoolIdLibrary for PoolKey;
    using StateLibrary for IPoolManager;

    // ---------------------------------------------------------------- config
    uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18;
    int24 public constant TICK_SPACING = 200;
    /// @notice Max bytes for a token's metadataURI. Enforced on-chain so the
    ///         32KB cap the off-chain builder advertises is actually guaranteed,
    ///         and to bound the SSTORE cost / event size of a launch.
    uint256 public constant MAX_METADATA_URI_BYTES = 32_768;
    /// Price band, expressed in currency1-per-currency0 ticks (token per ETH).
    /// TICK_UPPER is the *launch* price (~1.4e-9 ETH per token, ~1.4 Ξ market
    /// cap); TICK_LOWER is the top of the ladder (~2.05e-8 ETH per token,
    /// ~20.5 Ξ market cap). ETH buys push the tick downward through the band.
    int24 public constant TICK_LOWER = 177_000;
    int24 public constant TICK_UPPER = 203_800;
    /// Graduation: 95% of supply forms the curve; 5% is escrowed here and
    /// deployed by permissionless graduate() as locked liquidity spanning
    /// [GRAD_TICK_LOWER, TICK_LOWER) — the same band width again (~14.6x more
    /// price runway, curve top ~20.6 ETH FDV -> graduated top ~300 ETH FDV).
    uint256 public constant CURVE_SUPPLY = 950_000_000e18;
    uint256 public constant GRAD_RESERVE = 50_000_000e18;
    int24 public constant GRAD_TICK_LOWER = 150_200;

    IPoolManager public immutable poolManager;
    IHooks public immutable hook;
    address public immutable protocolFeeRecipient;
    IWETH public immutable weth;
    address public immutable router;

    // -------------------------------------------------------------- registry
    struct Launch {
        address token;
        address creator;
        string metadataURI;
        uint64 createdAt;
        uint128 liquidity;
    }

    mapping(address => Launch) public launches; // token => Launch
    address[] public allTokens;
    mapping(address => bool) public graduated; // token => curve filled & seed deployed

    event TokenLaunched(
        address indexed token,
        address indexed creator,
        PoolId indexed poolId,
        string name,
        string symbol,
        string metadataURI,
        uint128 liquidity
    );
    event FeesCollected(
        address indexed token,
        address indexed creator,
        uint256 ethAmount,
        uint256 tokenAmount,
        bool creatorWethFallback,
        bool protocolWethFallback
    );

    event Graduated(address indexed token, uint128 gradLiquidity);

    error UnknownToken();
    error NotPoolManager();
    error NotRouter();
    error UnexpectedEthOwed();
    error ZeroAddress();
    error MetadataUriTooLong();
    error AlreadyGraduated();
    error CurveNotFilled();

    uint8 private constant ACTION_ADD = 1;
    uint8 private constant ACTION_COLLECT = 2;
    uint8 private constant ACTION_GRADUATE = 3;

    constructor(IPoolManager poolManager_, IHooks hook_, address protocolFeeRecipient_, IWETH weth_, address router_) {
        if (
            address(poolManager_) == address(0) || address(hook_) == address(0)
                || protocolFeeRecipient_ == address(0) || address(weth_) == address(0) || router_ == address(0)
        ) revert ZeroAddress();
        poolManager = poolManager_;
        hook = hook_;
        protocolFeeRecipient = protocolFeeRecipient_;
        weth = weth_;
        router = router_;
    }

    // -------------------------------------------------------------- launching

    /// @notice Launch a new token. Caller becomes the creator.
    function launch(string calldata name, string calldata symbol, string calldata metadataURI)
        external
        returns (address token)
    {
        return _launch(msg.sender, name, symbol, metadataURI);
    }

    /// @notice Launch a new token on behalf of a creator. Router-only.
    /// @dev Used by launchAndBuy to preserve creator attribution when the router
    ///      is msg.sender. Without this, all fee revenue would route to the router.
    function launchFor(address creator, string calldata name, string calldata symbol, string calldata metadataURI)
        external
        returns (address token)
    {
        if (msg.sender != router) revert NotRouter();
        return _launch(creator, name, symbol, metadataURI);
    }

    function _launch(address creator, string calldata name, string calldata symbol, string calldata metadataURI)
        internal
        returns (address token)
    {
        if (bytes(metadataURI).length > MAX_METADATA_URI_BYTES) revert MetadataUriTooLong();
        token = address(new ZxoarToken(name, symbol, TOTAL_SUPPLY, address(this)));
        PoolKey memory key = poolKeyFor(token);

        // Initialize exactly at the top tick of the band: the position is then
        // 100% token (currency1); the first ETH buy moves price into the band.
        uint160 sqrtStart = TickMath.getSqrtPriceAtTick(TICK_UPPER);
        poolManager.initialize(key, sqrtStart);

        // Liquidity such that the position holds the curve supply (95%); the
        // graduation reserve stays escrowed here until graduate().
        // amount1 = L * (sqrtU - sqrtL) / Q96  =>  L = supply * Q96 / (sqrtU - sqrtL)
        uint160 sqrtL = TickMath.getSqrtPriceAtTick(TICK_LOWER);
        uint160 sqrtU = sqrtStart;
        uint128 liquidity =
            uint128(FullMath.mulDiv(CURVE_SUPPLY, FixedPoint96.Q96, uint256(sqrtU - sqrtL)));

        poolManager.unlock(abi.encode(ACTION_ADD, token, liquidity));

        launches[token] = Launch({
            token: token,
            creator: creator,
            metadataURI: metadataURI,
            createdAt: uint64(block.timestamp),
            liquidity: liquidity
        });
        allTokens.push(token);

        emit TokenLaunched(token, creator, key.toId(), name, symbol, metadataURI, liquidity);
    }

    // ----------------------------------------------------------- fee stream

    /// @notice Claim swap fees accrued to the locked position for `token`.
    ///         Anyone can poke this; proceeds always split 50/50 between the
    ///         token's creator and the protocol. Principal cannot be touched.
    function collectFees(address token) external nonReentrant returns (uint256 ethAmount, uint256 tokenAmount) {
        Launch memory l = launches[token];
        if (l.token == address(0)) revert UnknownToken();

        bytes memory result = poolManager.unlock(abi.encode(ACTION_COLLECT, token, uint128(0)));
        (ethAmount, tokenAmount) = abi.decode(result, (uint256, uint256));

        uint256 ethCreator = ethAmount / 2;
        uint256 tokCreator = tokenAmount / 2;
        bool creatorFallback;
        bool protocolFallback;
        if (ethCreator > 0) creatorFallback = _sendEth(l.creator, ethCreator);
        if (ethAmount - ethCreator > 0) protocolFallback = _sendEth(protocolFeeRecipient, ethAmount - ethCreator);
        if (tokCreator > 0) IERC20(token).safeTransfer(l.creator, tokCreator);
        if (tokenAmount - tokCreator > 0) {
            IERC20(token).safeTransfer(protocolFeeRecipient, tokenAmount - tokCreator);
        }

        emit FeesCollected(token, l.creator, ethAmount, tokenAmount, creatorFallback, protocolFallback);
    }

    // ------------------------------------------------------------ graduation

    /// @notice Deploy the graduation reserve as locked open-market liquidity in
    ///         [GRAD_TICK_LOWER, TICK_LOWER). Permissionless — callable by
    ///         anyone once the curve is bought through its floor. One-way and
    ///         once per token; the seed is locked exactly like the curve.
    function graduate(address token) external nonReentrant returns (uint128 gradLiquidity) {
        Launch memory l = launches[token];
        if (l.token == address(0)) revert UnknownToken();
        if (graduated[token]) revert AlreadyGraduated();
        (, int24 tick,,) = poolManager.getSlot0(poolKeyFor(token).toId());
        if (tick > TICK_LOWER) revert CurveNotFilled();
        graduated[token] = true;

        uint160 sqrtGL = TickMath.getSqrtPriceAtTick(GRAD_TICK_LOWER);
        uint160 sqrtGU = TickMath.getSqrtPriceAtTick(TICK_LOWER);
        gradLiquidity = uint128(FullMath.mulDiv(GRAD_RESERVE, FixedPoint96.Q96, uint256(sqrtGU - sqrtGL)));
        poolManager.unlock(abi.encode(ACTION_GRADUATE, token, gradLiquidity));
        emit Graduated(token, gradLiquidity);
    }

    /// @notice Lowest tick routers may let buys reach: the curve floor before
    ///         graduation, the graduated-band floor after. Single source of
    ///         truth so no caller can walk price into empty tick space.
    function buyTickLimit(address token) public view returns (int24) {
        return graduated[token] ? GRAD_TICK_LOWER : TICK_LOWER;
    }

    // --------------------------------------------------------- v4 callback

    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        (uint8 action, address token, uint128 liquidity) = abi.decode(data, (uint8, address, uint128));
        PoolKey memory key = poolKeyFor(token);

        if (action == ACTION_ADD || action == ACTION_GRADUATE) {
            bool grad = action == ACTION_GRADUATE;
            (BalanceDelta delta,) = poolManager.modifyLiquidity(
                key,
                IPoolManager.ModifyLiquidityParams({
                    tickLower: grad ? GRAD_TICK_LOWER : TICK_LOWER,
                    tickUpper: grad ? TICK_LOWER : TICK_UPPER,
                    liquidityDelta: int256(uint256(liquidity)),
                    salt: bytes32(0)
                }),
                ""
            );
            // Both mints owe only token (currency1): launch initializes at the
            // band top, and graduation mints at/above its range top. Any ETH
            // owed means price sits mid-band — revert; graduate() is retryable.
            if (delta.amount0() != 0) revert UnexpectedEthOwed();
            uint256 owed = uint256(uint128(-delta.amount1()));
            poolManager.sync(key.currency1);
            IERC20(token).safeTransfer(address(poolManager), owed);
            poolManager.settle();
            return "";
        }

        // ACTION_COLLECT: modifyLiquidity with zero delta realizes accrued fees.
        (BalanceDelta feeDelta,) = poolManager.modifyLiquidity(
            key,
            IPoolManager.ModifyLiquidityParams({
                tickLower: TICK_LOWER,
                tickUpper: TICK_UPPER,
                liquidityDelta: 0,
                salt: bytes32(0)
            }),
            ""
        );
        uint256 ethAmt = feeDelta.amount0() > 0 ? uint256(uint128(feeDelta.amount0())) : 0;
        uint256 tokAmt = feeDelta.amount1() > 0 ? uint256(uint128(feeDelta.amount1())) : 0;
        if (graduated[token]) {
            (BalanceDelta g,) = poolManager.modifyLiquidity(
                key,
                IPoolManager.ModifyLiquidityParams({
                    tickLower: GRAD_TICK_LOWER,
                    tickUpper: TICK_LOWER,
                    liquidityDelta: 0,
                    salt: bytes32(0)
                }),
                ""
            );
            if (g.amount0() > 0) ethAmt += uint256(uint128(g.amount0()));
            if (g.amount1() > 0) tokAmt += uint256(uint128(g.amount1()));
        }
        if (ethAmt > 0) poolManager.take(key.currency0, address(this), ethAmt);
        if (tokAmt > 0) poolManager.take(key.currency1, address(this), tokAmt);
        return abi.encode(ethAmt, tokAmt);
    }

    // ---------------------------------------------------------------- views

    function poolKeyFor(address token) public view returns (PoolKey memory) {
        return PoolKey({
            currency0: Currency.wrap(address(0)), // native ETH
            currency1: Currency.wrap(token),
            fee: LPFeeLibrary.DYNAMIC_FEE_FLAG,
            tickSpacing: TICK_SPACING,
            hooks: hook
        });
    }

    function tokenCount() external view returns (uint256) {
        return allTokens.length;
    }

    /// @dev Send ETH to `to`. If raw send fails (e.g. recipient is a contract
    ///      without payable receive), wrap to WETH and transfer instead.
    ///      WETH transfer cannot be recipient-reverted, so this never fails
    ///      for reasons outside our control.
    /// @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 explorer2045msChain node77ms