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
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