01 · CONTRACTS
合约地址
调用前请根据当前 chainId 选择 Router。不要跨链混用 Router、Factory、Pair Token 和 Launch Token 地址。
Arc Mainnet
Chain ID 5042
LaunchFactory
0xA4f31EE541862B3B7505e60CF2DB2D61fF54A730LaunchCurveRouter
0xb6F86747fe4004595Af15fcE30033CCf97ED4FC8LaunchLocker
0xae4c5b799ba5aE639C01Ea1a488F3Cd816773f10LaunchCurve
由 LaunchFactory 动态创建Robinhood L2 Mainnet
Chain ID 4663
LaunchFactory
0x59462DbAFE07E8155C152645C105D0E90B09d008LaunchCurveRouter
0x7Ddbb18e90Ad177825A5cf32531C475b9E7c6d70LaunchLocker
0xa56B1a41d31B571d53F74e646c4FFE5C2fF2f2b7LaunchCurve
由 LaunchFactory 动态创建页面只显示 chains 中启用的网络,地址读取自 src/config/contracts.ts。维护网络和地址配置后,本文档会自动更新。
02 · EXACT INPUT
精确输入交易
已知 launchToken 后,通过 Factory 获取 Pair Token 和 Curve,再完成报价、授权与 Router 调用。示例兼容 ERC20 和原生币 Pair Token。
const factory = new ethers.Contract(
factoryAddress,
launchFactoryAbi,
signer,
)
const launch = await factory.getLaunch(launchToken)
const curve = new ethers.Contract(launch.curve, launchCurveAbi, signer)
const router = new ethers.Contract(routerAddress, launchCurveRouterAbi, signer)
const [reserve0, reserve1] = await curve.getReserves()
const token0IsLaunchToken = await curve.token0IsLaunchToken() // Launch Token 是否为 token0
const pairTokenIsWrappedNative = await curve.pairTokenIsWrappedNative() // Pair Token 是否为封装原生币
const protocolFeeBps = await factory.protocolFeeBps() // 协议费率,分母为 10000
const graduationPairAmount = await curve.graduationPairAmount() // 发射成功所需 Pair Token 数量
const salePairReserve = await curve.salePairReserve() // 当前已进入曲线的 Pair Token 储备
const launchReserve = token0IsLaunchToken
? reserve0
: reserve1
const pairReserve = token0IsLaunchToken
? reserve1
: reserve0
const pairToken = launch.pairToken
const pair = new ethers.Contract(pairToken, erc20Abi, signer)
const amountIn = ethers.parseUnits('100', await pair.decimals())
const BPS = 10_000n
let curveAmountIn =
amountIn * (BPS - protocolFeeBps) / BPS
const remainingPairAmount =
graduationPairAmount - salePairReserve
if (curveAmountIn > remainingPairAmount) {
curveAmountIn = remainingPairAmount
}
const quotedOut =
curveAmountIn * launchReserve /
(pairReserve + curveAmountIn)
const slippageBps = 100n
const amountOutMin =
quotedOut * (BPS - slippageBps) / BPS
const path = [pairToken, launchToken]
const to = await signer.getAddress()
const deadline = Math.floor(Date.now() / 1000) + 600
if (!pairTokenIsWrappedNative) {
await (
await pair.approve(routerAddress, amountIn)
).wait()
}
const callAmountIn =
pairTokenIsWrappedNative ? 0n : amountIn
const overrides =
pairTokenIsWrappedNative
? { value: amountIn }
: {}
const tx = await router.swapExactTokensForTokens(
callAmountIn,
amountOutMin,
path,
to,
deadline,
overrides,
)
const receipt = await tx.wait()getLaunch 返回参数
| 字段 | 作用 |
|---|---|
token | Launch Token 地址 |
creator | 代币创建者地址 |
curve | 对应的 LaunchCurve 地址 |
pairToken | 买入和卖出使用的配对资产 |
dexId | 迁移目标 DEX 配置编号 |
launchConfigId | 发射曲线配置编号 |
supply | Launch Token 总供应量 |
initialBuyAmount | 创建时的初始购买数量 |
dexVersion | 迁移目标 DEX 版本 |
exists | Token 是否已注册到 Factory |
getReserves 返回参数
| 字段 | 作用 |
|---|---|
reserve0 | token0 对应的曲线虚拟储备 |
reserve1 | token1 对应的曲线虚拟储备 |
blockTimestampLast | 最近更新储备的区块时间戳 |
Router 参数与返回值
| 字段 | 作用 |
|---|---|
amountIn | 固定总输入;原生币买入时传 0 |
amountOutMin | 允许收到的最低净输出 |
path | 买入路径为 [pairToken, launchToken] |
to | Launch Token 接收地址 |
deadline | 交易失效的秒级时间戳 |
curveAmountIn | 扣除协议费并受 graduation 限制后进入 Curve 的数量 |
quotedOut | 根据虚拟储备计算的预计输出 |
receipt | 交易上链后的回执 |
getReserves() 按 token0/token1 顺序返回,必须通过 token0IsLaunchToken() 判断 Launch Token 储备。
03 · EXACT OUTPUT
精确输出交易
用户指定最终收到的 Launch Token 数量,Router 反算实际输入,并通过 amountInMax 限制最高支出。
const factory = new ethers.Contract(
factoryAddress,
launchFactoryAbi,
signer,
)
const launch = await factory.getLaunch(launchToken)
const curve = new ethers.Contract(launch.curve, launchCurveAbi, signer)
const router = new ethers.Contract(routerAddress, launchCurveRouterAbi, signer)
const [reserve0, reserve1] = await curve.getReserves()
const token0IsLaunchToken = await curve.token0IsLaunchToken() // Launch Token 是否为 token0
const pairTokenIsWrappedNative = await curve.pairTokenIsWrappedNative() // Pair Token 是否为封装原生币
const protocolFeeBps = await factory.protocolFeeBps() // 协议费率,分母为 10000
const graduationPairAmount = await curve.graduationPairAmount() // 发射成功所需 Pair Token 数量
const salePairReserve = await curve.salePairReserve() // 当前已进入曲线的 Pair Token 储备
const launchReserve = token0IsLaunchToken ? reserve0 : reserve1
const pairReserve = token0IsLaunchToken ? reserve1 : reserve0
const pairToken = launch.pairToken
const pair = new ethers.Contract(pairToken, erc20Abi, signer)
const token = new ethers.Contract(launchToken, erc20Abi, signer)
const amountOut = ethers.parseUnits('100000', await token.decimals())
if (amountOut >= launchReserve) {
throw new Error('amountOut exceeds curve reserve')
}
const BPS = 10_000n
const curveAmountIn =
pairReserve * amountOut / (launchReserve - amountOut) + 1n
const protocolFee =
curveAmountIn * protocolFeeBps / BPS
const quotedAmountIn =
curveAmountIn + protocolFee
const remainingPairAmount =
graduationPairAmount - salePairReserve
if (curveAmountIn > remainingPairAmount) {
throw new Error('amountOut exceeds remaining launch capacity')
}
const slippageBps = 100n
const amountInMax =
(
quotedAmountIn *
(BPS + slippageBps) +
BPS - 1n
) / BPS
const path = [pairToken, launchToken]
const to = await signer.getAddress()
const deadline = Math.floor(Date.now() / 1000) + 600
if (!pairTokenIsWrappedNative) {
await (
await pair.approve(routerAddress, amountInMax)
).wait()
}
const overrides =
pairTokenIsWrappedNative
? { value: amountInMax }
: {}
const tx = await router.swapTokensForExactTokens(
amountOut,
amountInMax,
path,
to,
deadline,
overrides,
)
const receipt = await tx.wait()| 字段 | 作用 |
|---|---|
amountOut | 用户希望收到的净输出数量 |
curveAmountIn | 根据虚拟储备反算的 Curve 净输入 |
protocolFee | 根据 Factory 协议费率计算的费用 |
quotedAmountIn | Curve 净输入与协议费的预计总和 |
amountInMax | 增加滑点后的最高输入 |
path | 买入路径为 [pairToken, launchToken] |
to | Launch Token 接收地址 |
deadline | 交易失效的秒级时间戳 |
ERC20 只转走实际需要的 amountIn。原生币调用将 value 设置为 amountInMax,未使用部分退回 msg.sender。
04 · QUOTE
报价与滑点
Router 当前没有公开 view 报价函数。应先根据虚拟储备计算报价,再通过 simulateContract 验证交易,并加入滑点保护。
amountOut = amountIn × reserveOut ÷ (reserveIn + amountIn)
const amountOutMin =
quotedOut * (10_000n - slippageBps) / 10_000n
const amountInMax =
(quotedIn * (10_000n + slippageBps) + 9_999n) /
10_000n生产环境不要将 amountOutMin 固定为 0。协议费和接近 graduation 时的输入限制都会影响最终数量。
05 · SECURITY
特殊行为与安全提示
- 零数量语义:amountIn 或 amountInMax 为 0 时,Router 会使用当前持有的全部对应资产。
- 不要预转账:不要提前将 ERC20 转入 Router,也不要把 Router 当作资金保管合约。
- Graduation 退款:超过剩余目标的 Pair Token 会退回 msg.sender,而不是 to。
- 接收地址:输出原生币时,如果 to 是合约,该合约必须支持接收原生币。
- 迁移后交易:graduated 为 true 后,应切换至迁移后的 V3 Router。
06 · ERRORS
常见错误
| 常见错误 | 原因与处理 |
|---|---|
InvalidPath | path 必须包含两个地址;检查买卖方向 |
InvalidLaunchCurve | Factory 没有找到对应 Curve |
TransactionExpired | deadline 已过期;重新生成 |
InsufficientOutputAmount | 输出低于最低值;刷新报价 |
ExcessiveInputAmount | 输入超过最高值;刷新报价 |
NativeTransferFailed | 接收地址无法接收原生币 |
SafeERC20FailedOperation | 检查余额、allowance 和代币行为 |
