Pump.fun 的核心是什么?用 300 行 Solidity 实现 Bonding Curve 与自动 LP 销毁

47 阅读9分钟

一、背景:Pump.fun 为什么能成功?

传统 Memecoin 发射的问题很简单:项目方预留大量代币,上线即抛售。社区参与的是一场信息不对称的博弈。

Pump.fun(运行在 Solana 上)的创新在于用机制设计代替了"相信项目方":

  • 无预售:没有团队预留份额,所有人从同一起点买入
  • Bonding Curve 定价:价格由链上公式决定,不可操纵
  • 自动毕业:达到市值阈值后,流动性自动迁移到公开 DEX,LP token 永久销毁,任何人无法撤走流动性

本文用 Solidity + Uniswap V2 复刻这套机制。先看全貌,再逐层拆解。

二、整体架构:一条 Token 的生命线

一个 Memecoin 从诞生到"毕业",经过三个阶段:

创建 → 在 Bonding Curve 上交易 → 积累 24 ETH 后自动毕业到 Uniswap V2

系统由两个合约协作完成:

合约职责
TokenFactory创建 ERC-20 token,铸造总量 10 亿枚,将 80%(8 亿枚)注入交易池
BondingCurve管理交易池:定价、买卖、收手续费、达到阈值后自动毕业

资金流向:

用户调用 Factory.createToken()
    → 部署新 ERC-20,80% token 转入 BondingCurve 池子
    → 用户用 ETH 在池子里买卖 token,价格随买入量上涨
    → 池子积累 ≥ 24 ETH
    → 自动调用 Uniswap Router,注入流动性,销毁 LP token
    → Token 正式在 Uniswap 上自由交易

接下来的问题是:池子里没有对手方提供流动性,价格怎么定?

三、交易机制:Bonding Curve 如何定价

合约就是做市商

传统 DEX(如 Uniswap)需要双边流动性——有人提供 ETH,有人提供 Token,用恒积公式 x * y = k 定价。但在 Memecoin 冷启动场景,Token 刚创建时没有人愿意垫 ETH 流动性——项目方垫付又回到了预售问题。

这套系统选择了不同的路径:合约本身充当做市商,用一条线性公式单边定价,不需要任何外部流动性提供者。

价格公式:

price(n) = INITIAL_PRICE + SLOPE × tokensSold

对应合约常量:

uint256 public constant INITIAL_PRICE = 1e10; // 初始价格 0.00000001 ETH
uint256 public constant SLOPE = 1e4;           // 每卖出 1 token,价格上升的幅度

直觉理解:一条从左下到右上的直线。每买一批 token,当前位置往右推,下一个买家面对的价格更高。卖出则反向——价格回落。

这比恒积曲线更可预测:你可以直接计算"再买 X ETH 能得到多少 token,价格会涨到多少",而不需要模拟 AMM 的非线性变化。

买入:花 X ETH 能买多少 token?

不能直接用瞬时价格乘以数量——买入过程中价格在持续上涨。正确做法是对价格曲线求积分(计算曲线下的面积)。

从已卖出 a 个 token 开始,再买 n 个 token 的总花费:

cost = ∫[a to a+n] (INITIAL_PRICE + SLOPE × x) dx
     = INITIAL_PRICE × n + SLOPE × (2a×n + n²) / 2

推导:

F(x) = INITIAL_PRICE·x + SLOPE·x²/2

F(a+n) - F(a)
= INITIAL_PRICE·n + SLOPE·((a+n)² - a²)/2
= INITIAL_PRICE·n + SLOPE·(2a·n + n²)/2

对应合约实现:

function _calculateCost(
    uint256 currentSold,  // 当前已卖出量 a
    uint256 tokenAmount   // 想买的数量 n
) public pure returns (uint256) {
    uint256 linearCost = (INITIAL_PRICE * tokenAmount) / PRECISION;
    uint256 quadraticCost = (SLOPE *
        (2 * currentSold * tokenAmount + tokenAmount * tokenAmount)) /
        (2 * PRECISION * PRECISION);
    return linearCost + quadraticCost;
}

