Skip to content

Token 兑换程序

一个类似于 Uniswap 的 swap 平台,用于 Solana 区块链上的 Token 程序,实现了多种自动化做市商(AMM)曲线。

审计

仓库的 README 文件包含了程序审计的信息。

可用部署

NetworkVersionProgram Address
Testnet3.0.0SwapsVeCiPHMUAtzQWZw7RjsKjgCjhwU55QGu4U1Szw
Devnet3.0.0SwapsVeCiPHMUAtzQWZw7RjsKjgCjhwU55QGu4U1Szw

虽然第三方在 Mainnet Beta 上部署了 token-swap,但团队没有计划自己部署。

查看程序仓库以获取更多开发者信息。

概述

代币交换程序允许不经由中心化限价订单簿简单地交易代币对。该程序使用一种称为“曲线”的数学公式来计算所有交易的价格。曲线的目的是模仿正常市场动态:例如,当交易者购买大量某种代币时,另一种代币的价值就会上升。

池中的存款人为代币对提供流动性。这种流动性使得以即时价格执行交易成为可能。作为提供流动性的回报,存款人会收到池代币,代表他们在池中的部分所有权。在每一笔交易中,程序会保留部分输入代币作为费用。这个费用通过存储在池中,增加了池代币的价值。

这个程序在很大程度上受到了 UniswapBalancer 的启发。更多信息可以在它们的优秀文档和白皮书中找到。

背景

Solana 的编程模型和本文档中使用的 Solana 术语的定义可以在以下链接找到:

源代码

Token Swap Program 的源代码可在 GitHub 上找到。

接口

提供了 JavaScript 绑定,支持将 Token Swap Program 加载到链上并发出指令。

示例用户界面可在此处找到。

操作概述

以下内容解释了 Token Swap Program 中可用的指令。请注意,每个指令都有一个简单的代码示例,可在端到端测试中找到。

创建新的代币交换池

创建池展示了 Solana 上的账户、指令和授权模型,这与其他区块链相比可能有很大的不同。初始化两种代币类型之间的池,为简单起见,我们将它们称为“A”和“B”,需要以下账户:

  • 空的池状态账户
  • 池权限
  • 代币 A 账户
  • 代币 B 账户
  • 池代币铸币
  • 池代币费用账户
  • 池代币接收账户
  • 代币程序

池状态账户只需使用 system_instruction::create_account 创建,并确保具有正确的大小和足够的 lamports 以免租金。池权限是一个程序派生地址,可以对其他程序的指令“签名”。这对于 Token Swap Program 来说是必需的,以便铸造池代币并从其代币 A 和 B 账户中转移代币。

代币 A / B 账户、池代币铸币和池代币账户都必须创建(使用 system_instruction::create_account)并初始化(使用 spl_token::instruction::initialize_mintspl_token::instruction::initialize_account)。代币 A 和 B 账户必须用代币充值,并将其所有者设置为交换权限,铸币也必须由交换权限拥有。

一旦所有这些账户都被创建,Token Swap 的 initialize 指令将正确设置一切,并允许立即交易。请注意,池状态账户在 initialize 上不需要是签名者,因此在执行 initialize 指令时将其与 system_instruction::create_account 放在同一交易中是很重要的。

交换操作

一旦池被创建,用户可以立即开始使用 swap 指令在其上进行交易。swap 指令会将代币从用户的源账户转移到交换的源代币账户,然后将代币从其目的代币账户转移到用户的目的代币账户。

由于 Solana 程序要求在指令中声明所有账户,用户需要从池状态账户收集所有账户信息:代币 A 和 B 账户、池代币铸币以及费用账户。

此外,用户必须允许从他们的源代币账户转移代币。最佳实践是使用 spl_token::instruction::approve 授权一个精确的金额给一个新的一次性密钥对,然后让这个新的密钥对签署交换交易。这限制了程序可以从用户账户中取走的代币数量。

存入流动性

为了实现交易,池需要从外部获得流动性。使用 deposit_all_token_typesdeposit_single_token_type_exact_amount_in 指令,任何人都可以为其他人提供交易所需的流动性,并且作为交换,存款人将收到代表池中所有 A 和 B 代币的部分所有权的池代币。

此外,用户将需要授权一个代表从他们的 A 和 B 代币账户转移代币。这限制了程序可以从用户账户中取走的代币数量。

提取流动性

在任何时候,池代币持有者都可以兑换他们的池代币,以换取代币 A 和 B,按照曲线确定的当前“公平”汇率返回。在 withdraw_all_token_typeswithdraw_single_token_type_exact_amount_out 指令中,池代币被销毁,代币 A 和 B 被转移到用户的账户中。

此外,用户将需要授权一个代表从他们的池代币账户转移代币。这限制了程序可以从用户账户中取走的代币数量。

曲线

