# Launchpad: security notes **Status: testnet prototype. Not audited. Do not deploy to mainnet or hold real value in it until an independent audit is done.** ## How it works Every launch mints 1B tokens. 90% goes straight into a Uniswap V4 pool against native HYPE (2% fee, no hooks) as one single-sided position starting at a fixed price, so anyone can trade from the first block; the creator can buy in the launch transaction itself (through Uniswap's Universal Router). The launchpad keeps the position NFT and never removes liquidity: its only position action after minting is a zero-liquidity decrease, which collects fees. The pool's liquidity is locked for good. Anyone can collect a pool's fees (in HYPE and in the token); they are split 30% PaperTax, 40% creator, 30% $TAX stakers. Seven days after launch the owner releases the two 5% drops: the PAPER stakers' 5% in equal shares through its own Uniswap MerkleDistributor, and the $TAX stakers' 5% to Morpho's Universal Rewards Distributor, which pays $TAX stakers in rounds. $TAX itself is launched once by the owner: 48% airdropped in equal shares to PAPER stakers at a snapshot, 40% in its own locked pool, 10% to a vesting wallet for the PaperTax team, and 2% held in the launchpad for early PaperTax traders until it is released as a Merkle drop. ## What is off the shelf and what is custom | Part | Source | Audited | | --- | --- | --- | | Tokens (`LaunchToken`, including $TAX): ERC-20, burn | OpenZeppelin Contracts v5.1 `ERC20`, `ERC20Burnable` | Yes (OpenZeppelin) | | Trading, liquidity, positions, fee accrual | Uniswap V4: PoolManager, PositionManager, Universal Router, Permit2 (official deployment on HyperEVM mainnet; on testnet a copy built from Uniswap's source, see `uniswap-v4/README.md`) | Yes (Uniswap) | | Starting price and liquidity math | Uniswap `TickMath`, `FullMath`, `LiquidityAmounts` (MIT), vendored unchanged except import paths in `src/vendor/uniswap/v4-core` | Yes (Uniswap) | | $TAX airdrop and PAPER-staker drops (equal shares, anyone can push) | Uniswap `MerkleDistributor` (the UNI airdrop contract), vendored unchanged except the pragma | Yes (Uniswap) | | $TAX stakers' rewards (cumulative Merkle roots, timelock, anyone can push) | Morpho `UniversalRewardsDistributor`, vendored unchanged except the pragma and import paths (`src/vendor/morpho`) | Yes (Cantina, OpenZeppelin) | | Pushing payments in batches | Multicall3 (`0xcA11bde05977b3631167028862bE2a173976CA11`) | Widely used, not formally audited | | Dev share vesting ($TAX) | OpenZeppelin `VestingWallet` | Yes | | Ownership, re-entrancy lock, HYPE and token transfers | OpenZeppelin `Ownable2Step`, `ReentrancyGuard`, `Address.sendValue`, `SafeERC20` | Yes | | Opening the pool and position, collecting and splitting fees, releasing drops, launching $TAX | Custom (`PaperLaunchpad`, about 409 lines with comments) | **No** | | $TAX staking: stake, unstake, checkpointed history | Custom (`RefundStaking`, about 80 lines, on OpenZeppelin `Checkpoints`) | **No** | | Creating the $TAX contracts (keeps the launchpad under the size limit) | Custom (`RefundLauncher`, no state) | **No** | | Drop lists and payout rounds (`lib/launchpad/rewards.js`, `payouts.js`) | Custom, off chain; lists are stored before they are used and never rebuilt | **No** | | Merkle trees (`lib/launchpad/merkle.js`) | Port of `@openzeppelin/merkle-tree`, checked against its published vector; keccak256 from noble-hashes (audited by Cure53), vendored | Port: **No** | ## Liquidity can't be withdrawn The launchpad holds every position NFT. After minting, the only position action it ever sends is DECREASE_LIQUIDITY with zero liquidity followed by TAKE_PAIR, which collects fees; it never burns or transfers the NFT, and nobody else is approved for it. Tested in `test_liquidityCannotBeRemoved`. ## Pool set up in advance A token's address is predictable, so someone can initialise its Uniswap pool before the launch. They can't profit: before launch nobody holds the token, so any liquidity they add is HYPE-only. If they set the price at or below the start (the token cheaper), the launch goes ahead and the position still starts at the start price; the first buy crosses the empty gap for free (`test_poolPreCreatedCheaperIsHarmless`, `test_poolPreCreatedAtTheSamePriceIsUsed`). If they set it above the start (the token dearer), the launch is refused (`"pool pre-created"`) because the mint would need HYPE. That is griefing, not theft: a determined attacker who watches pending transactions can keep blocking a creator's launches at their own gas cost (about 60k gas a time); the token address changes every block, so a retry in a later block usually goes through. A blocked launch costs the creator more gas than the attacker (the token is deployed before the check; checking the predicted address first would push the launchpad over the contract size limit). On mainnet, send launches through a private transaction service, or add a Uniswap hook that only lets the launchpad initialise its pools. ## Before mainnet - Fork-test the launchpad against the deployed HyperEVM V4 contracts: the Universal Router's swap parameters (`minHopPriceX36`) and the PositionManager's action formats must match the versions used here. - Move ownership of the launchpad and the rewards distributor to a multisig behind a timelock. - Turn on big blocks for the deployer and owner wallets. Deploying the launchpad (about 8M gas) and Uniswap's contracts don't fit HyperEVM's 3M-gas fast blocks; launching $TAX uses about 2.6M. Launches, fee collection and drop releases fit fast blocks. The page checks `eth_usingBigBlocks` before deploying. - Check on a fork that HyperEVM block timestamps never go backwards between fast and big blocks: staking history (OpenZeppelin checkpoints) refuses an earlier timestamp, so stake and unstake would fail until the clock caught up. ## Trust assumptions - **The owner publishes the PAPER drop lists and the $TAX stakers' payout roots.** A dishonest owner could misdirect those: the PAPER stakers' 5% of a token (bounded per token, it sits in that token's own distributor) and whatever is in the rewards distributor waiting to be claimed. It can't touch pool liquidity, creator fees or PaperTax fees. Payout roots go through the rewards distributor's timelock (set at deploy; use 24 hours or more on mainnet), so a bad root can be seen and revoked before anyone can accept it. Every list is rebuilt by the server from public data under fixed rules and stored before it is used (`lp_paper_drops`, `lp_payouts`), so anyone can check it. - **Rewards distributor owner.** The deployer owns the Universal Rewards Distributor and can replace its root without the timelock (`setRoot`). Before mainnet: make the owner a multisig behind a timelock, for the launchpad and the distributor. - **Payout rules (off chain).** Each round shares what reached the distributor since the last accepted round, pro rata to the lower of each wallet's $TAX stake at the round's start and end (so stake added just before a payout earns nothing from it). Rounding dust, and anything that arrives while nobody was staked for a whole round, carries over. PAPER drops go in equal shares to wallets with 1,000+ PAPER staked at the token's launch and at release; if nobody qualifies, that 5% goes to $TAX stakers. Rounding dust is burned. - **The early traders' 2% of $TAX.** `launchPlatformToken` keeps 2% of $TAX in the launchpad (`traderDrop`). The owner can release it once (`releaseTraderDrop`) to a new Uniswap MerkleDistributor for a root it chooses; the $TAX can't go anywhere else, and nothing else in the launchpad reads its $TAX balance (fee and pool code use before-and-after differences). The owner decides who is on that list, so the rules and the list should be published before the release. A tree that adds up to less leaves the rest in the distributor for good; one that adds up to more means the last claims fail. If it is never released, the 2% stays locked. - **$TAX's eligible count isn't checked against its root on chain.** The page builds the airdrop list for the same snapshot and refuses to launch if the counts differ. ## Fairness limits (by design) - Unclaimed drops stay in their distributor: Uniswap's MerkleDistributor (used unchanged) has no end date or sweep. Every wallet's drop is pushed to it with Multicall3, so this only affects pushes that fail. - Equal-share PAPER drops (and $TAX airdrop) reward splitting one stake across several wallets of 1,000+ PAPER each. - A payout round weighs the lower of a wallet's stake at the round's start and end, so stake removed in the middle and put back before the end still counts. Rounds should be frequent (daily) to keep this small. - Only wallets with at least 1,000 $TAX staked at the start of a round share in it, which keeps dust stakes from swelling the payout tree. ## Independent reviews (17 Oct 2026) **Second V4 pass, before testnet.** A reviewer diffed every vendored file against upstream (only pragmas and import paths differ) and re-checked every Uniswap action encoding, the fee and drop accounting, Permit2 approvals and pool pre-creation. No critical, high or medium findings. Low findings, fixed: a creator wallet that can't receive HYPE would strand its fees (the creator can now hand its share to another wallet, `setCreator`); 1-wei stakes could pad the staker list the payout builder reads (wallets are listed only once their stake reaches 1,000 $TAX). Documented: big blocks for deploys, the gas cost of a blocked launch, unclaimed drops, block timestamps. **Uniswap V4 version.** A reviewer who hadn't seen the code found no way to take funds or remove pool liquidity. They confirmed the Universal Router build used here is the release deployed on HyperEVM mainnet (v2.2), and that the mainnet PositionManager uses the same action formats. Findings: a pool set up in advance could block a launch (now only when the price is set above the start; documented above); start prices so high that the position exceeds Uniswap's per-tick liquidity cap would make every launch revert (start tick now capped at 340,000, and a clear error for a supply and price that exceed it); HYPE or tokens sent to the launchpad outside fee collection stay there unaccounted (they can't affect a split). **Uniswap V3 version (the design before V4).** A reviewer found no way to take or lock pool liquidity, fees or drops on chain, and every finding was fixed with tests or documented: - Medium-High, fixed: a round built while another was published but not yet accepted could share the same funds twice, leaving the rewards distributor short. Building now refuses while a round is waiting, and the page re-checks the distributor's root just before publishing. - Medium, fixed: stake history could be marked complete up to a block Etherscan hadn't fully indexed, losing a transfer for good. History is now only read up to blocks at least 600 blocks (about 10 minutes) old, and each drop's "at release" check reads the block exactly 7 days after launch, whoever asks and whenever. - Medium, fixed: many dust stakes could make payout rounds too slow to build. Rounds count only wallets with 1,000+ $TAX staked at the start. - Low, fixed: anyone could call the $TAX launcher first in the same block and take $TAX's address; only the launchpad can call it now. Start prices too close to Uniswap's top price are refused. - Owner trust (the distributor owner can set a root without the timelock; the owner can hold back a drop) and the fairness limits above are documented here. ## Checks done - Foundry: 24 launchpad tests plus 2 Merkle cross-checks, against the real Uniswap V4 contracts (PoolManager, PositionManager, Universal Router, Permit2, V4Quoter): the 90/5/5 split, the creator's same-transaction buy, locked liquidity, pool pre-creation, the 30/40/30 split of the 2% fee in HYPE and in the token (fuzzed), the quoter matching the router, drops after 7 days with dust burned and an empty list going to $TAX stakers, the rewards distributor's timelock and claims, $TAX's 50/40/10 split, the refund push through Multicall3, vesting and staking history; plus cross-checks that the server's Merkle trees pay out on the real distributors. - Site tests cover the payout maths (pro rata, carry-over, cumulative rounds), drop eligibility, and that lists are stored once and served from storage. - End to end on a local chain from the page: deploying Uniswap V4 and the launchpad, launching, trading, collecting fees, launching $TAX, the airdrop, staking, releasing and pushing drops, two payout rounds, claims. - Frontend: network, RPC and contract addresses come only from config (a link can't point the page at another chain or contract). Approvals are exact amounts, never unlimited.