Robinhood MainnetChain ID: 4663
Contract detail

MangoLaunchFactory

MangoLaunchFactory is a verified Robinhood Chain smart contract at 0xa2103eb3…bc0632b1 with 3 indexed transactions.

Address type
Smart contract
Transactions
3
First indexed block
#28590015
Latest indexed block
#28593891
Verification
Verified
Compiler
v0.8.35+commit.47b9dedd
Verification match
full
Creation transaction
0x318b3caf…419dfb30
Created at block
#28464892
Language
solidity
EVM version
osaka
License
none
Compiler optimization
Disabled
Related activity

Transactions

Verified contract

Contract source code

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

// ============================================================================
// MangoLaunchFactory — DRAFT, UNCOMPILED, HIGH UNCERTAINTY
// ============================================================================
// This is genuinely the most complex, least-verified contract in this
// project so far. Unlike the hook and token, which now compile clean, this
// one involves a Uniswap v4 mechanic none of the others needed: the
// unlock/callback pattern. v4 requires most PoolManager state changes
// (including adding liquidity) to happen inside a callback triggered by
// PoolManager.unlock() — you can't just call modifyLiquidity() directly the
// way you might expect from reading v3-era mental models. This file
// implements that pattern from documentation, not from a confirmed working
// example, and the exact liquidity-math calls (specifically the amounts
// passed to modifyLiquidity) are the least confident part of this draft.
//
// Treat this file as a strong starting structure, not working code. Expect
// multiple real rounds of compiler fixes — likely more than the hook took,
// given the added complexity here.
// ============================================================================

import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "v4-core/interfaces/callback/IUnlockCallback.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol";
import {BalanceDelta} from "v4-core/types/BalanceDelta.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {LiquidityAmounts} from "v4-periphery/src/libraries/LiquidityAmounts.sol";

import {MangoLaunchToken} from "./MangoLaunchToken.sol";
import {MangoLaunchHook} from "./MangoLaunchHook.sol";
import {MangoLaunchRegistry} from "./MangoLaunchRegistry.sol";

