LaunchFactoryV4

0x23ab86d6f6c9f721c57deb3fc00ca225bfffc9eb

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
13,244 bytes
ABI entries
29
16 read · 3 write
License
none

Contract information

Address
0x23ab86d6f6c9f721c57deb3fc00ca225bfffc9eb
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xf97cdcce22…e15665f85c

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (16)

MAX_DEV_BUY_BPS()uint256
hook()address
isLaunchedToken(address)bool
launchCount()uint256
launches(uint256)address, uint256
lpLocker()address
nameUsed(bytes32)bool
owner()address
permit2()address
platformBps()uint16
platformWallet()address
poolManager()address
positionManager()address
relaunchWhitelist(address)bool
symbolUsed(bytes32)bool
tokenInitCodeHash(string, string, uint256, string)bytes32

Events (2)

LaunchedRelaunchWhitelistUpdated

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_poolManager",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_positionManager",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_permit2",
        "type": "address"
      },
      {
        "internalType": "address payable",
        "name": "_hook",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_lpLocker",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_platformWallet",
        "type": "address"
      },
      {
        "internalType": "uint16",
        "name": "_platformBps",
        "type": "uint16"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "BadFee",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BadStockToken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DevBuyTooLarge",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NameTaken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SymbolTaken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "positionTokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "devBuyTokens",
        "type": "uint256"
      }
    ],
    "name": "Launched",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "wallet",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "allowed",
        "type": "bool"
      }
    ],
    "name": "RelaunchWhitelistUpdated",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "MAX_DEV_BUY_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hook",
    "outputs": [
      {
        "internalType": "contract VladdyHook",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isLaunchedToken",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "internalType": "uint256",
            "name": "totalSupply",
            "type": "uint256"
          },
          {
            "internalType": "string",
            "name": "metadataURI",
            "type": "string"
          },
          {
            "internalType": "int24",
            "name": "tickLower",
            "type": "int24"
          },
          {
            "internalType": "int24",
            "name": "tickUpper",
            "type": "int24"
          },
          {
            "internalType": "uint160",
            "name": "sqrtPriceX96",
            "type": "uint160"
          },
          {
            "internalType": "uint128",
            "name": "liquidity",
            "type": "uint128"
          },
          {
            "internalType": "uint16",
            "name": "feeBps",
            "type": "uint16"
          },
          {
            "internalType": "uint16",
            "name": "buybackBps",
            "type": "uint16"
          },
          {
            "internalType": "address",
            "name": "agent",
            "type": "address"
          },
          {
            "internalType": "bytes32",
            "name": "salt",
            "type": "bytes32"
          },
          {
            "components": [
              {
                "internalType": "address",
                "name": "addr",
                "type": "address"
              },
              {
                "internalType": "uint16",
                "name": "bps",
                "type": "uint16"
              },
              {
                "internalType": "uint8",
                "name": "kind",
                "type": "uint8"
              }
            ],
            "internalType": "struct LaunchFactoryV4.RecipientInput[]",
            "name": "recipients",
            "type": "tuple[]"
          },
          {
            "internalType": "bool",
            "name": "stockRewardsOn",
            "type": "bool"
          },
          {
            "internalType": "address",
            "name": "stockToken",
            "type": "address"
          }
        ],
        "internalType": "struct LaunchFactoryV4.LaunchParams",
        "name": "p",
        "type": "tuple"
      }
    ],
    "name": "launch",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "launches",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "positionTokenId",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lpLocker",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "name": "nameUsed",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "permit2",
    "outputs": [
      {
        "internalType": "contract IPermit2Minimal",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "platformBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "platformWallet",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "positionManager",
    "outputs": [
      {
        "internalType": "contract IPositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "relaunchWhitelist",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_owner",
        "type": "address"
      }
    ],
    "name": "setOwner",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "wallet",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "allowed",
        "type": "bool"
      }
    ],
    "name": "setRelaunchWhitelist",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "name": "symbolUsed",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "totalSupply",
        "type": "uint256"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      }
    ],
    "name": "tokenInitCodeHash",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";

import {LaunchToken} from "../LaunchToken.sol";
import {VladdyHook} from "./VladdyHook.sol";
import {StockRewardsDistributor} from "./StockRewardsDistributor.sol";

interface IPermit2Minimal {
    function approve(address token, address spender, uint160 amount, uint48 expiration) external;
}

