Contract

The full source of the smart contract that runs Grow, published directly here — not just linked off to an explorer.

LaunchPad · ARC Mainnet
0x23f64Be120Ec630086134f5c92399E2a16B0f40E
View on Explorer

What's guaranteed on-chain

1.00%
Total fee per trade, split 0.70% creator / 0.30% platform. Hard-coded as immutable constants — not adjustable by anyone, ever.
0%
Team or investor token allocation. Every token, including any future GROW token, sells through the same public bonding curve as everyone else.
$0
Presale. The bonding curve is the only way to acquire tokens before they graduate to a public DEX pool.
No path
for the contract owner to withdraw user funds. There is no function anywhere in this contract that lets the owner pull the USDC reserves backing live tokens.
What the owner actually can do: change where the platform's 0.3% fee cut is sent (setFeeRecipient), and — only until explicitly locked — point new token graduations at a specific DEX position manager address (setPositionManager / lockPositionManager), which exists solely because ARC's DEX infrastructure was still finalizing at launch. Once locked, that address can never change again. Neither function can move a single token or USDC unit out of any user's position.
Honest limitation: this contract has not yet undergone a third-party security audit. Read the source yourself below, verify it against the address on explorer.arc.io, and only risk what you can afford to lose.

LaunchPad.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./Token.sol";
import "./interfaces/IUniswapV3.sol";

/**
 * @title StablePump LaunchPad v2
 * @notice pump.fun 機制移植到 Stable Chain
 *         - 用 USDT0 買賣代幣(Bonding Curve)
 *         - 代幣賣完自動畢業注入 Uniswap v3(對齊 pump.fun)
 *         - 每筆交易 1% 手續費:0.3% 給 creator,0.7% 給平台
 *         - 支援社群連結(Twitter / Telegram / Website)
 */
