Skip to content
The Founding Airdrop is live. 20,000,000 $MOON for the founding crew. Join now →
Reference

Contracts and addresses

Moonfiesta is four contracts plus a stock Uniswap V3 deployment. There is no backend, no database, and no privileged relayer between you and the chain. Everything the web app does, you can do yourself with a wallet and an RPC endpoint.

This page lists every live address, the functions worth calling directly, and the exact commands to check the claims made everywhere else in these docs.

The four contracts

Each contract does one job, and the boundaries between them are the whole design. Nothing here is upgradeable and nothing here is a proxy you have to trust an implementation slot for.

  • LaunchFactory is the only entrypoint. It orchestrates the launch and is the one contract with an owner. It holds no user funds beyond ETH that is owed and unclaimed.
  • LaunchToken is the template every coin is cloned from. Fixed supply, no mint function, no owner, no transfer tax, no blacklist. The clone is an EIP-1167 minimal proxy, so every coin runs the exact same bytecode.
  • LpLocker permanently owns every launch's Uniswap position NFT. It has no owner and no function that transfers, withdraws, or reduces a position. It records the fee split at launch and can harvest fees. That is the entire surface.
  • FeeVault holds harvested fees and tracks who is owed what. Payouts are pull based: it never pushes, and there is no admin path to move an accrued balance.
One launch, end to end
deployToken(params)                    [one transaction]
  |
  |-- 1. clone LaunchToken at the predicted address
  |-- 2. mint 100% of supply to LaunchFactory
  |-- 3. create + initialize the WETH pool at the floor price
  |-- 4. mint the single sided position via the position manager
  |-- 5. finalize: arm anti snipe, zero the factory's admin
  '-- 6. safeTransferFrom(position NFT) --> LpLocker   [forever]

later, on every swap
  the pool accrues 1% in fees to the locked position
  |
  '-- anyone: LpLocker.collectRewards(tokenId)
        |-- splits by the bps fixed at launch
        '-- transfers + credits --> FeeVault
              |
              '-- you: FeeVault.claim(WETH)     [pull based]
Steps 1 through 6 all happen inside a single deployToken call. If any one of them reverts, the whole launch reverts and no half-built market exists.

The factory owns the position NFT for a few opcodes in step 6 and never again. After finalize the token's factory field is set to the zero address, which means no contract on the chain has any admin power over your coin. You can check that yourself in one call.

Live addresses

Robinhood Chain mainnet, chainId 4663. Every link goes to Blockscout at https://robinhoodchain.blockscout.com.

LpLockerholds every position NFT
0x5D5461aAB0D85DC81fc56cc9c3e369881969aC68
LaunchToken implementationclone template, not a tradable coin
0x9f5Ad4AB211DCC3EDeC7fe09F05F10659697dFDF

The Uniswap V3 deployment and WETH9 are not ours. They are the chain's existing infrastructure, and Moonfiesta is just another caller.

Uniswap V3 factorycreates the pool
0x1f7d7550B1b028f7571E69A784071F0205FD2EfA
NonfungiblePositionManagermints the position NFT
0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3
QuoterV2prices the quote in the UI
0x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7
Source verification is pending

The contracts are deployed and their bytecode is on chain, but the Solidity source is not yet verified on Blockscout. Until it is, the explorer shows raw bytecode rather than readable code, and you cannot click through to a Read Contract tab. You can still verify behaviour directly against the RPC with the calls in verify it yourself, which is the stronger check anyway.

Constants and live config

The first group is compiled into the contract and cannot change. The second group is set by the factory owner and applies to future launches only. Anything already launched keeps the values it was born with.

Chain ID
Robinhood Chain mainnet
4663
Pool fee tier
POOL_FEE, constant
1%
Tick spacing
TICK_SPACING, the 1% tier
200
Default supply
DEFAULT_SUPPLY. Pass supply = 0 to take it.
1,000,000,000
Protocol share hard cap
PROTOCOL_BPS_CAP = 4000 bps. Constant. The owner cannot exceed it.
40%
Protocol share
protocolBps. Owner settable up to the 40% cap.
20%
Max community share
maxCommunityBps. The ceiling on the holder carve out you pick per launch.
20%
Creator floor
10000 - protocolBps - communityBps, at the current config.
60%
Launch fee
launchFee. Owner settable. Read it live before you send.
0.0005 ETH
Burn contribution
burnContribution. Owner settable.
0.0012 ETH
Anti snipe max wallet
defaultMaxWalletBps = 200. Buys only.
2%
Anti snipe window
defaultSnipeWindowSecs. Copied into the clone at launch, then fixed.
300s