/// @title LaunchFactoryV4
/// @notice One-call token launch on Uniswap v4 (Robinhood Chain): deploy the ERC-20, open a native
///         ETH / TOKEN pool wired to the VladdyHook (LP fee 0 — the hook takes the creator-chosen
///         fee of 1% / 2% / 3% / 5% off the ETH side of every swap and distributes it instantly),
///         seed a single-sided position with the whole supply, and lock it in `LpLockerV4` forever.
///
///         Native ETH is always currency0 and the token currency1, so there is no ordering branch:
///         the pool starts at the position's upper tick (100% token) and buys push the tick down.
contract LaunchFactoryV4 {
    /* ───────────────────────── immutables ───────────────────────── */

    IPoolManager public immutable poolManager;
    IPositionManager public immutable positionManager;
    IPermit2Minimal public immutable permit2;
    VladdyHook public immutable hook;
    address public immutable lpLocker;
    address public immutable platformWallet;

    /// @notice Protocol fee applied to every launch, set once at deploy. Callers cannot bypass it.
    uint16 public immutable platformBps;

    address public owner;

    struct RecipientInput {
        address addr;
        uint16 bps;
        uint8 kind; // 0 wallet, 1 twitter, 2 github
    }

    struct LaunchParams {
        string name;
        string symbol;
        uint256 totalSupply;
        string metadataURI;
        int24 tickLower; // position range; pool starts at tickUpper (100% token side)
        int24 tickUpper;
        uint160 sqrtPriceX96; // initial pool price (must equal sqrtPrice at tickUpper)
        uint128 liquidity; // liquidity for the single-sided token position
        uint16 feeBps; // creator's chosen swap fee: 100 / 200 / 300 / 500 (1% / 2% / 3% / 5%)
        uint16 buybackBps; // creator's chosen buyback reserve (platform fee is fixed by the factory)
        address agent;
        bytes32 salt; // CREATE2 salt (any value; must be unique per identical name/symbol/supply/URI)
        RecipientInput[] recipients;
        // ── stock-rewards mode (mutually exclusive with buyback/recipients) ──
        bool stockRewardsOn; // when true: buy a tokenized stock with the fees and pay it to holders
        address stockToken; // the tokenized stock ERC-20 to reward holders with
    }

    struct Launch {
        address token;
        uint256 positionTokenId;
    }

    Launch[] public launches;
    mapping(address => bool) public isLaunchedToken;

    /// @notice Names/tickers that have already launched (keyed by keccak of the exact string).
    mapping(bytes32 => bool) public nameUsed;
    mapping(bytes32 => bool) public symbolUsed;

    /// @notice Wallets the owner allows to relaunch an already-used name/ticker (e.g. official relaunches).
    mapping(address => bool) public relaunchWhitelist;

    /// @notice Max share of supply a dev buy may acquire at launch: 10%.
    uint256 public constant MAX_DEV_BUY_BPS = 1_000;

    address internal constant DEAD = 0x000000000000000000000000000000000000dEaD;

    event Launched(address indexed token, uint256 positionTokenId, uint256 devBuyTokens);
    event RelaunchWhitelistUpdated(address indexed wallet, bool allowed);

    error BadFee();
    error OnlyOwner();
    error NameTaken();
    error SymbolTaken();
    error DevBuyTooLarge();
    error BadStockToken();
    error ZeroAddress();

    constructor(
        address _poolManager,
        address _positionManager,
        address _permit2,
        address payable _hook,
        address _lpLocker,
        address _platformWallet,
        uint16 _platformBps
    ) {
        if (_platformBps > 10_000) revert BadFee();
        // All of these are immutable — a zero from a misconfigured deploy script would produce a
        // permanently broken factory, so fail at deploy time instead.
        if (
            _poolManager == address(0) || _positionManager == address(0) || _permit2 == address(0)
                || _hook == address(0) || _lpLocker == address(0) || _platformWallet == address(0)
        ) revert ZeroAddress();
        poolManager = IPoolManager(_poolManager);
        positionManager = IPositionManager(_positionManager);
        permit2 = IPermit2Minimal(_permit2);
        hook = VladdyHook(_hook);
        lpLocker = _lpLocker;
        platformWallet = _platformWallet;
        platformBps = _platformBps;
        owner = msg.sender;
    }

    /// @notice Launch a token. Any ETH sent along is an atomic "dev buy": swapped for the new token
    ///         (fee-exempt, straight to the caller) inside the launch tx, capped at 10% of supply.
    function launch(LaunchParams calldata p) external payable returns (address token) {
        // 0a. Fee must be one of the offered tiers.
        if (p.feeBps != 100 && p.feeBps != 200 && p.feeBps != 300 && p.feeBps != 500) revert BadFee();

        // 0b. One launch per name/ticker — unless the owner whitelisted the caller for a relaunch.
        bytes32 nameKey = keccak256(bytes(p.name));
        bytes32 symbolKey = keccak256(bytes(p.symbol));
        if (!relaunchWhitelist[msg.sender]) {
            if (nameUsed[nameKey]) revert NameTaken();
            if (symbolUsed[symbolKey]) revert SymbolTaken();
        }
        nameUsed[nameKey] = true;
        symbolUsed[symbolKey] = true;

        // 1. Deploy token via CREATE2, mint full supply to this factory (to seed liquidity).
        LaunchToken t = new LaunchToken{salt: p.salt}(p.name, p.symbol, p.totalSupply, p.metadataURI, address(this));
        token = address(t);

        // 2. Register the fee config with the hook (platform cut fixed by the factory). In stock-rewards
        //    mode we first deploy the token's distributor and wire it into the token, then register the
        //    pool in stock mode (no recipients, no buyback).
        if (p.stockRewardsOn) {
            if (p.stockToken == address(0)) revert BadStockToken();

            address[] memory excluded = new address[](4);
            excluded[0] = address(poolManager); // v4 custodies the pool's token liquidity
            excluded[1] = lpLocker; // holds the LP NFT
            excluded[2] = DEAD; // dust burns
            excluded[3] = address(this); // the factory holds the full supply transiently

            StockRewardsDistributor distributor = new StockRewardsDistributor(token, p.stockToken, excluded);
            t.setDistributor(address(distributor));

            VladdyHook.Recipient[] memory none = new VladdyHook.Recipient[](0);
            hook.registerPool(token, p.feeBps, platformBps, 0, p.agent, none, true, p.stockToken, address(distributor));
        } else {
            VladdyHook.Recipient[] memory recs = new VladdyHook.Recipient[](p.recipients.length);
            for (uint256 i; i < p.recipients.length; ++i) {
                recs[i] = VladdyHook.Recipient({
                    addr: p.recipients[i].addr,
                    bps: p.recipients[i].bps,
                    kind: p.recipients[i].kind
                });
            }
            hook.registerPool(token, p.feeBps, platformBps, p.buybackBps, p.agent, recs, false, address(0), address(0));
        }

        // 3. Initialize the hooked pool at the launch price.
        PoolKey memory key = hook.poolKeyFor(token);
        poolManager.initialize(key, p.sqrtPriceX96);

        // 4. Mint the single-sided position (100% token) directly to the locker.
        uint256 tokenId = _mintLockedPosition(key, p, token);

        // 5. Burn any rounding dust the mint left behind so the factory never holds supply.
        uint256 dust = IERC20(token).balanceOf(address(this));
        if (dust > 0) IERC20(token).transfer(0x000000000000000000000000000000000000dEaD, dust);

        // 6. Optional atomic dev buy through the hook (fee-exempt), capped at 10% of supply.
        uint256 bought;
        if (msg.value > 0) {
            bought = hook.devBuy{value: msg.value}(token, msg.sender);
            if (bought > (p.totalSupply * MAX_DEV_BUY_BPS) / 10_000) revert DevBuyTooLarge();
        }

        // 7. Record + finalize.
        isLaunchedToken[token] = true;
        launches.push(Launch({token: token, positionTokenId: tokenId}));

        emit Launched(token, tokenId, bought);
    }

    /// @dev Mint the position via the v4 PositionManager (pulls ERC-20s through Permit2), owner = locker.
    function _mintLockedPosition(PoolKey memory key, LaunchParams calldata p, address token)
        internal
        returns (uint256 tokenId)
    {
        IERC20(token).approve(address(permit2), type(uint256).max);
        permit2.approve(token, address(positionManager), type(uint160).max, type(uint48).max);

        tokenId = positionManager.nextTokenId();

        bytes memory actions = abi.encodePacked(uint8(Actions.MINT_POSITION), uint8(Actions.SETTLE_PAIR));
        bytes[] memory params = new bytes[](2);
        // MINT_POSITION(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, owner, hookData)
        params[0] = abi.encode(
            key,
            p.tickLower,
            p.tickUpper,
            p.liquidity,
            uint128(0), // amount0Max: native ETH side — single-sided, nothing owed
            uint128(p.totalSupply), // amount1Max: the token side
            lpLocker,
            bytes("")
        );
        // SETTLE_PAIR(currency0, currency1) — pay what we owe from this factory's balances.
        params[1] = abi.encode(key.currency0, key.currency1);

        positionManager.modifyLiquidities(abi.encode(actions, params), block.timestamp + 60);
    }

    function launchCount() external view returns (uint256) {
        return launches.length;
    }

    /// @notice Init-code hash for a prospective token — lets callers predict a launch's CREATE2
    ///         address off-chain with a single RPC call:
    ///         address = keccak256(0xff ++ factory ++ salt ++ initCodeHash)[12:]
    function tokenInitCodeHash(string calldata name, string calldata symbol, uint256 totalSupply, string calldata metadataURI)
        external
        view
        returns (bytes32)
    {
        return keccak256(
            abi.encodePacked(type(LaunchToken).creationCode, abi.encode(name, symbol, totalSupply, metadataURI, address(this)))
        );
    }

    function setOwner(address _owner) external {
        if (msg.sender != owner) revert OnlyOwner();
        owner = _owner;
    }

    /// @notice Allow (or revoke) a wallet to relaunch an already-taken name/ticker.
    function setRelaunchWhitelist(address wallet, bool allowed) external {
        if (msg.sender != owner) revert OnlyOwner();
        relaunchWhitelist[wallet] = allowed;
        emit RelaunchWhitelistUpdated(wallet, allowed);
    }
}
Chain explorer3121msChain node92ms