contract LaunchPad is ReentrancyGuard, Ownable {
    using SafeERC20 for IERC20;

    // ── 常數 ────────────────────────────────────────────────────────
    uint256 public constant TOTAL_SUPPLY        = 1_000_000_000e18;
    uint256 public constant SALE_SUPPLY         = 800_000_000e18;
    uint256 public constant LP_SUPPLY           = 200_000_000e18;

    // pump.fun: 30 SOL virtual / 1.073B tokens → 初始市值 ≈ $4,200
    // 我們:  5,000 USDT virtual / 1.073B tokens → 初始市值 ≈ $4,660
    uint256 public constant VIRTUAL_USDT        = 5_000e6;
    uint256 public constant VIRTUAL_TOKENS      = 1_073_000_191e18;

    // 手續費:總 1%,拆分 creator 0.7% + 平台 0.3%(對齊市場標準)
    uint256 public constant TOTAL_FEE_BPS       = 100;   // 1.00%
    uint256 public constant CREATOR_FEE_BPS     = 70;    // 0.70%
    uint256 public constant PLATFORM_FEE_BPS    = 30;    // 0.30%
    uint256 public constant FEE_DENOMINATOR     = 10_000;

    // 創建代幣固定費用:5 USDC(6 decimals)
    uint256 public constant CREATE_FEE          = 5e6;   // 5 USDC

    uint24  public constant UNISWAP_FEE         = 3000;

    // ── 狀態變數 ────────────────────────────────────────────────────
    IERC20  public immutable usdt0;

    // 2026-09-16:拿掉 immutable,改成 owner 可設定 + 可鎖定,原因見下方
    // setPositionManager/lockPositionManager 的註解。createToken/buy/sell 完全
    // 不會用到這個變數,只有 _graduate() 才會呼叫,所以部署時先填一個佔位
    // 地址也不影響日常發幣/交易,等真正要用的 DEX 地址確定後再設定+鎖定。
    INonfungiblePositionManager public positionManager;
    bool public positionManagerLocked;

    address public feeRecipient;

    struct TokenInfo {
        address tokenAddress;
        address creator;
        uint256 realUSDT;
        uint256 realTokensSold;
        bool    graduated;
        uint256 createdAt;
        string  twitter;
        string  telegram;
        string  website;
    }

    mapping(address => TokenInfo) public tokens;
    address[] public allTokens;

    // ── 事件 ────────────────────────────────────────────────────────
    event TokenCreated(
        address indexed tokenAddress,
        address indexed creator,
        string  name,
        string  symbol,
        string  imageURI,
        string  description,
        string  twitter,
        string  telegram,
        string  website,
        uint256 timestamp
    );

    event Trade(
        address indexed tokenAddress,
        address indexed trader,
        bool    isBuy,
        uint256 usdtAmount,
        uint256 tokenAmount,
        uint256 price,
        uint256 marketCap,
        uint256 timestamp
    );

    event Graduated(
        address indexed tokenAddress,
        uint256 usdtLiquidity,
        uint256 tokenLiquidity,
        address pool,
        uint256 timestamp
    );

    event PositionManagerUpdated(address indexed newPositionManager);
    event PositionManagerLocked();

    // ── 建構子 ──────────────────────────────────────────────────────
    constructor(
        address _usdt0,
        address _positionManager,
        address _feeRecipient
    ) Ownable(msg.sender) {
        usdt0           = IERC20(_usdt0);
        positionManager = INonfungiblePositionManager(_positionManager);
        feeRecipient    = _feeRecipient;
    }

    // ── 發幣 ────────────────────────────────────────────────────────
    function createToken(
        string calldata name,
        string calldata symbol,
        string calldata imageURI,
        string calldata description,
        string calldata twitter,
        string calldata telegram,
        string calldata website
    ) external nonReentrant returns (address tokenAddr) {
        // 收取 5 USDC 創建費
        usdt0.safeTransferFrom(msg.sender, feeRecipient, CREATE_FEE);

        Token token = new Token(
            name,
            symbol,
            imageURI,
            description,
            msg.sender,
            TOTAL_SUPPLY
        );
        tokenAddr = address(token);

        tokens[tokenAddr] = TokenInfo({
            tokenAddress:   tokenAddr,
            creator:        msg.sender,
            realUSDT:       0,
            realTokensSold: 0,
            graduated:      false,
            createdAt:      block.timestamp,
            twitter:        twitter,
            telegram:       telegram,
            website:        website
        });
        allTokens.push(tokenAddr);

        emit TokenCreated(
            tokenAddr, msg.sender, name, symbol, imageURI, description,
            twitter, telegram, website, block.timestamp
        );
    }

    // ── 買入 ────────────────────────────────────────────────────────
    function buy(
        address tokenAddr,
        uint256 usdtIn,
        uint256 minTokens
    ) external nonReentrant {
        TokenInfo storage info = tokens[tokenAddr];
        require(info.tokenAddress != address(0), "Token not found");
        require(!info.graduated, "Token graduated");
        require(usdtIn > 0, "USDT must > 0");

        uint256 totalFee    = (usdtIn * TOTAL_FEE_BPS)    / FEE_DENOMINATOR;
        uint256 creatorFee  = (usdtIn * CREATOR_FEE_BPS)  / FEE_DENOMINATOR;
        uint256 platformFee = (usdtIn * PLATFORM_FEE_BPS) / FEE_DENOMINATOR;
        uint256 usdtNet     = usdtIn - totalFee;

        uint256 tokensOut = _calcBuy(info.realUSDT, info.realTokensSold, usdtNet);
        require(tokensOut >= minTokens, "Slippage too high");
        require(info.realTokensSold + tokensOut <= SALE_SUPPLY, "Exceeds sale supply");

        usdt0.safeTransferFrom(msg.sender, address(this), usdtIn);
        if (creatorFee > 0)  usdt0.safeTransfer(info.creator,  creatorFee);
        if (platformFee > 0) usdt0.safeTransfer(feeRecipient,  platformFee);

        info.realUSDT       += usdtNet;
        info.realTokensSold += tokensOut;

        IERC20(tokenAddr).safeTransfer(msg.sender, tokensOut);

        uint256 price     = _currentPrice(info.realUSDT, info.realTokensSold);
        uint256 marketCap = (price * TOTAL_SUPPLY) / 1e18;

        emit Trade(tokenAddr, msg.sender, true, usdtIn, tokensOut, price, marketCap, block.timestamp);

        if (info.realTokensSold >= SALE_SUPPLY) {
            _graduate(tokenAddr);
        }
    }

    // ── 賣出 ────────────────────────────────────────────────────────
    function sell(
        address tokenAddr,
        uint256 tokensIn,
        uint256 minUSDT
    ) external nonReentrant {
        TokenInfo storage info = tokens[tokenAddr];
        require(info.tokenAddress != address(0), "Token not found");
        require(!info.graduated, "Token graduated");
        require(tokensIn > 0, "Tokens must > 0");
        require(info.realTokensSold >= tokensIn, "Exceeds sold supply");

        uint256 usdtOut = _calcSell(info.realUSDT, info.realTokensSold, tokensIn);
        if (usdtOut > info.realUSDT) usdtOut = info.realUSDT;

        uint256 creatorFee  = (usdtOut * CREATOR_FEE_BPS)  / FEE_DENOMINATOR;
        uint256 platformFee = (usdtOut * PLATFORM_FEE_BPS) / FEE_DENOMINATOR;
        uint256 totalFee    = creatorFee + platformFee;
        uint256 usdtNet     = usdtOut - totalFee;
        require(usdtNet >= minUSDT, "Slippage too high");

        IERC20(tokenAddr).safeTransferFrom(msg.sender, address(this), tokensIn);

        info.realUSDT       -= usdtOut;
        info.realTokensSold -= tokensIn;

        if (creatorFee > 0)  usdt0.safeTransfer(info.creator,  creatorFee);
        if (platformFee > 0) usdt0.safeTransfer(feeRecipient,  platformFee);
        usdt0.safeTransfer(msg.sender, usdtNet);

        uint256 price     = _currentPrice(info.realUSDT, info.realTokensSold);
        uint256 marketCap = (price * TOTAL_SUPPLY) / 1e18;

        emit Trade(tokenAddr, msg.sender, false, usdtOut, tokensIn, price, marketCap, block.timestamp);
    }

    // ── 畢業機制 ────────────────────────────────────────────────────
    function _graduate(address tokenAddr) internal {
        TokenInfo storage info = tokens[tokenAddr];
        info.graduated = true;

        uint256 usdtForLP   = info.realUSDT;
        uint256 tokensForLP = LP_SUPPLY;

        usdt0.approve(address(positionManager), usdtForLP);
        IERC20(tokenAddr).approve(address(positionManager), tokensForLP);

        address token0 = address(usdt0) < tokenAddr ? address(usdt0) : tokenAddr;
        address token1 = address(usdt0) < tokenAddr ? tokenAddr : address(usdt0);
        uint256 amount0 = token0 == address(usdt0) ? usdtForLP  : tokensForLP;
        uint256 amount1 = token0 == address(usdt0) ? tokensForLP : usdtForLP;

        uint160 sqrtPriceX96 = _calcSqrtPriceX96(token0, token1, usdtForLP, tokensForLP);

        address pool = positionManager.createAndInitializePoolIfNecessary(
            token0, token1, UNISWAP_FEE, sqrtPriceX96
        );

        positionManager.mint(
            INonfungiblePositionManager.MintParams({
                token0:         token0,
                token1:         token1,
                fee:            UNISWAP_FEE,
                tickLower:      -887220,
                tickUpper:       887220,
                amount0Desired: amount0,
                amount1Desired: amount1,
                amount0Min:     0,
                amount1Min:     0,
                recipient:      address(this),
                deadline:       block.timestamp + 300
            })
        );

        emit Graduated(tokenAddr, usdtForLP, tokensForLP, pool, block.timestamp);
    }

    // ── 數學函數 ────────────────────────────────────────────────────
    function _calcBuy(uint256 realUSDT, uint256 realTokensSold, uint256 usdtIn)
        internal pure returns (uint256 tokensOut)
    {
        uint256 poolUSDT      = VIRTUAL_USDT + realUSDT;
        uint256 poolTokens    = VIRTUAL_TOKENS - realTokensSold;
        uint256 k             = poolUSDT * poolTokens;
        uint256 newPoolUSDT   = poolUSDT + usdtIn;
        uint256 newPoolTokens = k / newPoolUSDT;
        tokensOut = poolTokens - newPoolTokens;
    }

    function _calcSell(uint256 realUSDT, uint256 realTokensSold, uint256 tokensIn)
        internal pure returns (uint256 usdtOut)
    {
        uint256 poolUSDT      = VIRTUAL_USDT + realUSDT;
        uint256 poolTokens    = VIRTUAL_TOKENS - realTokensSold;
        uint256 k             = poolUSDT * poolTokens;
        uint256 newPoolTokens = poolTokens + tokensIn;
        uint256 newPoolUSDT   = k / newPoolTokens;
        usdtOut = poolUSDT - newPoolUSDT;
    }

    function _currentPrice(uint256 realUSDT, uint256 realTokensSold)
        internal pure returns (uint256)
    {
        uint256 poolUSDT   = VIRTUAL_USDT + realUSDT;
        uint256 poolTokens = VIRTUAL_TOKENS - realTokensSold;
        return (poolUSDT * 1e18) / poolTokens;
    }

    function _calcSqrtPriceX96(
        address token0,
        address /* token1 */,
        uint256 usdtAmount,
        uint256 tokenAmount
    ) internal view returns (uint160) {
        uint256 price;
        if (token0 == address(usdt0)) {
            price = (tokenAmount * 1e6) / usdtAmount;
        } else {
            price = (usdtAmount * 1e18) / tokenAmount;
        }
        uint256 sqrtPrice = _sqrt(price) * (2**96) / 1e9;
        return uint160(sqrtPrice);
    }

    function _sqrt(uint256 x) internal pure returns (uint256 y) {
        if (x == 0) return 0;
        uint256 z = (x + 1) / 2;
        y = x;
        while (z < y) { y = z; z = (x / z + z) / 2; }
    }

    // ── 查詢函數 ────────────────────────────────────────────────────
    function getTokenCount() external view returns (uint256) {
        return allTokens.length;
    }

    function getTokens(uint256 offset, uint256 limit)
        external view returns (address[] memory)
    {
        uint256 end = offset + limit > allTokens.length ? allTokens.length : offset + limit;
        address[] memory result = new address[](end - offset);
        for (uint256 i = offset; i < end; i++) result[i - offset] = allTokens[i];
        return result;
    }

    function getTokenInfo(address tokenAddr)
        external view returns (
            address creator,
            uint256 realUSDT,
            uint256 realTokensSold,
            bool    graduated,
            uint256 createdAt,
            string memory twitter,
            string memory telegram,
            string memory website
        )
    {
        TokenInfo storage info = tokens[tokenAddr];
        return (
            info.creator, info.realUSDT, info.realTokensSold,
            info.graduated, info.createdAt,
            info.twitter, info.telegram, info.website
        );
    }

    function getBuyQuote(address tokenAddr, uint256 usdtIn)
        external view returns (uint256 tokensOut, uint256 fee)
    {
        TokenInfo storage info = tokens[tokenAddr];
        fee      = (usdtIn * TOTAL_FEE_BPS) / FEE_DENOMINATOR;
        tokensOut = _calcBuy(info.realUSDT, info.realTokensSold, usdtIn - fee);
    }

    function getSellQuote(address tokenAddr, uint256 tokensIn)
        external view returns (uint256 usdtOut, uint256 fee)
    {
        TokenInfo storage info = tokens[tokenAddr];
        uint256 gross = _calcSell(info.realUSDT, info.realTokensSold, tokensIn);
        fee     = (gross * TOTAL_FEE_BPS) / FEE_DENOMINATOR;
        usdtOut = gross - fee;
    }

    function getCurrentPrice(address tokenAddr) external view returns (uint256) {
        TokenInfo storage info = tokens[tokenAddr];
        return _currentPrice(info.realUSDT, info.realTokensSold);
    }

    function getMarketCap(address tokenAddr) external view returns (uint256) {
        uint256 price = this.getCurrentPrice(tokenAddr);
        return (price * TOTAL_SUPPLY) / 1e18;
    }

    function getProgress(address tokenAddr) external view returns (uint256 pct) {
        TokenInfo storage info = tokens[tokenAddr];
        pct = (info.realTokensSold * 100) / SALE_SUPPLY;
        if (pct > 100) pct = 100;
    }

    // ── 管理員函數 ──────────────────────────────────────────────────
    function setFeeRecipient(address _feeRecipient) external onlyOwner {
        feeRecipient = _feeRecipient;
    }

    /**
     * @notice 更新畢業時要接的 DEX Position Manager 地址。
     * @dev 只在還沒鎖定前可以呼叫。這個彈性是刻意留給「還沒有代幣真的畢業前,
     *      可以先用佔位地址上線,等正式 DEX 地址確定後再設定」這個情境用的——
     *      不是要長期保留可修改的權力。真正的地址設定好、驗證過運作正常後,
     *      應該立刻呼叫 lockPositionManager() 鎖死,把安全性恢復到跟原本
     *      immutable 一樣的保障,不要讓這個可修改狀態長期存在。
     */
    function setPositionManager(address _positionManager) external onlyOwner {
        require(!positionManagerLocked, "LaunchPad: position manager locked");
        positionManager = INonfungiblePositionManager(_positionManager);
        emit PositionManagerUpdated(_positionManager);
    }

    /// @notice 永久鎖定 positionManager,之後任何人(包含 owner)都無法再修改。
    function lockPositionManager() external onlyOwner {
        positionManagerLocked = true;
        emit PositionManagerLocked();
    }

    // 2026-09-17:移除原本的 withdrawStuckTokens(address,uint256)。
    // 這個函式沒有限制能提領哪個 token,而合約在代幣畢業前,本來就會把每筆
    // 交易扣除手續費後的真實 USDC 儲備金留在合約自己身上(見 buy()/sell() 的
    // info.realUSDT)——owner 呼叫這個函式等於能一次抽走所有未畢業代幣的儲備
    // 金,是能被拿來 rug pull 的後門,跟「零後門、資金安全寫死在合約裡」的
    // 定位互相矛盾,直接拿掉,不留任何 owner 可提領使用者資金的路徑。
}