Treat the second table as documentation, not as a promise. The owner can change those values in a block, so read them from the chain before a launch rather than trusting this page. The commands are below.

LaunchFactory functions

One state changing function matters, plus two you may need afterwards. deployToken is payable and does everything in the diagram above.

struct LaunchParams {
    string  name;
    string  symbol;
    string  tokenURI;          // see the metadata section below
    uint256 supply;            // 0 => DEFAULT_SUPPLY (1e9 * 1e18)
    int24   tickLower;         // single sided range, must be a multiple of 200
    int24   tickUpper;
    uint160 sqrtPriceX96;      // the launch (floor) price
    uint16  communityBps;      // holder carve out, <= maxCommunityBps
    address communityRecipient;
    bytes32 salt;              // caller chosen, scoped to msg.sender
}

function deployToken(LaunchParams calldata p)
    external payable
    returns (address token, address pool, uint256 tokenId);

function predictTokenAddress(address deployer, bytes32 salt)
    external view returns (address);

function withdrawEth() external returns (uint256 amount);
function ethOwed(address account) external view returns (uint256);

tickLower and tickUpper are not conveniences you can skip. The factory rejects a range that is not aligned to the tick spacing, and it rejects the whole launch with SingleSidedMintIncomplete if the position manager did not take at least 99.99% of your supply. A range that straddles or sits below spot would strand part of the supply, so the transaction reverts rather than shipping a broken market.

The pool is also price checked. If the pool already exists the factory only initializes it when it is unpriced, then requires the live price to equal your sqrtPriceX96 exactly, or reverts with PoolMispriced. Someone who front runs you by creating the pool at a hostile price cannot make your launch go through at their price.

Overpaying is not auto refunded

deployToken requires msg.value >= launchFee + burnContribution. Anything above that is credited to ethOwed[msg.sender] rather than sent back, because pushing ETH inside the launch would let a reverting recipient brick it. Call withdrawEth to collect it whenever you want. It does not expire.

LpLocker functions

Read the list and notice what is missing. There is no withdraw, no unlock, no migrate, no transfer, no owner, and no upgrade path. That absence is the whole point of the contract. See locked liquidity for what that does and does not protect you from.

function collectRewards(uint256 tokenId) external;

function tokenIdOf(address launchToken) external view returns (uint256);

function positionInfo(uint256 tokenId) external view returns (
    address launchToken,
    address creator,
    address protocolRecipient,
    address communityRecipient,
    uint16  creatorBps,
    uint16  protocolBps,
    uint16  communityBps,
    bool    registered
);

collectRewards is permissionless. Anyone can harvest anyone else's token, and the fees still go where the split says. If you are a holder waiting on a revenue share you do not need the creator to act: call it yourself and pay the gas. It sweeps both sides of the pair, splits by the bps recorded at launch, and credits the FeeVault. Rounding dust goes to the creator.

positionInfo is the receipt. It is written once, when the NFT lands in the locker, and there is no function that writes it again. That is what makes the split immutable per token: not a promise in a blog post, but the absence of a setter.

FeeVault functions

Two claim functions and one getter. Fees arrive as WETH, so token is almost always the WETH address above.

function owed(address recipient, address token) external view returns (uint256);

function claim(address token) external returns (uint256 amount);
function claimMany(address[] calldata tokens) external;

claim reverts with NothingToClaim on a zero balance, which trips people up: if owed reads zero, nobody has harvested since your last claim. Call LpLocker.collectRewards first, then claim. claimMany skips zero balances instead of reverting, so it is the safe one to batch with.

Harvesting and claiming are two separate steps on purpose. See fees and rewards for the arithmetic on a real split.

LaunchToken functions

A standard ERC20 plus a handful of launch specific getters. There is no mint, no owner, no setFee, and no blacklist to find.

function tokenURI()        external view returns (string memory);
function pool()            external view returns (address);
function factory()         external view returns (address);  // 0x0 after launch
function launched()        external view returns (bool);
function maxWalletBps()    external view returns (uint16);
function snipeWindowSecs() external view returns (uint32);
function snipeEndTime()    external view returns (uint256);
function maxWalletAmount() external view returns (uint256);  // supply * bps / 10000