contract MangoLaunchFactory is IUnlockCallback {
    IPoolManager public immutable poolManager;
    MangoLaunchHook public immutable hook;
    MangoLaunchRegistry public immutable registry;
    address public immutable protocolTreasury;

    uint256 public constant DEFAULT_GRADUATION_THRESHOLD_USD = 30_000 * 1e8; // matches the registry's 8-decimal convention

    // Deliberately NO cap on developer buy size. Considered a 50% cap
    // (real precedent exists — Virtuals Protocol documents exactly that),
    // decided against it: everything here is on-chain and visible, and a
    // large buy that gets burned afterward is a genuinely positive signal
    // for holders, not a rug pattern. A hard cap would block that
    // legitimate case along with anything else. Left undocumented in code
    // (no constant to remove later) — this is a deliberate design choice,
    // not an oversight.
    uint256 private constant BPS_DENOMINATOR = 10_000;

    // Starting valuation: $4,000 for the full 1B supply. Not arbitrary —
    // this matches pump.fun's own documented starting market cap ($4,500 at
    // first buy), the platform this entire category is modeled after, and
    // sits within the range observed across comparable launchpads generally.
    //
    // Computed precisely (not hand-approximated) for currency0=ETH,
    // currency1=token, both 18 decimals: at $4,000 valuation and ETH at
    // $3,120, price (token1 per token0) = 780,000,000. sqrtPriceX96 =
    // sqrt(780,000,000) * 2^96. Verified by reversing the calculation back
    // to exactly $4,000.00 — real math, not a placeholder.
    //
    // Worth flagging plainly: at this starting price, a single 1 ETH buy
    // would capture roughly 78% of the entire supply. That's aggressive —
    // whether that's the intended feel (matches pump.fun's own dynamics,
    // where early buyers can capture large early shares) or too
    // concentrated for this specific product is a real decision still
    // worth revisiting, not something this number alone resolves. The tick
    // range chosen for the liquidity position (not yet decided) also
    // meaningfully affects how this plays out in practice — a flat price
    // until exhausted behaves very differently from a gradual curve even
    // at the same starting valuation.
    uint160 public constant DEFAULT_STARTING_SQRT_PRICE_X96 = 2212722159151846836555898085703680;

    // The single-sided position's tick range — entirely BELOW the starting
    // price, which is what makes it composed purely of the token side (no
    // ETH needed to seed it). Deliberately wide, not narrow: a wide range
    // spreads the 1B supply across many price points, so price rises
    // gradually as tokens sell — closer to pump.fun's curve feel than a
    // narrow range, which would behave like a flat price until a sudden
    // cliff. Both bounds must be multiples of tickSpacing (60, set below).
    //
    // TickMath.MIN_TICK is the absolute floor Uniswap allows. The upper
    // bound is set just below the starting tick (calculated from
    // DEFAULT_STARTING_SQRT_PRICE_X96), rounded to the nearest valid
    // tickSpacing multiple — this exact calculation is the least-verified
    // number in this file and needs real testing, not just a clean compile.
    int24 public constant TICK_LOWER = -887220; // TickMath.MIN_TICK rounded to a multiple of 60
    // Precisely computed (not approximated) from DEFAULT_STARTING_SQRT_PRICE_X96:
    // exact tick = 204758.28, rounded down to the nearest tickSpacing=60
    // multiple = 204720. Verified by reversing the calculation back to a
    // sqrtPriceX96 within 0.19% of the original target — real math, not a
    // guess, though still worth confirming against TickMath's actual
    // compiled behavior once this reaches a real compiler.
    int24 public constant TICK_UPPER = 204720;

    event TokenLaunched(address indexed token, address indexed creator, bytes32 indexed poolId, uint256 devBuyEth);

    error ZeroAddress();
    error EmptyName();

    // Encodes what a launch actually does, passed through unlock() into the
    // callback below — this indirection is required by v4's unlock pattern,
    // not an unnecessary abstraction.
    struct LaunchParams {
        string name;
        string symbol;
        address creator;
        uint256 devBuyEth;
    }

    constructor(IPoolManager _poolManager, MangoLaunchHook _hook, MangoLaunchRegistry _registry, address _protocolTreasury) {
        if (address(_hook) == address(0) || _protocolTreasury == address(0)) revert ZeroAddress();
        poolManager = _poolManager;
        hook = _hook;
        registry = _registry;
        protocolTreasury = _protocolTreasury;
    }

    // Entry point — the only function meant to be called directly. Takes an
    // optional ETH developer buy alongside the launch itself.
    function launch(string calldata name, string calldata symbol, address creator) external payable returns (address token) {
        if (bytes(name).length == 0 || bytes(symbol).length == 0) revert EmptyName();
        if (creator == address(0)) revert ZeroAddress();

        LaunchParams memory params = LaunchParams({
            name: name,
            symbol: symbol,
            creator: creator,
            devBuyEth: msg.value
        });

        bytes memory result = poolManager.unlock(abi.encode(params));
        token = abi.decode(result, (address));
    }

    // Called back by PoolManager during unlock() — this is where the actual
    // token deployment, pool creation, and liquidity seeding happens. Per
    // v4's design, PoolManager only trusts calls originating from itself
    // here, hence the onlyPoolManager-equivalent check below.
    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        require(msg.sender == address(poolManager), "MangoLaunchFactory: not pool manager");
        LaunchParams memory params = abi.decode(data, (LaunchParams));

        // 1. Deploy the token — mints the full 1B supply to this factory.
        MangoLaunchToken newToken = new MangoLaunchToken(params.name, params.symbol);
        address tokenAddress = address(newToken);

        // 2. Build the pool key — token paired against native ETH, hook
        // attached. Currency ordering matters in v4 (lower address is
        // currency0) — this comparison needs verification against actual
        // compiled Currency behavior, not just assumed.
        // Currency ordering in v4 requires the lower address as currency0.
        // Native ETH is represented as address(0) — the literal minimum
        // possible address — so it is deterministically ALWAYS currency0
        // against any real token contract, which will always have a
        // non-zero, numerically larger address. This isn't a runtime
        // decision (an earlier draft of this file wrote it as one via a
        // comparison that could only ever evaluate one way — dead code,
        // fixed here to just state the fact directly).
        PoolKey memory key = PoolKey({
            currency0: Currency.wrap(address(0)),
            currency1: Currency.wrap(tokenAddress),
            fee: 3000, // 0.3% base LP fee tier — standard, on top of the hook's own separate 3% skim
            tickSpacing: 60,
            hooks: hook
        });

        // 3. Initialize the pool.
        poolManager.initialize(key, DEFAULT_STARTING_SQRT_PRICE_X96);

        // 4. Seed liquidity with the full token supply. The position sits
        // entirely below the starting price (TICK_LOWER to TICK_UPPER, both
        // below the starting tick), which is what makes it composed purely
        // of the token side — no ETH required to seed it. Any developer buy
        // happens as a SEPARATE, immediate follow-up swap after this, not
        // mixed into the seeding liquidity itself, keeping this step's
        // math simpler and easier to verify independently.
        //
        // getLiquidityForAmount1 is the standard, battle-tested Uniswap
        // periphery function for exactly this calculation — deliberately
        // not hand-rolled. Its exact import path (v4-periphery/libraries/
        // LiquidityAmounts.sol) is written from the documented package
        // layout, not yet confirmed against this specific installed
        // version — given tonight's repeated pattern of version
        // mismatches on similar imports, treat this path as the most
        // likely candidate for a real compiler error, not a certainty.
        uint128 liquidity = LiquidityAmounts.getLiquidityForAmount1(
            TickMath.getSqrtPriceAtTick(TICK_LOWER),
            TickMath.getSqrtPriceAtTick(TICK_UPPER),
            newToken.TOTAL_SUPPLY()
        );

        poolManager.modifyLiquidity(
            key,
            IPoolManager.ModifyLiquidityParams({
                tickLower: TICK_LOWER,
                tickUpper: TICK_UPPER,
                liquidityDelta: int256(uint256(liquidity)),
                salt: bytes32(0)
            }),
            ""
        );

        // 4b. Settle the token owed to PoolManager for the liquidity just
        // added. v4's flash accounting requires this explicitly — the
        // modifyLiquidity() call alone only records the debt, it doesn't
        // move any tokens. sync() tells PoolManager to start tracking this
        // currency's balance, the transfer actually delivers the tokens,
        // settle() reconciles the two. This exact three-step pattern is
        // documented v4 behavior, but genuinely not yet confirmed against
        // this specific installed version's function signatures — same
        // category of uncertainty flagged honestly elsewhere in this file.
        Currency tokenCurrency = Currency.wrap(tokenAddress);
        poolManager.sync(tokenCurrency);
        // Explicitly checked rather than assumed — OpenZeppelin's ERC20
        // implementation (which MangoLaunchToken is built on) always
        // reverts on failure rather than returning false, so this should
        // never actually trip. But "should never trip by reasoning" is
        // exactly the category of assumption that causes real bugs, so
        // it's a real check instead of a silenced warning, same principle
        // used in the hook contract's own overflow check.
        bool success = newToken.transfer(address(poolManager), newToken.TOTAL_SUPPLY());
        require(success, "MangoLaunchFactory: token settlement transfer failed");
        poolManager.settle();

        // 5. If a developer buy was requested, execute it as a real swap —
        // paying the ETH in, receiving tokens out, sent to the creator
        // since it's their own purchase. amountSpecified is negative for
        // "exact input" in v4's convention (this sign convention is the
        // single least-confident line in this whole block — needs real
        // verification, not just a clean compile, before trusting it).
        if (params.devBuyEth > 0) {
            IPoolManager.SwapParams memory swapParams = IPoolManager.SwapParams({
                zeroForOne: true, // paying currency0 (ETH) for currency1 (token)
                amountSpecified: -int256(params.devBuyEth),
                sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1 // no meaningful slippage bound yet — see checklist
            });

            BalanceDelta swapDelta = poolManager.swap(key, swapParams, "");

            // Settle the ETH owed (native currency, sent with value rather
            // than transferred like the token above), then collect the
            // tokens received from the swap and send them to the creator.
            // No cap on how much this can be — deliberate, see the note
            // above BPS_DENOMINATOR.
            poolManager.settle{value: params.devBuyEth}();
            int128 tokensReceived = swapDelta.amount1();
            if (tokensReceived > 0) {
                // Safe: only reached when tokensReceived is already
                // confirmed positive by the check above.
                // forge-lint: disable-next-line(unsafe-typecast)
                poolManager.take(tokenCurrency, params.creator, uint256(uint128(tokensReceived)));
            }
        }

        // 5. Register the creator with the hook, and the launch with the
        // registry — atomically, in this same transaction, closing the
        // exact gap flagged in the hook's own registerCreator() comments.
        bytes32 poolId = keccak256(abi.encode(key));
        hook.registerCreator(poolId, params.creator);
        registry.registerLaunch(poolId, params.creator, tokenAddress, DEFAULT_GRADUATION_THRESHOLD_USD);

        emit TokenLaunched(tokenAddress, params.creator, poolId, params.devBuyEth);

        return abi.encode(tokenAddress);
    }
}

