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.
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]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.
The Uniswap V3 deployment and WETH9 are not ours. They are the chain's existing infrastructure, and Moonfiesta is just another caller.
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.
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.
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 / 10000The 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.
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=0xYourWalletIs 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 $RPCDoes 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 +%sWhat 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 $PKWhat 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 $RPCEvery 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.
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.
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.
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.