关于精度处理: PRECISION = 1e18,是 Solidity 用整数模拟小数的标准做法。tokenAmount 以最小单位传入(实际代币数 × 1e18),因此线性项除以一个 PRECISION,二次项(两个 tokenAmount 相乘)除以两个 PRECISION。

为什么买入要用二分查找?

用户给出的是 ETH 数量,需要反解出能买多少 token。但二次方程在整数域没有精确解——Solidity 没有浮点数,直接解方程会有截断误差。

解决方案:在 [0, 十亿 × PRECISION] 范围内二分查找"花费不超过传入 ETH 的最大 token 数量":

function _calculateBuyTokens(
    uint256 currentSold,
    uint256 ethAmount
) internal pure returns (uint256) {
    uint256 low = 0;
    uint256 high = 1_000_000_000 * PRECISION;
    uint256 mid;

    while (low < high) {
        mid = (low + high + 1) / 2;
        uint256 cost = _calculateCost(currentSold, mid);
        if (cost <= ethAmount) {
            low = mid;
        } else {
            high = mid - 1;
        }
    }
    return low;
}

卖出:直接计算退款

卖出不需要二分查找——数量已知,直接用"卖出前的累计花费 - 卖出后的累计花费"得到退款:

function _calculateSellReturn(
    uint256 currentSold,
    uint256 tokenAmount
) public pure returns (uint256) {
    require(tokenAmount <= currentSold, "Cannot sell more than sold");
    uint256 newSold = currentSold - tokenAmount;
    uint256 costAtCurrent = _calculateCost(0, currentSold);
    uint256 costAtNew = _calculateCost(0, newSold);
    return costAtCurrent - costAtNew;
}

买卖的数学对称性成立:买入价格曲线的面积等于卖出返还的面积。由于整数截断,实际存在极微小的舍入误差(dust),但不影响机制的公平性。

四、毕业机制:24 ETH 之后发生了什么

Bonding Curve 是一个"孵化池",不是终点。当池子积累足够 ETH,Token 自动"毕业"——流动性迁移到 Uniswap V2,成为正式的链上交易对。

为什么是 24 ETH?

这是一个工程参数,权衡两件事:太低则流动性不够深、毕业后容易被大单冲击;太高则大多数 Memecoin 永远毕业不了。24 ETH 参考了 Pump.fun 在 Solana 上验证过的市值锚定思路。

触发条件

买入时检查:

if (pool.ethBalance >= GRADUATION_THRESHOLD) {
    _graduate(token);
}

毕业流程

_graduate 做了三件事——标记状态、清空池子、注入 Uniswap:

function _graduate(address token) internal {
    Pool storage pool = pools[token];
    pool.graduated = true;  // 1. 标记已毕业,阻止后续交易

    uint256 ethForLiquidity = pool.ethBalance;
    uint256 tokensForLiquidity = pool.tokenBalance;

    pool.ethBalance = 0;    // 2. 清空状态(先写状态,防重入)
    pool.tokenBalance = 0;

    IERC20(token).approve(address(uniswapRouter), tokensForLiquidity);

    uniswapRouter.addLiquidityETH{value: ethForLiquidity}(  // 3. 注入 Uniswap
        token,
        tokensForLiquidity,
        0,              // 接受任意数量的 token
        0,              // 接受任意数量的 ETH
        address(0),     // LP token 发给零地址 = 销毁
        block.timestamp + 300
    );
}

LP 发给 address(0) 是什么意思?

LP token 是注入流动性后拿到的凭证——凭它可以随时按比例赎回池中的 ETH 和 Token。发给零地址意味着没有任何人能取走这些 LP token,等于销毁了赎回流动性的唯一钥匙。

这是机制设计的核心承诺:流动性被永久锁死,没有人能撤走,包括合约开发者。Rug pull 在机制层面被消除,而不是依赖项目方的道德自律。

五、架构设计:为什么拆成两个合约