Token Swap 程序完全可以自定义,适用于任何实现了 CurveCalculator 特性的交易曲线。如果您想实现一个新的自动化做市商,可能只需复制 Token Swap 程序并实现一个新的曲线即可。以下是一些现成的曲线可供参考。

恒定乘积

恒定乘积曲线 是众所周知的 Uniswap 和 Balancer 风格的曲线,它在所有交换中保持一个不变量,表达为交换中代币 A 和代币 B 数量的乘积。

text
A_total * B_total = invariant

如果交易者希望用代币 A 兑换一定数量的代币 B,代币 B 的计算方式如下:

text
(A_total + A_in) * (B_total - B_out) = invariant

例如,如果交换中有100个代币A和5000个代币B,而交易者希望投入10个代币A,我们可以解出invariant然后计算B_out

text
A_total * B_total = 100 * 5,000 = 500,000 = invariant

然后

text
(A_total + A_in) * (B_total - B_out) = invariant(100 + 10) * (5,000 - B_out) = 500,0005,000 - B_out = 500,000 / 1105,000 - (500,000 / 110) = B_outB_out = 454.5454...

更多信息可在 Uniswap 白皮书Balancer 白皮书 中找到。

固定价格曲线

固定价格曲线 是一种简单的曲线,始终保持代币 A 相对于代币 B 的价格不变。在初始化时,交换创建者设定 1 个代币 B 相对于代币 A 的成本。例如,如果价格设定为 17,则始终需要 17 个代币 A 来换取 1 个代币 B,反之亦然。

请注意,这种曲线不遵循传统市场动态,因为价格始终保持不变。

固定价格曲线最适用于新代币的固定发行,这些新代币明确不应受市场动态影响。例如,一位去中心化游戏创建者希望出售新的“SOLGAME”代币以供在其游戏中使用,因此他们创建了一个固定价格交换,每个 SOLGAME 定价为 2 USDC,并在交换创建时提供所有的 SOLGAME 代币。用户可以前往交换处购买他们想要的所有代币,而不用担心市场会使 SOLGAME 代币变得过于昂贵。

稳定曲线(建设中)

来自 curve.fi稳定曲线拥有不同的形状,优先考虑“稳定”交易,即交易中价格保持恒定。最重要的是,价格的变动不像常数乘积曲线那样快,因此代表相同价值的两种币之间的稳定交换应尽可能接近1:1。例如,代表美元价值的稳定币(USDC、TUSD、USDT、DAI)由于交换中的代币数量不应存在大的价格差异。

该曲线反映了 curve 的动态,更多信息可以在他们的白皮书中找到。

Token Swap Program 中稳定曲线的实现仍在建设中,一个更完整的版本可以在stable-swap-program中找到。

偏移曲线

偏移曲线可以被视为常数价格曲线和常数产品曲线的结合。它遵循常数产品曲线的动态,但允许池创建者在一侧设置一个“偏移”。该曲线的不变量是:

text
(A_total) * (B_total + B_offset) = invariant

这对于初始代币发行非常有用,其中代币创造者希望在不提供资金支持交换另一侧的情况下,通过交换出售一些新代币。这与常数价格曲线相似,但关键的区别在于偏移曲线捕捉了正常的市场动态,即随着购买,提供的代币价格将会上升。

例如,一个去中心化博彩应用的创建者希望以美元稳定币USDC交换市场上新的“SOLBET”代币,并且他们认为每个代币至少值4 USDC。他们创建了一个SOLBET与USDC之间的池,一边投入1000 SOLBET,另一边则是0 USDC,但设有4000 USDC的偏移。

如果交易者尝试用40 USDC购乏SOLBET,那么将使用偏移来计算不变量:

text
(SOLBET_total) * (USDC_total + USDC_offset) = invariant1,000 * (0 + 4,000) = 4,000,000(SOLBET_total - SOLBET_out) * (USDC_total + USDC_offset + USDC_in) = invariantSOLBET_out = 9.901

交易者用40 USDC收到了9.901 SOLBET,因此每个SOLBET的价格约为4.04 USDC,略高于每个SOLBET 4 USDC的最低价。

相反,如果交易者在创建后立即尝试用SOLBET购买USDC,交易会失败,因为池中实际上没有USDC。

测试

代币交换程序使用多种策略进行测试,包括单元测试、集成测试、属性测试和模糊测试。由于单元测试和集成测试已经广为人知,我们在这里重点介绍属性测试和模糊测试。

属性测试

在使用 proptest 工具箱时,我们专门测试曲线的特定数学属性,尤其是为了避免在任何交易、存款或提款中泄露价值。本文档不涉及属性测试的详细解释,但是与代币交换程序相关的特定属性测试可以在仓库的 curvesmath 部分找到。

模糊测试

使用 honggfuzz ,我们定期测试代币交换程序的所有可能输入,确保程序不会意外崩溃或泄露代币。本文档不涉及模糊测试的详细解释,但是程序的具体实现可以在仓库中的 指令模糊测试 部分找到。