Contract detail
HoodLaunchpad
HoodLaunchpad is a verified Robinhood Chain smart contract at 0x5037b1ea…f9f6f1c2 with 0 indexed transactions.
- Address type
- Smart contract
- Transactions
- 0
- History coverage
- No indexed activity found
- Emitted events
- 1
- Verification
- Verified
- Compiler
- v0.8.28+commit.7893614a
- Verification match
- full
- Creation transaction
- 0xa98775e2…6c97fc13
- Created at block
- #1045266
- Language
- solidity
- EVM version
- prague
- License
- none
- Compiler optimization
- Enabled · 5000 runs
Verified contract
Contract source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {HoodToken} from "./HoodToken.sol";
import {IUniswapV2Factory, IUniswapV2Pair, IWETH} from "./interfaces/IUniswapV2.sol";
/// @title HoodLaunchpad
/// @notice hood.fun core: token factory, constant-product bonding curve with
/// virtual reserves, and permissionless graduation to Uniswap v2.
///
/// Curve model (pump.fun-style x*y=k with virtual reserves, quoted in ETH):
/// - Each token mints CURVE_SUPPLY + LP_SUPPLY to this contract.
/// - Buys pull from CURVE_SUPPLY against growing virtual ETH reserves.
/// - When the last curve token is sold the curve graduates; anyone may then
/// call `migrate`, which pairs LP_SUPPLY with the raised ETH on Uniswap v2
/// and burns the LP tokens, making the migrated liquidity unruggable.
///
/// Security posture:
/// - Tokens are transfer-locked (except to/from this contract) until
/// graduation, closing pre-created-pair / pre-seeded-liquidity exploits.
/// - Migration mints directly against the pair and tolerates a pre-existing
/// pair; stray donations are absorbed into burned LP.
/// - Checks-effects-interactions plus reentrancy guards on all mutators.
/// - Slippage bounds (minOut) on every trade; optional launch snipe guard
/// capping per-wallet buys during the first blocks after creation.
/// - No privileged withdrawals other than accrued protocol fees.
contract HoodLaunchpad is Ownable2Step, ReentrancyGuard {
// ---------------------------------------------------------------- types
struct Curve {
uint128 virtualEth; // current virtual ETH reserve
uint128 virtualTokens; // current virtual token reserve
uint128 realEth; // ETH held for this curve (excludes protocol fees)
uint128 realTokens; // tokens still purchasable on the curve
address creator;
uint48 createdAtBlock;
bool graduated;
bool migrated;
uint16 tradeFeeBps; // creator-chosen fee on buys/sells for THIS token
}
struct Config {
uint128 virtualEthSeed; // initial virtual ETH reserve for new curves
uint64 creationFee; // flat fee to create a token
uint16 defaultTradeFeeBps; // suggested default fee if creator picks none
uint64 migrationFee; // flat fee taken from raised ETH at migration
uint16 migrationFeeBps; // percentage fee on raised ETH at migration
uint16 guardBlocks; // snipe-guard window after creation
uint16 guardMaxWalletBps; // max curve-supply share one wallet may buy in window
uint16 creatorFeeShareBps; // creator's share of every fee (trades + migration)
bool vanityEnforced; // require token addresses to end in 0x600d
}
// ------------------------------------------------------------ constants
uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18;
uint256 public constant CURVE_SUPPLY = 800_000_000e18;
uint256 public constant LP_SUPPLY = TOTAL_SUPPLY - CURVE_SUPPLY;
uint256 public constant VIRTUAL_TOKEN_SEED = 1_073_000_000e18;
uint256 internal constant BPS = 10_000;
uint256 public constant MIN_TRADE_FEE_BPS = 100; // floor, 1%
uint256 public constant MAX_TRADE_FEE_BPS = 500; // hard cap, 5%
uint256 public constant MAX_MIGRATION_FEE_BPS = 1000; // hard cap, 10% of raise
address internal constant LP_BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD;
/// @dev Brand suffix: every hood.fun token address ends in 0x600d.
uint256 internal constant VANITY_SUFFIX = 0x600d;
// -------------------------------------------------------------- storage
IUniswapV2Factory public immutable uniswapFactory;
IWETH public immutable weth;
Config public config;
uint256 public protocolFeesAccrued;
mapping(address token => Curve) public curves;
mapping(address token => mapping(address wallet => uint256)) public guardBought;
mapping(address token => uint256) public creatorFees;
address[] public allTokens;
// --------------------------------------------------------------- events
event TokenCreated(
address indexed token,
address indexed creator,
string name,
string symbol,
string metadataURI,
uint256 virtualEth,
uint256 virtualTokens,
uint256 curveSupply
);
/// @dev Post-trade virtual reserves are emitted so indexers can derive
/// price/OHLCV without extra RPC reads (Uniswap Sync-style).
event Trade(
address indexed token,
address indexed trader,
bool isBuy,
uint256 ethAmount,
uint256 tokenAmount,
uint256 fee,
uint256 virtualEthAfter,
uint256 virtualTokensAfter
);
event Graduated(address indexed token, uint256 raisedEth);
event Migrated(
address indexed token, address indexed pair, uint256 ethLiquidity, uint256 tokenLiquidity, uint256 lpBurned
);
event ConfigUpdated(Config config);
event FeesWithdrawn(address indexed to, uint256 amount);
event CreatorFeesClaimed(address indexed token, address indexed creator, uint256 amount);
// --------------------------------------------------------------- errors
error UnknownToken();
error AlreadyGraduated();
error NotGraduated();
error AlreadyMigrated();
error ZeroAmount();
error SlippageExceeded();
error InsufficientPayment();
error SnipeGuardExceeded();
error FeeTooHigh();
error EthTransferFailed();
error NothingToWithdraw();
error BadVanitySalt();
error NotCreator();
// ---------------------------------------------------------- constructor
constructor(address uniswapFactory_, address weth_, address owner_) Ownable(owner_) {
uniswapFactory = IUniswapV2Factory(uniswapFactory_);
weth = IWETH(weth_);
config = Config({
virtualEthSeed: 2 ether,
creationFee: 0,
defaultTradeFeeBps: 100, // 1% default; creators may pick 0..MAX
migrationFee: 0.05 ether,
migrationFeeBps: 300, // 3% of the raise at migration
guardBlocks: 100,
guardMaxWalletBps: 1000, // 10% of curve supply per wallet in the window
creatorFeeShareBps: 5000, // creator earns 50% of every fee
vanityEnforced: true
});
}
// ---------------------------------------------------------------- admin
function setConfig(Config calldata newConfig) external onlyOwner {
if (newConfig.defaultTradeFeeBps > MAX_TRADE_FEE_BPS) revert FeeTooHigh();
if (newConfig.migrationFeeBps > MAX_MIGRATION_FEE_BPS) revert FeeTooHigh();
if (newConfig.creatorFeeShareBps > BPS) revert FeeTooHigh();
if (newConfig.virtualEthSeed == 0) revert ZeroAmount();
config = newConfig;
emit ConfigUpdated(newConfig);
}
function withdrawFees(address to) external onlyOwner nonReentrant {
uint256 amount = protocolFeesAccrued;
if (amount == 0) revert NothingToWithdraw();
protocolFeesAccrued = 0;
(bool ok,) = to.call{value: amount}("");
if (!ok) revert EthTransferFailed();
emit FeesWithdrawn(to, amount);
}
/// @notice Claim the creator's accrued share of trade fees for a token.
function claimCreatorFees(address token) external nonReentrant {
if (curves[token].creator != msg.sender) revert NotCreator();
uint256 amount = creatorFees[token];
if (amount == 0) revert NothingToWithdraw();
creatorFees[token] = 0;
(bool ok,) = msg.sender.call{value: amount}("");
if (!ok) revert EthTransferFailed();
emit CreatorFeesClaimed(token, msg.sender, amount);
}
function _accrueFee(address token, uint256 fee) internal {
uint256 creatorCut = (fee * config.creatorFeeShareBps) / BPS;
if (creatorCut != 0) creatorFees[token] += creatorCut;
protocolFeesAccrued += fee - creatorCut;
}
// --------------------------------------------------------------- create
/// @notice Deploy a new token and its bonding curve. Any ETH sent beyond
/// the creation fee is used as the creator's initial buy, which
/// executes atomically inside this transaction — a guaranteed
/// dev-first-buy that cannot be front-run.
/// @param salt CREATE2 salt (mixed with msg.sender so it cannot be stolen
/// from the mempool). When vanity enforcement is on, grind salts
/// client-side until the predicted address ends in 0x600d.
/// @param tradeFeeBps creator-chosen buy/sell fee for this token, capped at
/// MAX_TRADE_FEE_BPS. Lower fee = cheaper to trade (attracts traders);
/// the creator still earns their share of whatever they set.
function createToken(
string calldata name,
string calldata symbol,
string calldata metadataURI,
uint256 minTokensOut,
bytes32 salt,
uint16 tradeFeeBps
) external payable nonReentrant returns (address token) {
Config memory cfg = config;
if (msg.value < cfg.creationFee) revert InsufficientPayment();
if (tradeFeeBps < MIN_TRADE_FEE_BPS || tradeFeeBps > MAX_TRADE_FEE_BPS) revert FeeTooHigh();
protocolFeesAccrued += cfg.creationFee;
token = address(
new HoodToken{salt: keccak256(abi.encodePacked(msg.sender, salt))}(name, symbol, TOTAL_SUPPLY)
);
if (cfg.vanityEnforced && uint160(token) & 0xffff != VANITY_SUFFIX) revert BadVanitySalt();
curves[token] = Curve({
virtualEth: cfg.virtualEthSeed,
virtualTokens: uint128(VIRTUAL_TOKEN_SEED),
realEth: 0,
realTokens: uint128(CURVE_SUPPLY),
creator: msg.sender,
createdAtBlock: uint48(block.number),
graduated: false,
migrated: false,
tradeFeeBps: tradeFeeBps
});
allTokens.push(token);
emit TokenCreated(
token, msg.sender, name, symbol, metadataURI, cfg.virtualEthSeed, VIRTUAL_TOKEN_SEED, CURVE_SUPPLY
);
uint256 buyEth = msg.value - cfg.creationFee;
if (buyEth > 0) {
// Creator's bundled dev-buy is exempt from the snipe guard: the
// token did not exist before this tx, so there is nothing to snipe.
_buy(token, buyEth, minTokensOut, cfg, true);
} else if (minTokensOut != 0) {
revert SlippageExceeded();
}
}
// ---------------------------------------------------------------- trade
function buy(address token, uint256 minTokensOut) external payable nonReentrant {
if (msg.value == 0) revert ZeroAmount();
_requireLiveCurve(token);
_buy(token, msg.value, minTokensOut, config, false);
}
function sell(address token, uint256 tokenAmount, uint256 minEthOut) external nonReentrant {
if (tokenAmount == 0) revert ZeroAmount();
Curve storage c = _requireLiveCurve(token);
uint256 vEth = c.virtualEth;
uint256 vTok = c.virtualTokens;
uint256 ethOut = vEth - Math.ceilDiv(vEth * vTok, vTok + tokenAmount);
uint256 fee = (ethOut * c.tradeFeeBps) / BPS;
uint256 ethToSeller = ethOut - fee;
if (ethToSeller < minEthOut || ethToSeller == 0) revert SlippageExceeded();
c.virtualEth = uint128(vEth - ethOut);
c.virtualTokens = uint128(vTok + tokenAmount);
c.realEth = uint128(c.realEth - ethOut);
c.realTokens = uint128(c.realTokens + tokenAmount);
_accrueFee(token, fee);
emit Trade(token, msg.sender, false, ethToSeller, tokenAmount, fee, c.virtualEth, c.virtualTokens);
require(HoodToken(token).transferFrom(msg.sender, address(this), tokenAmount), "TRANSFER_FAILED");
(bool ok,) = msg.sender.call{value: ethToSeller}("");
if (!ok) revert EthTransferFailed();
}
function _buy(address token, uint256 ethIn, uint256 minTokensOut, Config memory cfg, bool exemptGuard) internal {
Curve storage c = curves[token];
uint256 feeBps = c.tradeFeeBps;
uint256 vEth = c.virtualEth;
uint256 vTok = c.virtualTokens;
uint256 realTokens = c.realTokens;
uint256 fee = (ethIn * feeBps) / BPS;
uint256 ethForCurve = ethIn - fee;
uint256 tokensOut = vTok - (vEth * vTok) / (vEth + ethForCurve);
uint256 refund = 0;
if (tokensOut >= realTokens) {
// Final buy: clamp to remaining supply, charge exactly what it
// costs (fee grossed up), refund the rest.
tokensOut = realTokens;
uint256 newVTok = vTok - realTokens;
uint256 exactEth = Math.ceilDiv(vEth * vTok, newVTok) - vEth;
// Ceil rounding can land 1 wei above what was sent; never charge
// more than msg.value.
if (exactEth > ethForCurve) exactEth = ethForCurve;
uint256 grossEth = feeBps == 0 ? exactEth : Math.ceilDiv(exactEth * BPS, BPS - feeBps);
if (grossEth > ethIn) grossEth = ethIn;
refund = ethIn - grossEth;
fee = grossEth - exactEth;
ethForCurve = exactEth;
}
if (tokensOut < minTokensOut || tokensOut == 0) revert SlippageExceeded();
if (!exemptGuard && block.number < c.createdAtBlock + cfg.guardBlocks && cfg.guardMaxWalletBps != 0) {
uint256 bought = guardBought[token][msg.sender] + tokensOut;
if (bought > (CURVE_SUPPLY * cfg.guardMaxWalletBps) / BPS) revert SnipeGuardExceeded();
guardBought[token][msg.sender] = bought;
}
c.virtualEth = uint128(vEth + ethForCurve);
c.virtualTokens = uint128(vTok - tokensOut);
c.realEth = uint128(c.realEth + ethForCurve);
c.realTokens = uint128(realTokens - tokensOut);
_accrueFee(token, fee);
emit Trade(token, msg.sender, true, ethForCurve + fee, tokensOut, fee, c.virtualEth, c.virtualTokens);
if (c.realTokens == 0) {
c.graduated = true;
emit Graduated(token, c.realEth);
}
require(HoodToken(token).transfer(msg.sender, tokensOut), "TRANSFER_FAILED");
if (refund > 0) {
(bool ok,) = msg.sender.call{value: refund}("");
if (!ok) revert EthTransferFailed();
}
}
// -------------------------------------------------------------- migrate
/// @notice Move a graduated curve's liquidity to Uniswap v2 and burn the
/// LP tokens. Permissionless: anyone may call.
function migrate(address token) external nonReentrant returns (address pair) {
Curve storage c = curves[token];
if (c.creator == address(0)) revert UnknownToken();
if (!c.graduated) revert NotGraduated();
if (c.migrated) revert AlreadyMigrated();
c.migrated = true;
uint256 ethLiq = c.realEth;
uint256 migrationFee = config.migrationFee + (ethLiq * config.migrationFeeBps) / BPS;
if (migrationFee < ethLiq) {
ethLiq -= migrationFee;
// creator gets their share of the migration fee, same as trade fees
_accrueFee(token, migrationFee);
}
c.realEth = 0;
HoodToken(token).unlock();
pair = uniswapFactory.getPair(token, address(weth));
if (pair == address(0)) {
pair = uniswapFactory.createPair(token, address(weth));
}
weth.deposit{value: ethLiq}();
require(weth.transfer(pair, ethLiq), "TRANSFER_FAILED");
require(HoodToken(token).transfer(pair, LP_SUPPLY), "TRANSFER_FAILED");
uint256 liquidity = IUniswapV2Pair(pair).mint(address(this));
require(IUniswapV2Pair(pair).transfer(LP_BURN_ADDRESS, liquidity), "TRANSFER_FAILED");
emit Migrated(token, pair, ethLiq, LP_SUPPLY, liquidity);
}
// ---------------------------------------------------------------- views
function quoteBuy(address token, uint256 ethIn)
external
view
returns (uint256 tokensOut, uint256 fee, uint256 ethUsed)
{
Curve memory c = curves[token];
if (c.creator == address(0)) revert UnknownToken();
if (c.graduated || ethIn == 0) return (0, 0, 0);
uint256 feeBps = c.tradeFeeBps;
fee = (ethIn * feeBps) / BPS;
uint256 ethForCurve = ethIn - fee;
tokensOut = c.virtualTokens - (uint256(c.virtualEth) * c.virtualTokens) / (c.virtualEth + ethForCurve);
ethUsed = ethIn;
if (tokensOut >= c.realTokens) {
tokensOut = c.realTokens;
uint256 newVTok = c.virtualTokens - c.realTokens;
uint256 exactEth = Math.ceilDiv(uint256(c.virtualEth) * c.virtualTokens, newVTok) - c.virtualEth;
if (exactEth > ethForCurve) exactEth = ethForCurve;
ethUsed = feeBps == 0 ? exactEth : Math.ceilDiv(exactEth * BPS, BPS - feeBps);
if (ethUsed > ethIn) ethUsed = ethIn;
fee = ethUsed - exactEth;
}
}
function quoteSell(address token, uint256 tokenAmount) external view returns (uint256 ethOut, uint256 fee) {
Curve memory c = curves[token];
if (c.creator == address(0)) revert UnknownToken();
if (c.graduated || tokenAmount == 0) return (0, 0);
uint256 gross =
c.virtualEth - Math.ceilDiv(uint256(c.virtualEth) * c.virtualTokens, c.virtualTokens + tokenAmount);
fee = (gross * c.tradeFeeBps) / BPS;
ethOut = gross - fee;
}
/// @notice Spot price in ETH wei per whole token (1e18 base units).
function currentPrice(address token) external view returns (uint256) {
Curve memory c = curves[token];
if (c.creator == address(0)) revert UnknownToken();
return (uint256(c.virtualEth) * 1e18) / c.virtualTokens;
}
/// @notice Curve progress toward graduation in basis points.
function progressBps(address token) external view returns (uint256) {
Curve memory c = curves[token];
if (c.creator == address(0)) revert UnknownToken();
return ((CURVE_SUPPLY - c.realTokens) * BPS) / CURVE_SUPPLY;
}
function tokenCount() external view returns (uint256) {
return allTokens.length;
}
function getCurve(address token) external view returns (Curve memory) {
return curves[token];
}
/// @notice Provenance check for integrators and indexers: true iff the
/// token was deployed by this launchpad.
function isHoodToken(address token) external view returns (bool) {
return curves[token].creator != address(0);
}
/// @notice Predicted CREATE2 address for a launch. Grind `salt` until the
/// result ends in 0x600d (when vanity enforcement is on).
function predictTokenAddress(address creator, bytes32 salt, string calldata name, string calldata symbol)
external
view
returns (address)
{
bytes32 initCodeHash =
keccak256(abi.encodePacked(type(HoodToken).creationCode, abi.encode(name, symbol, TOTAL_SUPPLY)));
bytes32 h = keccak256(
abi.encodePacked(bytes1(0xff), address(this), keccak256(abi.encodePacked(creator, salt)), initCodeHash)
);
return address(uint160(uint256(h)));
}
/// @notice Init code hash for client-side salt grinding (one RPC call,
/// then grind locally with keccak(0xff ++ launchpad ++ mixedSalt ++ hash)).
function tokenInitCodeHash(string calldata name, string calldata symbol) external pure returns (bytes32) {
return keccak256(abi.encodePacked(type(HoodToken).creationCode, abi.encode(name, symbol, TOTAL_SUPPLY)));
}
/// @notice Paginated token listing for indexers (newest tokens last).
function tokens(uint256 offset, uint256 limit) external view returns (address[] memory page) {
uint256 n = allTokens.length;
if (offset >= n) return new address[](0);
uint256 end = offset + limit;
if (end > n) end = n;
page = new address[](end - offset);
for (uint256 i = offset; i < end; i++) {
page[i - offset] = allTokens[i];
}
}
// -------------------------------------------------------------- helpers
function _requireLiveCurve(address token) internal view returns (Curve storage c) {
c = curves[token];
if (c.creator == address(0)) revert UnknownToken();
if (c.graduated) revert AlreadyGraduated();
}
}
library Math {
function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
return a == 0 ? 0 : (a - 1) / b + 1;
}
}