The only non standard transfer logic is the anti snipe cap, and it is narrow: it applies only when the sender is the pool, only while block.timestamp <= snipeEndTime, and only to the receiving balance. Sells into the pool, wallet to wallet transfers, and burns are never capped, so it cannot be used to trap you in a position. It is a deterrent, not a control: read why.

The TokenLaunched event

One event carries everything an indexer needs. Both token and creator are indexed, so you can filter by either without scanning.

event TokenLaunched(
    address indexed token,     // the new coin
    address indexed creator,   // msg.sender of the launch
    address pool,              // the Uniswap V3 pool, 1% tier, paired with WETH
    uint256 tokenId,           // the locked position NFT id
    uint256 supply,            // full supply, all of it in the pool
    uint16  creatorBps,        // fee split, fixed here forever
    uint16  protocolBps,
    uint16  communityBps
);

The three bps fields always sum to 10000. The locker independently re rejects the transfer with BadSplit if they do not, so a launch where they disagree cannot exist. Other events worth watching: PositionLocked and RewardsCollected on the locker, Credited and Claimed on the vault.

The tokenURI convention

There is no IPFS pin and no metadata server. A coin's image, description, and links live inside the on chain tokenURI string as a base64 JSON data URI, written once at launch. If the chain is up, the metadata is up.

data:application/json;base64,<base64 of>

{
  "image":       "https://... | ipfs://... | data:image/...",
  "description": "up to 500 characters",
  "telegram":    "https://t.me/...",
  "twitter":     "https://x.com/...",
  "website":     "https://..."
}

Every field is optional and empty ones are dropped before encoding, so a bare {} is legal. Storage costs roughly 710 gas per byte, which is why the description is capped at 500 characters and the whole string at 4096 bytes. A large inline image will blow through that; host it and store the URL.

Legacy tokens

Tokens launched before this encoding existed hold a bare image URL in tokenURI instead of a JSON blob. Any reader should treat a value that does not start with data:application/json as { image: uri }. The SDK's parseTokenURI does exactly that, and never throws: it runs against arbitrary bytes that any stranger can write, so it degrades to an empty object rather than failing.

Because anyone can put anything in that string, treat it as user input and not as fact. A twitter link in the metadata is a claim by the creator, not a verification of anything.

Why the token address is predictable

The factory does not deploy your token with a plain CREATE. It uses a deterministic clone:

token = Clones.cloneDeterministic(
    tokenImplementation,
    keccak256(abi.encodePacked(msg.sender, p.salt))
);

This is not a gimmick, it is a requirement. Uniswap orders a pair by address, so whether your coin is token0 or token1 depends on how its address compares to WETH's. That ordering decides which side of spot the single sided range has to sit on, which decides the sign of the ticks. The tick range cannot be computed until the address is known, and the address has to be known before the transaction is sent. So it has to be predictable.

The safety comes from the msg.sender in the hash. The salt is scoped to the caller, so your salt and someone else's salt produce different addresses. Nobody can compute the address you are about to use and take it, because they cannot deploy from your address slot at all.

The remaining edge is someone squatting the pool for an address you were going to use. That fails for two reasons. The mispricing check rejects it outright, and because this is CREATE2 rather than a nonce, a blocked salt only blocks that one salt. You retry with a fresh one and nothing has been consumed. There is no shared counter to roll back, so a single squat cannot brick the factory for everyone.

One more consequence: the implementation itself is locked in its own constructor. The template cannot be initialized, so nobody can hijack it and make every future clone point at something hostile.

Verify it yourself

These are real calls against the public RPC. You need cast from Foundry. Nothing here needs a wallet or a key except the last one.

export RPC=https://rpc.mainnet.chain.robinhood.com
export FACTORY=0x8AA1DF66a44C97D5B182451916E99fCCd095f7Ac
export LOCKER=0x5D5461aAB0D85DC81fc56cc9c3e369881969aC68
export VAULT=0x6Ef8d43e565b2AD52909591427089b41083bE0De
export NPM=0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3
export WETH=0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73

# The coin you care about. Take it from the address bar on /token?address=0x...
export TOKEN=0xYourTokenAddress
export YOU=0xYourWallet

