一、背景:Pump.fun 为什么能成功?
传统 Memecoin 发射的问题很简单:项目方预留大量代币,上线即抛售。社区参与的是一场信息不对称的博弈。
Pump.fun(运行在 Solana 上)的创新在于用机制设计代替了"相信项目方":
- 无预售:没有团队预留份额,所有人从同一起点买入
- Bonding Curve 定价:价格由链上公式决定,不可操纵(Bonding Curve 是一种价格随买入量自动上涨的定价机制)
- 自动毕业:达到市值阈值后,流动性自动迁移到公开 DEX,LP token 永久销毁,任何人无法撤走流动性("毕业"指从内部孵化池迁移到公开交易所)
本文用 Solidity + Uniswap V2 复刻同一套机制,将毕业阈值设定为 24 ETH。整个流程三步走:
创建 Token → 在 Bonding Curve 上交易 → 积累 24 ETH 后自动毕业到 Uniswap V2,LP 永久销毁
接下来,我们逐步拆解这三步背后的设计决策。
二、为什么是线性曲线,而不是恒积公式?
Uniswap 用的是恒积公式 x * y = k,已经被验证是优秀的 AMM 机制。为什么不直接用它?
原因:恒积 AMM 需要双边初始流动性。
部署一个 Uniswap 池子,你必须同时提供 ETH 和 Token。而在 Memecoin 冷启动场景,项目刚创建时没有人愿意提供 ETH 流动性------项目方提供又回到了预售问题。
线性 Bonding Curve 解决了这个问题:合约本身就是做市商,用公式单边定价。
价格公式:
scss
price(n) = INITIAL_PRICE + SLOPE × tokensSold
对应合约里的常量:
solidity
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 的总花费:
scss
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
代入上下限 [a, a+n]:
css
F(a+n) - F(a)
= INITIAL_PRICE·(a+n) + SLOPE·(a+n)²/2 - INITIAL_PRICE·a - SLOPE·a²/2
= INITIAL_PRICE·n + SLOPE·((a+n)² - a²)/2
= INITIAL_PRICE·n + SLOPE·(2a·n + n²)/2
对应合约实现(注意精度处理):
solidity
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 用整数模拟小数的标准做法(与 ERC-20 的 18 位小数一致)。tokenAmount 以最小单位传入(实际代币数 × 1e18),所以:
- 线性项:
INITIAL_PRICE * tokenAmount / PRECISION--- 除以一个 PRECISION 还原为"实际代币数" - 二次项:
SLOPE * (...) / (2 * PRECISION * PRECISION)--- 两个 tokenAmount 相乘,需除以两个 PRECISION
为什么买入要用二分查找?
理想情况下,用户给出 ETH 数量,我们反解出能买多少 token。但二次方程在整数域没有精确解------Solidity 没有浮点数,直接解方程会有截断误差。
解决方案:二分查找。在 [0, 十亿 × PRECISION] 范围内搜索"花费不超过传入 ETH 的最大 token 数量":
solidity
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;
}
卖出如何计算退款?
卖出不需要二分查找,因为卖出数量是已知的。用"卖出前的总花费 - 卖出后的总花费"即可得到退款金额:
solidity
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),但数学层面的对称性成立:买入价格曲线的面积等于卖出返还的面积。
四、工厂模式:TokenFactory 为什么要独立
最直接的实现方式是把所有逻辑塞进一个合约:创建 token、管理价格、执行毕业。但这个项目拆成了两个合约:TokenFactory 和 BondingCurve。
为什么?
TokenFactory 负责"创建"------每发一个新 Memecoin,Factory 就部署一个新的 ERC-20 token 合约实例,这和 Web2 工厂模式里的"创建对象"是同一个概念,只是这里创建的是链上合约。BondingCurve 负责"交易"。分离之后:
BondingCurve可以独立迭代,便于后续升级曲线逻辑而不影响 TokenFactory 的部署- 每个合约更小,更容易审计
- 权限边界清晰:只有 Factory 能在 BondingCurve 上创建池子
这个边界通过 onlyFactory modifier 强制执行:
solidity
modifier onlyFactory() {
require(msg.sender == factory, "Only factory");
_;
}
function createPool(
address token,
address creator,
uint256 tokenAmount
) external onlyFactory {
// ...
}
如果没有这个限制,任意外部账户都可以调用 createPool,向 BondingCurve 注入伪造的池子------比如伪装成已有 token 的池子,覆盖其状态,或注册一个恶意 token 合约。onlyFactory 在权限层面封堵了这个入口。
部署顺序与初始化顺序
注意 setFactory 的设计:
solidity
function setFactory(address _factory) external onlyOwner {
require(factory == address(0), "Factory already set");
factory = _factory;
}
factory 只能写入一次(address(0) 检查)。这是一个常见的"一次性初始化"模式:部署时先部署 BondingCurve,再部署 TokenFactory,最后调用 setFactory 绑定,之后无法更改。
这比构造函数传参更灵活(两个合约互相依赖,构造函数无法同时传入对方地址),同时比可随时修改的 setter 更安全。
五、毕业机制:24 ETH 之后发生了什么
Bonding Curve 不是终点,它是一个"孵化池"。当池子积累足够的 ETH,token 就"毕业"------自动转移到 Uniswap V2,成为正式的链上流动性对。
为什么是 24 ETH?
这是一个工程参数,权衡的是两件事:
- 太低:流动性不够深,毕业后容易被大单冲击
- 太高:门槛太高,大多数 Memecoin 永远毕业不了
24 ETH 是本文实现选择的阈值,参考了 Pump.fun 在 Solana 上验证过的市值锚定思路。
毕业流程
买入时,合约检查是否达到阈值:
solidity
if (pool.ethBalance >= GRADUATION_THRESHOLD) {
_graduate(token);
}
_graduate 做了三件事:
solidity
function _graduate(address token) internal {
Pool storage pool = pools[token];
pool.graduated = true; // 标记已毕业,阻止后续交易
uint256 ethForLiquidity = pool.ethBalance;
uint256 tokensForLiquidity = pool.tokenBalance;
pool.ethBalance = 0; // 清空状态(先写状态,防重入)
pool.tokenBalance = 0;
IERC20(token).approve(address(uniswapRouter), tokensForLiquidity);
uniswapRouter.addLiquidityETH{value: ethForLiquidity}(
token,
tokensForLiquidity,
0, // 接受任意数量的 token(不设滑点保护)
0, // 接受任意数量的 ETH
address(0), // LP token 接收地址:零地址 = 销毁
block.timestamp + 300 // deadline:防止交易被长时间挂起后在不利价格下成交
);
}
LP 发给 address(0) 意味着什么?
addLiquidityETH 的第六个参数是 LP token 的接收地址。LP token 是你向池子注入流动性后拿到的凭证,凭它可以随时按比例赎回池中的 ETH 和 token。发给零地址意味着这些 LP token 没有任何人能取走------等于销毁了赎回流动性的唯一钥匙,流动性被永久锁死在 Uniswap 池子里。
这是机制设计的核心承诺:没有人能撤走流动性,包括合约开发者。rug pull 在机制层面被消除,而不是依赖项目方的道德自律。
六、安全考量
合约处理真实资金,安全设计不能马虎。这个实现里有三个值得注意的地方。
1. 重入攻击
什么是重入攻击?
重入攻击是智能合约特有的安全漏洞:合约在向外部地址转账(ETH 或 ERC-20)时,控制权暂时交给了接收方。如果接收方是一个恶意合约,它可以在这个时机"回调"原合约,在第一笔转账的状态还没更新时再发起第二笔转账------不断循环,直到把池子里的资金榨干。经典例子是"提款时先转 ETH,再清零余额",攻击者通过回调在余额清零前反复提款。
buy 和 sell 都涉及向外部地址转账(ETH 或 ERC-20),这是重入攻击的经典入口。合约用了两层防护:
第一层:nonReentrant modifier(来自 OpenZeppelin ReentrancyGuard),通过一个状态锁阻止嵌套调用:
solidity
function sell(address token, uint256 tokenAmount)
external
nonReentrant // 防重入锁
poolExists(token)
notGraduated(token)
{ ... }
两层防护各司其职:nonReentrant 锁住公开入口(buy/sell),CEI 是函数内部的状态更新纪律------即使没有锁,状态也不会因回调变脏。
第二层:Checks-Effects-Interactions 模式(CEI)。规则是:先更新所有状态变量(Effects),再和外部合约交互(Interactions)。以 sell 为例------先更新余额,再给用户转 ETH:
solidity
// Effects:先更新状态
pool.tokenBalance += tokenAmount;
pool.ethBalance -= ethToReturn;
pool.tokensSold -= tokenAmount;
// Interactions:再向外部地址转账
(bool success, ) = payable(msg.sender).call{value: ethAfterFee}("");
如果顺序反过来,攻击者的恶意合约在收到 ETH 时可以回调 sell,此时 pool.ethBalance 还没扣减,攻击者可以反复提款直到池子被榨干。
2. sell 时的余额校验
solidity
require(ethAfterFee <= pool.ethBalance, "Not enough ETH in pool");
这里校验的是 ethAfterFee(扣除手续费后的实际退款),合约实际从 pool.ethBalance 扣减的是扣费前的 ethToReturn(ethAfterFee + fee),因此校验 ethAfterFee <= pool.ethBalance 是充分条件。
这个 check 看起来多余------理论上 Bonding Curve 的数学保证了退款不会超过池内 ETH。但整数截断、边界情况、或未来的代码修改都可能打破这个不变量。显式校验让合约在最坏情况下安全失败,而不是悄悄出错。
3. approve 顺序
_graduate 里先 approve 再调用 addLiquidityETH:
solidity
IERC20(token).approve(address(uniswapRouter), tokensForLiquidity);
uniswapRouter.addLiquidityETH{value: ethForLiquidity}(
token,
tokensForLiquidity,
...
);
顺序不能反。addLiquidityETH 内部会调用 transferFrom 从合约拉取 token,如果 approve 没有提前执行,整个毕业交易会 revert。
七、总结:三个关键设计决策
回顾整篇文章,这套 Memecoin 发射台的核心是三个相互支撑的设计决策:
1. 线性 Bonding Curve 而非 AMM
解决了冷启动问题。合约本身作为做市商,任何人无需提供对手方流动性就可以买卖 token。价格走势透明可预测,数学上可验证。
2. TokenFactory 职责分离
TokenFactory 只负责创建,BondingCurve 只负责交易。onlyFactory 守卫边界,setFactory 一次性绑定防止后期修改。小合约更容易审计,也更容易在不影响存量 token 的情况下迭代。
3. 毕业时销毁 LP
LP token 发给零地址,流动性永久锁死。这把"不 rug pull"从一个承诺变成了一个链上事实。机制信任取代人工信任,这是 DeFi 设计哲学的精髓。
可以继续探索的方向:
- 二次曲线:价格随买入加速上涨,早期参与者获得更高回报,但价格波动更剧烈
- 动态手续费:根据市场活跃度调整手续费,平衡协议收入和用户体验
- 链上元数据:将 token 名称/描述/图片存储到 IPFS,减少对中心化存储的依赖
- 多链部署:相同合约逻辑部署到 Base 等低手续费链
完整代码见 yolo-pump 项目