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
- Funding transaction
- 0x1bc5abac…87df9935 in block #28590724
- 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
| Transaction | Block | From | To | Value (wei) |
|---|---|---|---|---|
| 0x7cd35ed3…400303fd | #28593891 | 0xf07becc2…03582e88 | 0xa2103eb3…bc0632b1 | 100000000000000 |
| 0x1bc5abac…87df9935 | #28590724 | 0xf07becc2…03582e88 | 0xa2103eb3…bc0632b1 | 10000000000000 |
| 0x5c54c54a…5637e924 | #28590015 | 0xf07becc2…03582e88 | 0xa2103eb3…bc0632b1 | 0 |
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
// ============================================================================