// ============================================================================
// Honest status of this file — do not treat as more finished than this:
// ============================================================================
// [x] Compiles cleanly — confirmed via real forge build output, after
//     fixing the LiquidityAmounts import path and the TOTAL_SUPPLY access
//     pattern (both real errors caught by the compiler, not guessed)
// [x] modifyLiquidity() is now real code using the standard
//     LiquidityAmounts.getLiquidityForAmount1() calculation, not a
//     placeholder comment — still unverified against a real compiler
// [x] DEFAULT_STARTING_SQRT_PRICE_X96 is a real, computed $4,000 starting
//     valuation (researched against pump.fun's own documented figure),
//     verified by reversing the calculation
// [x] TICK_LOWER/TICK_UPPER are now precisely computed, not approximated —
//     verified by reversing the tick calculation to within 0.19% of target
// [x] Settlement of the token owed to PoolManager after modifyLiquidity()
//     is now real code (sync -> transfer -> settle) — the exact function
//     signatures used (sync(), settle()) are written from documented v4
//     patterns, NOT yet individually confirmed against this specific
//     installed version, same category of uncertainty that's bitten this
//     project before (SwapParams' location, BaseHook's existence)
// [x] The developer buy swap is now real code — a real poolManager.swap()
//     call, real settlement of the ETH side, real collection of the
//     tokens received. Two specific things in this block are the least
//     confident lines in the whole file: the amountSpecified sign
//     convention (negative for exact-input, per documented v4 convention,
//     not verified against real swap behavior) and sqrtPriceLimitX96 using
//     TickMath.MIN_SQRT_PRICE + 1, which means genuinely NO real slippage
//     protection right now — it accepts any price. That's fine for a
//     compile check, not acceptable before real ETH touches this
// [ ] hook and registry must both have this factory's address set as their
//     launchOperator/factory before this can actually call
//     registerCreator()/registerLaunch() successfully — that's a separate
//     configuration step, not automatic
// [x] Currency ordering is now correctly stated as deterministic (ETH is
//     always currency0), not a runtime comparison that could only ever
//     resolve one way
// [ ] No REAL slippage/deadline protection anywhere in this file yet —
//     the placeholder sqrtPriceLimitX96 above must be replaced with a real
//     bound before any real ETH touches this
// [ ] Full test suite, then audit, same requirement as every other contract
// ============================================================================