AaroLauncher

0xdb0fc3298fbf4a831fccd032741b271562b97af9

Verification
Verified
v0.8.23+commit.f704f362
Type
Contract
7,216 bytes
ABI entries
18
10 read · 2 write
License
none

Contract information

Address
0xdb0fc3298fbf4a831fccd032741b271562b97af9
Chain
Robinhood Chain (4663)
Compiler
v0.8.23+commit.f704f362
Optimization
Enabled
Creation tx
0x8540bf4101…b4304a33e4

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (10)

CREATOR_BPS()uint16
SUPPLY()uint256
WETH()address
creatorOf(address)address
launchCount()uint256
npm()address
onERC721Received(address, address, uint256, bytes)bytes4
poolOf(address)address
positionOf(address)uint256
treasury()address

Events (2)

FeesCollectedTokenLaunched

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_weth",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_npm",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_treasury",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnknownToken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "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": "wethCreator",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "wethTreasury",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenCreator",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenTreasury",
        "type": "uint256"
      }
    ],
    "name": "FeesCollected",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "positionId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "uri",
        "type": "string"
      }
    ],
    "name": "TokenLaunched",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "CREATOR_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "WETH",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "collectFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount0",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amount1",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "creatorOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "uri",
        "type": "string"
      }
    ],
    "name": "launch",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "npm",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "name": "onERC721Received",
    "outputs": [
      {
        "internalType": "bytes4",
        "name": "",
        "type": "bytes4"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "poolOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "positionOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "positionId",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";

import {AaroToken} from "./AaroToken.sol";
import {INonfungiblePositionManager} from "../interfaces/INonfungiblePositionManager.sol";

/**
 * AARO direct-to-Uniswap launcher.
 *
 * launch() deploys a fixed 1B-supply ERC20 and seeds the ENTIRE supply as a
 * single-sided (all-token / zero-WETH) Uniswap V3 position at ~$2k FDV, so the
 * token is tradeable on Uniswap from block one and its price walks up the range
 * as buyers arrive. The LP position NFT is held here forever and its liquidity
 * is NEVER decreased — there is no withdraw/decrease code path, so it cannot be
 * rugged. Only the 1% trading fees are ever collectible, split 80% creator /
 * 20% platform via collectFees().
 *
 * No bonding curve, no graduation: a token is live on Uniswap the moment it is
 * launched.
 *
 * Single-sided seeding works by placing the LP range entirely on the token side
 * of the initial price. In V3 a position is 100% token0 when currentTick <=
 * tickLower and 100% token1 when currentTick >= tickUpper. We initialize the
 * pool exactly at the launch tick and put the range above (token0 = TOKEN) or
 * below (token0 = WETH) it, so the seed is all token and zero WETH. As buyers
 * swap WETH in, price walks into the range and the memecoin's price rises.
 */
contract AaroLauncher is IERC721Receiver {
    using SafeERC20 for IERC20;

    // --- token economics ---
    uint256 public constant SUPPLY = 1_000_000_000e18; // 1B, 100% into the pool
    uint16 public constant CREATOR_BPS = 8000; // 80% of fees to creator, 20% platform
    uint16 internal constant BPS = 10_000;

    // --- Uniswap V3 pool params (1% fee tier => tickSpacing 200) ---
    uint24 internal constant LP_FEE = 10000;
    int24 internal constant MAX_TICK = 887200; // full-range max for spacing 200
    // Launch price ~= $2,000 FDV. |tick| = 205800 (aligned to spacing 200).
    // The token is always currency0 (address < WETH, forced via CREATE2 below),
    // so the pool initializes at tick -205800 == tickLower: an INCLUSIVE, ACTIVE
    // boundary. That makes the single-sided seed simultaneously 100% token AND
    // live liquidity from block one, so Uniswap's router quotes it immediately.
    // sqrtPriceX96 computed via TickMath.getSqrtRatioAtTick, validated on-chain.
    int24 internal constant LAUNCH_TICK = 205800;
    uint160 internal constant SQRT_PRICE_TOKEN0 = 2692853918866877851296459;
    uint256 internal constant MAX_SALT = 1_000_000; // salt-search bound (~22 avg)

    address public immutable WETH;
    address public immutable npm; // Uniswap V3 NonfungiblePositionManager
    address public immutable treasury;

    mapping(address token => address creator) public creatorOf;
    mapping(address token => uint256 positionId) public positionOf;
    mapping(address token => address pool) public poolOf;
    uint256 public launchCount; // per-launch nonce, keeps CREATE2 addresses unique

    error ZeroAddress();
    error UnknownToken();

    event TokenLaunched(
        address indexed token,
        address indexed pool,
        address indexed creator,
        uint256 positionId,
        string name,
        string symbol,
        string uri
    );
    event FeesCollected(
        address indexed token,
        address indexed creator,
        uint256 wethCreator,
        uint256 wethTreasury,
        uint256 tokenCreator,
        uint256 tokenTreasury
    );

    constructor(address _weth, address _npm, address _treasury) {
        if (_weth == address(0) || _npm == address(0) || _treasury == address(0)) revert ZeroAddress();
        WETH = _weth;
        npm = _npm;
        treasury = _treasury;
    }

    /**
     * Launches a new token: deploys the ERC20, creates + initializes its
     * TOKEN/WETH 1% pool at the fixed launch price, and seeds all 1B tokens as a
     * single-sided, permanently-locked LP position held by this contract.
     * msg.sender is recorded as the creator (earns 80% of the pool's fees).
     */
    function launch(string calldata name, string calldata symbol, string calldata uri)
        external
        returns (address token, address pool)
    {
        // 1. Deploy the fixed-supply token to an address BELOW WETH (CREATE2 salt
        //    mining) so it is always Uniswap currency0. That ordering is the only
        //    one where a single-sided all-token position is BOTH 100% token AND
        //    active liquidity at launch — so the pool is routable on Uniswap's UI
        //    immediately, not just after the first trade drags the price in-range.
        token = _deployTokenBelowWeth(name, symbol, uri);

        // 2. Create + initialize the pool at the launch price. token is currency0.
        pool = INonfungiblePositionManager(npm).createAndInitializePoolIfNecessary(token, WETH, LP_FEE, SQRT_PRICE_TOKEN0);

        // 3. Seed ALL supply single-sided as a [tickLower=-LAUNCH_TICK, max]
        //    position. The pool inits exactly at tickLower (inclusive => active),
        //    so the deposit is 100% token / 0 WETH and liquidity is live at once.
        IERC20(token).forceApprove(npm, SUPPLY);
        INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({
            token0: token,
            token1: WETH,
            fee: LP_FEE,
            tickLower: -LAUNCH_TICK,
            tickUpper: MAX_TICK,
            amount0Desired: SUPPLY,
            amount1Desired: 0,
            amount0Min: 0,
            amount1Min: 0,
            recipient: address(this),
            deadline: block.timestamp
        });
        (uint256 positionId,,,) = INonfungiblePositionManager(npm).mint(params);

        creatorOf[token] = msg.sender;
        positionOf[token] = positionId;
        poolOf[token] = pool;

        emit TokenLaunched(token, pool, msg.sender, positionId, name, symbol, uri);
    }

    /// Deploys AaroToken via CREATE2, mining the salt so the resulting address
    /// sorts below WETH (making the token Uniswap currency0). ~4.6% of salts
    /// qualify, so this finds one in ~22 tries on average.
    function _deployTokenBelowWeth(string calldata name, string calldata symbol, string calldata uri)
        internal
        returns (address token)
    {
        bytes memory initCode =
            abi.encodePacked(type(AaroToken).creationCode, abi.encode(name, symbol, uri, SUPPLY, address(this)));
        bytes32 initHash = keccak256(initCode);
        // Seed the salt search with a per-launch nonce so two launches with the
        // SAME name/symbol/uri (identical initHash) still explore disjoint salts
        // and never collide on the same CREATE2 address.
        uint256 nonce = launchCount++;
        bytes32 salt;
        bool found;
        for (uint256 i = 0; i < MAX_SALT; ++i) {
            salt = keccak256(abi.encodePacked(nonce, i));
            address predicted =
                address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, initHash)))));
            if (predicted < WETH) {
                found = true;
                break;
            }
        }
        require(found, "no salt below WETH");
        token = address(new AaroToken{salt: salt}(name, symbol, uri, SUPPLY, address(this)));
        require(token < WETH, "token not currency0"); // CREATE2 determinism guard
    }

    /**
     * Collects accrued 1% LP trading fees (in WETH + the token) from the locked
     * position and splits them 80% creator / 20% platform. Permissionless — the
     * split is fixed and funds only ever reach the creator + treasury. Uses
     * uint128-max so it collects all owed fees and can NEVER touch the locked
     * principal liquidity, preserving the "locked forever" guarantee.
     */
    function collectFees(address token) external returns (uint256 amount0, uint256 amount1) {
        uint256 positionId = positionOf[token];
        if (positionId == 0) revert UnknownToken();
        address creator = creatorOf[token];

        (amount0, amount1) = INonfungiblePositionManager(npm).collect(
            INonfungiblePositionManager.CollectParams({
                tokenId: positionId,
                recipient: address(this),
                amount0Max: type(uint128).max,
                amount1Max: type(uint128).max
            })
        );

        bool tokenIsToken0 = token < WETH;
        uint256 wethAmt = tokenIsToken0 ? amount1 : amount0;
        uint256 tokenAmt = tokenIsToken0 ? amount0 : amount1;

        (uint256 wc, uint256 wt) = _split(WETH, creator, wethAmt);
        (uint256 tc, uint256 tt) = _split(token, creator, tokenAmt);

        emit FeesCollected(token, creator, wc, wt, tc, tt);
    }

    /// Sends `amount` of `asset` 80/20 to creator/treasury via SafeERC20.
    function _split(address asset, address creator, uint256 amount)
        internal
        returns (uint256 toCreator, uint256 toTreasury)
    {
        if (amount == 0) return (0, 0);
        toCreator = (amount * CREATOR_BPS) / BPS;
        toTreasury = amount - toCreator;
        IERC20(asset).safeTransfer(creator, toCreator);
        IERC20(asset).safeTransfer(treasury, toTreasury);
    }

    /// Accept the LP position NFT. Uniswap's V3 NPM uses _mint (not _safeMint),
    /// so this is only defensive, but it lets the launcher hold the position.
    function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
        return this.onERC721Received.selector;
    }
}
Chain explorer932msChain node97ms