Is the liquidity actually locked? This is the only question that matters, and it is two calls.

# Which position holds this token's liquidity
cast call $LOCKER "tokenIdOf(address)(uint256)" $TOKEN --rpc-url $RPC
export ID=<the number that came back>

# Who owns that position NFT
cast call $NPM "ownerOf(uint256)(address)" $ID --rpc-url $RPC
# Must equal $LOCKER. The locker has no transfer path, so this is terminal.

Is the whole supply in the pool? Compare the pool balance against total supply. They should match, minus tick rounding dust.

cast call $TOKEN "totalSupply()(uint256)" --rpc-url $RPC
cast call $TOKEN "pool()(address)" --rpc-url $RPC
cast call $TOKEN "balanceOf(address)(uint256)" $(cast call $TOKEN "pool()(address)" --rpc-url $RPC) --rpc-url $RPC

Does anyone have admin over the coin? The factory zeroes itself out during the launch.

cast call $TOKEN "factory()(address)" --rpc-url $RPC
# Expect 0x0000000000000000000000000000000000000000

cast call $TOKEN "launched()(bool)" --rpc-url $RPC
cast call $TOKEN "snipeEndTime()(uint256)" --rpc-url $RPC
# Compare against: date +%s

What is the fee split, and what am I owed?

# The immutable split recorded at launch
cast call $LOCKER \
  "positionInfo(uint256)(address,address,address,address,uint16,uint16,uint16,bool)" \
  $ID --rpc-url $RPC

# What is claimable for you right now, in WETH
cast call $VAULT "owed(address,address)(uint256)" $YOU $WETH --rpc-url $RPC

# Harvest for everyone (permissionless, costs you gas, pays whoever the split says)
cast send $LOCKER "collectRewards(uint256)" $ID --rpc-url $RPC --private-key $PK

What is the factory config right now? Do this before a launch rather than trusting the table above.

cast call $FACTORY "launchFee()(uint256)" --rpc-url $RPC
cast call $FACTORY "burnContribution()(uint256)" --rpc-url $RPC
cast call $FACTORY "protocolBps()(uint16)" --rpc-url $RPC
cast call $FACTORY "maxCommunityBps()(uint16)" --rpc-url $RPC
cast call $FACTORY "defaultMaxWalletBps()(uint16)" --rpc-url $RPC
cast call $FACTORY "defaultSnipeWindowSecs()(uint32)" --rpc-url $RPC

# And the address a given salt would produce for you
cast call $FACTORY "predictTokenAddress(address,bytes32)(address)" \
  $YOU 0x0000000000000000000000000000000000000000000000000000000000000001 \
  --rpc-url $RPC

Every clone shares one implementation, so checking the bytecode once checks it for every coin on the platform. cast code $TOKEN returns the 45 byte EIP-1167 proxy, and the address embedded in the middle of it must be the LaunchToken implementation listed above. If it is not, it did not come from this factory.

What the owner key can and cannot do

LaunchFactory is Ownable, and the owner key is compromised. That is the real state of the system, so here is exactly what it means at the contract level.

Future launches are exposed

Whoever holds the owner key can call setFees, setSplit, setPayoutAddresses, and setAntiSnipe. In practice they could raise the launch fee, raise the protocol share up to the 40% hard cap, redirect the protocol and buyback payouts to themselves, or set a hostile anti snipe configuration. The window is a uint32 of seconds and the cap is a bps value, so a tiny cap with a very long window would throttle buys on coins launched after that change.

Read the live config before you launch. The commands are directly above this section.

Already launched tokens are out of reach

The anti snipe values are copied into the clone's own storage at initialize, and the fee split is written into positionInfo when the NFT is locked. Neither has a setter. An owner change applies to launches after it, and cannot reach backwards into a coin that already exists.

The owner also has no power over the other three contracts. LpLocker has no owner and no way out for a position. FeeVault has no admin path to an accrued balance. Their only privileged functions, setFactory and setLocker, are one shot wiring calls that were already used at deploy and revert forever after.

Unaudited

None of this code has been through a third party audit. It is tested and it has been reviewed internally, and neither of those is an audit. Locked liquidity and an immutable split are properties of the code as written, and the code could still have a bug that nobody has found. Read safety and risks before you put money in.