最直接的实现是把所有逻辑塞进一个合约。但这个项目把"创建"和"交易"分离到了 TokenFactoryBondingCurve 两个合约。

拆分的好处:

  • BondingCurve 可以独立迭代(比如换一种曲线),不影响 TokenFactory 的部署
  • 每个合约更小,更容易审计
  • 权限边界清晰:只有 Factory 能创建池子

权限守卫

onlyFactory modifier 确保只有 Factory 能在 BondingCurve 上注册新池子:

modifier onlyFactory() {
    require(msg.sender == factory, "Only factory");
    _;
}

function createPool(
    address token,
    address creator,
    uint256 tokenAmount
) external onlyFactory { ... }

没有这个限制,任意外部账户都可以调用 createPool,注入伪造的池子——比如伪装成已有 token 的池子覆盖状态,或注册恶意 token 合约。

一次性绑定

function setFactory(address _factory) external onlyOwner {
    require(factory == address(0), "Factory already set");
    factory = _factory;
}

factory 只能写入一次。部署顺序是:先部署 BondingCurve,再部署 TokenFactory,最后调用 setFactory 绑定。之后无法更改。

这比构造函数传参更灵活(两个合约互相依赖,构造函数无法同时传入对方地址),同时比可随时修改的 setter 更安全。

六、安全考量

重入攻击防护

什么是重入? 合约向外部地址转账时,控制权暂时交给接收方。如果接收方是恶意合约,它可以在状态更新前"回调"原合约,反复提款直到资金耗尽。

本合约用了两层防护:

第一层:nonReentrant(OpenZeppelin ReentrancyGuard),通过状态锁阻止嵌套调用:

function sell(address token, uint256 tokenAmount)
    external
    nonReentrant
    poolExists(token)
    notGraduated(token)
{ ... }

第二层:Checks-Effects-Interactions 模式(CEI)。 先更新所有状态变量,再和外部合约交互。以 sell 为例:

// Effects:先更新状态
pool.tokenBalance += tokenAmount;
pool.ethBalance -= ethToReturn;
pool.tokensSold -= tokenAmount;

// Interactions:再向外部地址转账
(bool success, ) = payable(msg.sender).call{value: ethAfterFee}("");

如果顺序反过来,攻击者的恶意合约在收到 ETH 时回调 sell,此时余额还没扣减,可以反复提款。

余额校验兜底

require(ethAfterFee <= pool.ethBalance, "Not enough ETH in pool");

理论上 Bonding Curve 的数学保证了退款不会超过池内 ETH。但整数截断、边界情况、或未来代码修改都可能打破这个不变量。显式校验让合约在最坏情况下安全失败。

approve 顺序

_graduate 里必须先 approve 再调用 addLiquidityETH——后者内部会调用 transferFrom 从合约拉取 token,如果 approve 没有提前执行,整个毕业交易会 revert。

七、总结:三个相互支撑的设计决策

1. 线性 Bonding Curve 而非 AMM

解决了冷启动问题。合约本身作为做市商,无需对手方流动性即可买卖。价格走势透明可预测,数学上可验证。

2. TokenFactory 职责分离

TokenFactory 只负责创建,BondingCurve 只负责交易。onlyFactory 守卫边界,setFactory 一次性绑定防止后期修改。小合约更容易审计,也更容易在不影响存量 token 的情况下迭代。

3. 毕业时销毁 LP

LP token 发给零地址,流动性永久锁死。这把"不 rug pull"从一个承诺变成了一个链上事实。机制信任取代人工信任,这是 DeFi 设计哲学的精髓。


可以继续探索的方向:

  • 二次曲线:价格随买入加速上涨,早期参与者获得更高回报,但价格波动更剧烈
  • 动态手续费:根据市场活跃度调整手续费,平衡协议收入和用户体验
  • 链上元数据:将 token 名称/描述/图片存储到 IPFS,减少对中心化存储的依赖
  • 多链部署:相同合约逻辑部署到 Base 等低手续费链

完整代码见 yolo-pump 项目