LaunchpadFactory
0x7bd11796f1d9710e9e7a4dbc476a3a0f8c262dcc
Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
11,234 bytes
ABI entries
33
14 read · 5 write
License
none
Contract information
- Address
- 0x7bd11796f1d9710e9e7a4dbc476a3a0f8c262dcc
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.28+commit.7893614a
- Optimization
- Enabled
- Creator
- 0x04a081D955…ABfE6Fcd17
- Creation tx
- 0x7cb06aeaba…6afec4db0a
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (14)
BONDING_CURVE_SUPPLY() → uint256
CREATION_FEE() → uint256
DEX_RESERVE_SUPPLY() → uint256
TOTAL_SUPPLY() → uint256
allTokens(uint256) → address
allTokensLength() → uint256
bondingCurveOf(address) → address
creatorOf(address) → address
feeRecipient() → address
feeWalletOf(address) → address
imageUriOf(address) → string
isThorToken(address) → bool
migrationManager() → address
pendingFeeWalletOf(address) → address
Events (4)
DexReserveReleasedFeeWalletProposedFeeWalletTransferredTokenCreated
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "_migrationManager",
"type": "address"
},
{
"internalType": "address",
"name": "_feeRecipient",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AlreadyGraduated",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientCreationFee",
"type": "error"
},
{
"inputs": [],
"name": "NotFeeWallet",
"type": "error"
},
{
"inputs": [],
"name": "NotMigrationManager",
"type": "error"
},
{
"inputs": [],
"name": "NotPendingFeeWallet",
"type": "error"
},
{
"inputs": [],
"name": "Reentrancy",
"type": "error"
},
{
"inputs": [],
"name": "ReserveAlreadyReleased",
"type": "error"
},
{
"inputs": [],
"name": "UnknownToken",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "DexReserveReleased",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "currentWallet",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "proposedWallet",
"type": "address"
}
],
"name": "FeeWalletProposed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "previousWallet",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newWallet",
"type": "address"
}
],
"name": "FeeWalletTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "bondingCurve",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "feeWallet",
"type": "address"
},
{
"indexed": false,
"internalType": "string",
"name": "name",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "imageUri",
"type": "string"
}
],
"name": "TokenCreated",
"type": "event"
},
{
"inputs": [],
"name": "BONDING_CURVE_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "CREATION_FEE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "DEX_RESERVE_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TOTAL_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "acceptFeeWallet",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "allTokens",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "allTokensLength",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "bondingCurveOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "imageUri",
"type": "string"
},
{
"internalType": "address",
"name": "feeWallet",
"type": "address"
}
],
"name": "createToken",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "curve",
"type": "address"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "imageUri",
"type": "string"
}
],
"name": "createToken",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "curve",
"type": "address"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "creatorOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "feeRecipient",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "feeWalletOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "imageUriOf",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "isThorToken",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "migrationManager",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "pendingFeeWalletOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "newWallet",
"type": "address"
}
],
"name": "proposeFeeWallet",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "releaseDexReserve",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.28;
import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol";
import { ReentrancyGuard } from "solady/utils/ReentrancyGuard.sol";
import { ILaunchpadFactory } from "../interfaces/ILaunchpadFactory.sol";
import { LaunchToken } from "./LaunchToken.sol";
import { BondingCurve } from "./BondingCurve.sol";
/// @title LaunchpadFactory
/// @notice User-facing entry point: pay the 0.005 ETH creation fee, get a fresh LaunchToken with
/// a freshly-bonded BondingCurve and 793.1M of the supply pre-loaded
/// into the curve. The remaining 206.9M (DEX_RESERVE_SUPPLY) sits in
/// this contract until graduation, when MigrationManager pulls it via
/// releaseDexReserve and seeds the Thor DEX pair.
///
/// @dev Curve allocation = 793.1M (was 800M); DEX reserve = 206.9M (was
/// 200M). Allocation matches pump.fun's verified on-chain triple
/// scaled to ETH so that `BONDING_CURVE_SUPPLY ==
/// BondingCurve.VIRTUAL_TOKEN_RESERVE − BondingCurve.GRADUATION_FLOOR`,
/// making graduation triggerable by token-reserve depletion. See
/// docs/adr/0001-bonding-curve-constants.md.
///
/// @dev Deployment ordering note: BondingCurve needs `migrationManager`
/// in its constructor, so this factory takes `migrationManager` as
/// immutable. To break the circular dependency with MigrationManager
/// (which needs this factory's address), production deploys should
/// use CREATE2 to predict one address before deploying the other.
contract LaunchpadFactory is ILaunchpadFactory, ReentrancyGuard {
using SafeTransferLib for address;
// ---------------- Constants (README Appendix E) ----------------
uint256 public constant override TOTAL_SUPPLY = 1_000_000_000e18;
uint256 public constant override BONDING_CURVE_SUPPLY = 793_100_000e18;
uint256 public constant override DEX_RESERVE_SUPPLY = 206_900_000e18;
uint256 public constant override CREATION_FEE = 0.005 ether;
// ---------------- Immutables ----------------
address public immutable override migrationManager;
address public immutable override feeRecipient;
// ---------------- Storage ----------------
struct TokenRecord {
address creator;
address bondingCurve;
uint64 createdAt;
string imageUri;
}
mapping(address => TokenRecord) private _records;
mapping(address => bool) public override isThorToken;
mapping(address => bool) private _reserveReleased;
address[] public override allTokens;
/// @dev Transferable fee-wallet role per token (defaults to the creator).
/// Snapshotted into the lock vault at graduation; frozen here afterwards.
mapping(address => address) private _feeWallets;
/// @inheritdoc ILaunchpadFactory
mapping(address => address) public override pendingFeeWalletOf;
// ---------------- Errors ----------------
error ZeroAddress();
error InsufficientCreationFee();
error UnknownToken();
error ReserveAlreadyReleased();
error NotMigrationManager();
error NotFeeWallet();
error NotPendingFeeWallet();
error AlreadyGraduated();
// ---------------- Construction ----------------
constructor(
address _migrationManager,
address _feeRecipient
) {
if (_migrationManager == address(0) || _feeRecipient == address(0)) {
revert ZeroAddress();
}
migrationManager = _migrationManager;
feeRecipient = _feeRecipient;
}
// ---------------- Views ----------------
/// @inheritdoc ILaunchpadFactory
function bondingCurveOf(
address token
) external view override returns (address) {
return _records[token].bondingCurve;
}
/// @inheritdoc ILaunchpadFactory
function creatorOf(
address token
) external view override returns (address) {
return _records[token].creator;
}
/// @inheritdoc ILaunchpadFactory
function feeWalletOf(
address token
) external view override returns (address) {
return _feeWallets[token];
}
/// @inheritdoc ILaunchpadFactory
function imageUriOf(
address token
) external view override returns (string memory) {
return _records[token].imageUri;
}
/// @inheritdoc ILaunchpadFactory
function allTokensLength() external view override returns (uint256) {
return allTokens.length;
}
// ---------------- createToken ----------------
/// @inheritdoc ILaunchpadFactory
function createToken(
string calldata name,
string calldata symbol,
string calldata imageUri
) external payable override nonReentrant returns (address token, address curve) {
return _createToken(name, symbol, imageUri, address(0));
}
/// @inheritdoc ILaunchpadFactory
function createToken(
string calldata name,
string calldata symbol,
string calldata imageUri,
address feeWallet
) external payable override nonReentrant returns (address token, address curve) {
return _createToken(name, symbol, imageUri, feeWallet);
}
/// @dev Shared body of both `createToken` overloads. `feeWallet ==
/// address(0)` assigns the fee-wallet role to the creator.
function _createToken(
string calldata name,
string calldata symbol,
string calldata imageUri,
address feeWallet
) private returns (address token, address curve) {
if (msg.value < CREATION_FEE) revert InsufficientCreationFee();
// Deploy LaunchToken — receives full TOTAL_SUPPLY into this factory.
token = address(new LaunchToken(name, symbol));
// Deploy BondingCurve referencing the new token.
curve = address(new BondingCurve(token, migrationManager, feeRecipient));
// Allocate: 793.1M to the curve, 206.9M retained here (released on migration).
token.safeTransfer(curve, BONDING_CURVE_SUPPLY);
// Register.
_records[token] = TokenRecord({
creator: msg.sender, bondingCurve: curve, createdAt: uint64(block.timestamp), imageUri: imageUri
});
if (feeWallet == address(0)) feeWallet = msg.sender;
_feeWallets[token] = feeWallet;
isThorToken[token] = true;
allTokens.push(token);
// Forward creation fee + refund any overpayment.
feeRecipient.safeTransferETH(CREATION_FEE);
if (msg.value > CREATION_FEE) {
msg.sender.safeTransferETH(msg.value - CREATION_FEE);
}
emit TokenCreated(token, msg.sender, curve, feeWallet, name, symbol, imageUri);
}
// ---------------- Fee-wallet handoff (two-step, pre-graduation) ----------------
/// @inheritdoc ILaunchpadFactory
function proposeFeeWallet(
address token,
address newWallet
) external override nonReentrant {
_checkTransferable(token);
if (msg.sender != _feeWallets[token]) revert NotFeeWallet();
pendingFeeWalletOf[token] = newWallet;
emit FeeWalletProposed(token, msg.sender, newWallet);
}
/// @inheritdoc ILaunchpadFactory
function acceptFeeWallet(
address token
) external override nonReentrant {
_checkTransferable(token);
// msg.sender can never be address(0), so an empty pending slot can
// never be "accepted" — no explicit zero check needed.
if (msg.sender != pendingFeeWalletOf[token]) revert NotPendingFeeWallet();
address previous = _feeWallets[token];
_feeWallets[token] = msg.sender;
delete pendingFeeWalletOf[token];
emit FeeWalletTransferred(token, previous, msg.sender);
}
/// @dev Handoffs live here only while the curve is live. After graduation
/// the lock vault's registry is authoritative (the factory copy is
/// frozen as a historical snapshot) — any in-flight proposal lapses.
function _checkTransferable(
address token
) private view {
TokenRecord storage record = _records[token];
if (record.bondingCurve == address(0)) revert UnknownToken();
if (BondingCurve(record.bondingCurve).graduated()) revert AlreadyGraduated();
}
// ---------------- releaseDexReserve ----------------
/// @inheritdoc ILaunchpadFactory
function releaseDexReserve(
address token
) external override {
if (msg.sender != migrationManager) revert NotMigrationManager();
if (!isThorToken[token]) revert UnknownToken();
if (_reserveReleased[token]) revert ReserveAlreadyReleased();
_reserveReleased[token] = true;
token.safeTransfer(migrationManager, DEX_RESERVE_SUPPLY);
emit DexReserveReleased(token, migrationManager, DEX_RESERVE_SUPPLY);
}
}
Chain explorer1502msChain node82ms