代码裂缝:理解AMM协议的漏洞

本文深入探讨了自动化做市商(AMM)协议中的关键特性、协议类型以及核心安全漏洞。主要分析了Token Pair Pools、Flash swaps等关键特性,并对Uniswap V1/V2/V3、Balancer、Curve等不同类型的AMM协议进行了比较。着重介绍了重入攻击、逻辑流错误、溢出/下溢等常见的攻击手段,并给出了审计AMM协议时需要重点关注的建议。

代码中的裂缝:理解 AMM 协议的漏洞

millie

本文与 Vladislav Yaroshuk 合作撰写。

介绍

在不断发展的去中心化金融(DeFi)领域中,寻求一种高效的交易方式,以便在智能合约中同时容纳做市商和交易者,这一努力从未停止。传统的金融市场依赖于订单簿模型,做市商设定价格和数量,交易者则据此执行交易。然而,区块链的独特特性给采用这种模型带来了挑战,特别是由于在漫长的区块生成时间的限制下,频繁处理订单的下达和取消是不切实际的。

自动化做市商(AMM) 简化了做市商的行为。通过允许做市商向单个流动性池提供多个代币,AMM 根据池的余额自动计算当前价值和汇率。然后,交易者可以无缝地执行交易,而无需与做市商直接互动。虽然这种创新简化了流程,但也带来了一系列挑战。

本文探讨了 AMM 协议中的关键特性、协议类型和核心安全漏洞,揭示了审计员和开发人员必须应对的缺陷。

关键特性

代币对池

作为用户,你可以通过组合两种不同的代币来创建流动性池。例如,你可以将像 USDC 这样的稳定币与另一种代币(如 ETH)配对,从而形成像 USDC/ETH 这样的交易对。

用户可以提供流动性

你向 USDC/ETH 代币对贡献 100,000 美元,分配 50,000 美元给 USDC,50,000 美元给 ETH。完成此交易后,你的地址将自动获得 100,000 个流动性提供者代币(LPT),代表你对流动性池的贡献。

使用资产进行交易的机会

选择像 USDC 和 ETH 这样的交易对,包括指定要出售或购买的 USDC 代币数量,以通过 AMM 平台界面换取 ETH。AMM 算法根据交易后流动性池的变化自动计算价格,并在操作完成后使用新的代币分布更新池。

闪电兑换

即时兑换功能,允许你作为用户,立即从流动性池中借用代币,但条件是在交易结束前归还借入金额加上费用,或提供等值的另一种代币。

价格预言机

一种向区块链提供资产价格信息的预言机。

协议类型

AMM 协议的审计过程很大程度上取决于其类型。

1. 无集中流动性的恒定乘积

Uniswap V1/V2
  • 与 Uniswap V1 相比,Uniswap V2 显著改进了流动性池,通过启用灵活的 ERC-20/ERC-20 代币对,消除了对 ETH 作为主要货币的需求。

  • Uniswap V2 使用现有的流动性池增强了价格预言机系统,以实现更准确和防篡改的定价。它还引入了闪电兑换,允许用户立即借用代币。

  • 交易对充当自动流动性提供者,只要保持“恒定乘积”公式,就随时准备接受一种代币换另一种代币。这个公式最简单的表达形式是 x∗y=kx * y = kx∗y=k,它表示交易不得改变一对储备余额(xx x 和 yyy)的 乘积(k)product (k)product(k)

![Constant Product Market Maker (CPMM)](https://img.learnblockchain.cn/2025/11/10/ecEp1o-RuQfveNouy97eQ.png)

恒定乘积做市商(CPMM)

Balancer
  • Balancer 上的流动性池可以包含最多 8 种加密货币的任意比例。这意味着 Balancer 池中的每个代币可以具有不同的权重或占总池价值的百分比,从而为流动性提供者提供更复杂的策略。

  • 该协议具有两种池类型:公共池和私有池。公共池对所有人开放,创建后参数不可更改。私有池为创建者提供了更大的控制权,可以管理代币、权重、费用和暂停交易。

  • 资产恒定乘积池的公式是 Uniswap 的 x∗y=kx * y = kx∗y=k 的扩展。对于三个资产,它看起来像这样:x∗y∗z=kx * y * z = kx∗y∗z=k,其中:xxxyyy、zzz 是池中每个代币的余额。

![](https://img.learnblockchain.cn/2025/11/10/8jA_6wz6-mJEjdo3mPwPi.png)

2. 稳定币交换池

Curve
  • 主要用于行为相似的资产交换,例如稳定币,或类似资产的封装版本,例如 wBTC 和 tBTC。

  • 实现了三元池的创建,从而降低了操作期间与费用和滑点相关的成本。

  • 应用公式 x∗y∗z=kx*y*z = kx∗y∗z=k,其中 xxxyyyzzz 表示池中的代币余额,以确保 余额(k)balances (k)balances(k) 乘积的恒定性。

![Stable Swap from Curve Finance](https://img.learnblockchain.cn/2025/11/10/YA37yQ99oyzPo1qUrrie0.png)

来自 Curve Finance 的稳定币交换

3. 具有集中流动性的恒定乘积

Uniswap V3
  • 集中流动性是 V3 背后的主要概念。流动性提供者可以专注于特定的价格范围(例如,每 ETH 1,800 美元到 1,900 美元),而不是在整个价格曲线上均匀分配流动性,从而优化资本效率。

  • 这种有针对性的方法增加了在高交易量区域参与交易的可能性,从而最大化了潜在的交易收入。

  • 提供的流动性数量可以通过值 𝐿𝐿L 来衡量,该值等于 √k√k√k

![](https://img.learnblockchain.cn/2025/11/10/iXPko7c3ACIhfDmpTAdiI.png)
![](https://img.learnblockchain.cn/2025/11/10/bha8LT7gsEjRImWuGuW5I.png)

常见的攻击向量

所有攻击的一般概念在于操纵智能合约的行为,以从中提取资产。

重入

在重入攻击中,一个函数可以在其执行期间被外部调用,从而允许它在单个事务中被多次执行。当一个合约在完成对其当前状态的处理之前调用另一个合约时,通常会发生这种情况。

为了更清楚地理解这种攻击,让我们看一个 Beanstalk 的 Wells 协议 的例子。

可以通过利用 removeLiquidity 函数中的漏洞来对 Beanstalk 协议进行攻击。主要漏洞在于违反了 CEI 模式

在攻击期间,恶意行为者可以利用具有 ERC-777 回调函数的外部合约,然后在 removeLiquidity 函数的执行期间调用 getReserves()。这允许获得不正确的储备值。

由于储备的更新发生在与外部合约交互(通过 safeTransfer)之后,而不是在它之前,因此攻击者可以操纵协议的状态,从而产生潜在的关键漏洞。

尽管 removeLiquidity 函数受到 nonReentrant 修饰符的保护,该修饰符可防止重入调用,但由于只读重入,该漏洞仍然存在。外部合约可以调用 getReserves(.html) 并干扰该过程,特别是如果它们使用具有回调函数的 ERC-777 代币。

来源: https://github.com/solodit/solodit_content/blob/main/reports/Cyfrin/2023-06-16-Beanstalk wells.md#read-only-reentrancy

更多例子:

逻辑流程

这种类型的漏洞表明开发者对项目文档的误解或智能合约逻辑的错误执行,这可能导致不良结果或漏洞利用。

让我们以 Kyberswap 为例来检查此漏洞。

逻辑流程与 CLMM 中对刻度的错误处理相关联,这可能导致双倍流动性添加。

用户在刻度划分的价格范围内提供流动性。利用 currentTick 位于刻度范围边界上的状态,nearestCurrentTick 错误计算为 currentTick — 1,导致在范围(currentTickcurrentTick + n)中挖掘流动性。在随后的单向零交换期间,此错误计算会导致重新添加创建的流动性。

问题在于,在预挖掘时,跨越刻度边界会添加流动性 l0。挖掘会添加 l1 流动性,但也会促成刻度范围,从而导致在跨越刻度边界时添加 l0 + l1 流动性。最后,由于挖掘和跨越,并且两个刻度变得相同,因此添加了 l1 + l0 + l1 流动性。

黑客从 1000 ETH、2,000,000 USDC 池开始。使用闪电贷以 1 美元(刻度 0)的价格用 5000 ETH 兑换 USDC。黑客计算 (0, n) 中所需的流动性以耗尽 6000 ETH 的池。铸造此流动性的一半,利用该漏洞。黑客用 6000 USDC 兑换 6000 ETH,偿还闪电贷,并保留约 1000 ETH 作为利润以及收到的 USDC。

来源: https://100proof.org/kyberswap-post-mortem.html

更多例子:

溢出/下溢

溢出: 结果超出数据类型的最大值(例如,uint8 在 255 之后环绕到 0)。

下溢: 结果在减少到有符号类型的最小值以下时环绕到最大值(例如,int8 在 -128 之后环绕到 127)。

一个很好的例子是 Velodrome 协议

该协议采用稳定的 curve 公式 ( x³y+y³x >= k) 用于稳定币对,其中存在由于 不变值 (k) 的计算中的舍入误差而导致的关键漏洞。

具体来说,_k functionvariable _a 的计算中遇到舍入误差,导致当 x * y < 1e18 时将其无效化。如果 x * y 的值小于或等于 1e18,则会发生舍入误差。此错误随后导致 swap() 函数中产品常数的错误且成功的验证,从而触发未经授权的池耗尽。

攻击场景涉及第一个流动性提供者 铸造 少量流动性到池中,利用舍入误差将不变值 k 设置为零。随后,攻击者可以通过铸造和耗尽来重复从池中窃取,直到总供应量溢出。

来源: https://github.com/spearbit/portfolio/blob/master/pdfs/Velodrome-Spearbit-Security-Review.pdf

还值得考虑 舍入误差,在 Balancer 的上下文中。

由于舍入的困难,此问题在 线性池 中进行代币兑换操作期间表现出来。

错误源于假设缩放操作始终可以在舍入的情况下执行,而不会产生严重的后果。在线性池中,兑换是以极小的代币数量进行的,导致舍入误差逐渐累积。反过来,这会影响池中的代币兑换率。

该漏洞涉及使用闪电兑换,其中借入的代币被兑换为基础代币和封装代币,然后以较低的汇率返回。这为攻击者提供了从舍入误差中获利的机会。

来源: https://medium.com/balancer-protocol/rate-manipulation-in-balancer-boosted-pools-technical-postmortem-53db4b642492

数据验证

缺少或不够彻底地验证和清理用户提供的输入数据。

FraxSwap 使用重新调整基数的代币在长期交换期间引入了一个关键的数据验证漏洞。

用户可以通过 cancelLongTermSwap() 函数随时取消这些交换,从而回收未售出的代币。但是,当涉及重新调整基数的代币时,在交换期间可能会出现实际合约余额与 internal reserves[] 帐户之间的差异。这种不一致会带来在取消和提取操作期间向用户过度转移代币的风险,从而过早地耗尽合约的余额并导致交易还原。

Uniswap 文档 中所述,正确构建重新调整基数的代币对于防止用户代币丢失至关重要。

来源: https://github.com/trailofbits/publications/blob/master/reviews/FraxQ22022.pdf

更多例子:

不正确的集成

在基于现成协议或第三方库的智能合约中集成组件或功能时出错。

让我们看一下 Uranium Finance 攻击示例。

原始 Uniswap V2 代码 中,该算法依赖于 1000 的乘数进行余额调整,从而确保了 K=XY 不变量的完整性。但是,在分叉到 Uranium Finance 期间,开发人员错误地将此乘数增加到 swap() 函数中的两个实例中的 10000,但未能更新函数末尾的相应检查。

在 Uranium Finance 代码中,乘数在相同位置增加到 10000:

uint balance0Adjusted = balance0.mul(10000).sub(amount0In.mul(16));
uint balance1Adjusted = balance1.mul(10000).sub(amount1In.mul(16));

但是,关键的疏忽在于未能更新末尾的检查,其中保留了原始的 1000 乘数:

require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UraniumSwap: K');

这种不一致性允许恶意行为者利用该缺陷,允许将可忽略的少量代币换成不成比例的大量代币。

来源: https://uraniumfinance.medium.com/exploit-d3a88921531c

更多例子:

价格操纵

人为地影响市场中资产的价格以获得优势。

一种常见的价格操纵方法涉及使用 闪电贷 — 在单个交易中在交易所提供的无抵押贷款。恶意行为者可以利用闪电贷立即借入大量资金并操纵市场价格,从而造成资产价格的人为变动。

另一种方法涉及影响 DEX 上的流动性。肇事者可能会执行一系列旨在人为地增加或减少交易量的交易操作,从而导致资产价格发生变化。

这种操纵发生在 PancakeSwap BH/USDT 交易对 上。

在这种情况下,攻击者瞄准了 PancakeSwap 上的 BH/USDT 交易对。通过以低价将 USDT 兑换为 BH,攻击者设法以显著虚高的价格从指定的交易对中提取流动性。当攻击者偿还闪电贷并保留利润时,攻击结束。

为了在 BNB 链上执行攻击,攻击者支付了大约 4.16 美元的费用。但是,在策划价格操纵并利用创建的滑点之后,攻击者成功提取了大约 127 万美元的 USDT。

来源: https://www.halborn.com/blog/post/explained-the-bh-token-hack-october-2023

更多例子:

访问控制

访问控制漏洞与对控制谁有权访问智能合约中特定函数或数据的机制的实施不足或不正确有关。这可能导致未经授权的函数调用。

让我们以 CEXISWAP 为例来探讨这种类型的漏洞。

该合约通过一个未受保护的 initialize() function 被利用,允许攻击者将自己确立为合约的所有者。

为了执行攻击,创建了 Exploiter contract,并调用了其 exploit() 函数。在 exploit() 中,触发了 initialize() 函数,将攻击者设置为 CexiSwap 合约的所有者。随后,调用了 upgradeToAndCall(),提供了攻击者的地址并调用了 exploit2() 函数。

在通过 upgradeToAndCall() 激活的 exploit2() 函数中,delegatecall 用于调用转移函数,从而促进了所有 USDT 向攻击者帐户的转移。

通过承担合约的所有权并利用 upgradeAndCall() 函数,攻击者成功地将 USDT 从易受攻击的合约中移出。

来源: https://twitter.com/DecurityHQ/status/1704759560614126030?s=20

更多例子:

编译器错误

该漏洞与编程语言编译器本身的错误有关,这可能导致智能合约编译过程中出现不可预见或不正确的结果。

此类问题可能导致源代码处理不正确、优化不当、gas 成本估算不准确,并可能影响语言版本的兼容性。

一个很好的例子是 pETH/ETH 池 漏洞。

该漏洞是由于智能合约实现中的错误以及使用具有 递归保护 缺陷的旧版本 Vyper 编译器所致。

攻击者通过 Balancer 使用了 80,000 WETH 的闪电贷,将其兑换为 ETH,并向 Curve pETH/ETH 池 提供了 40,000 ETH,收到了 32,431.41 个 LP 代币。通过销毁这些代币,他们提取了 3,740 pETH 和 34,316 ETH。

然后,他们又存入了 40,000 ETH,创建了额外的 82,182 个 LP 代币。提取了 1,184.73 pETH 和 47,506.53 ETH,同时销毁了 10,272.84 个 Curve LP 代币。他们在 Curve 池中用 4,924 pETH 兑换了 4,285 ETH,然后将 86,106.65 ETH 封装为 WETH。最后,他们将 80,000 WETH 返还给 Balancer 以偿还闪电贷。此次攻击导致了 6,106.65 WETH(约 1100 万美元)的利润。

由于移除和添加流动性之间存在递归错误,从而允许攻击者无限增加池代币并对整个池的持有量提出欺诈性索赔,因此发生了漏洞。

来源: https://hackmd.io/@LlamaRisk/BJzSKHNjn

消除协议中编译器错误的最佳方法是使用 模糊测试。模糊测试还有助于消除与业务逻辑相关的风险,例如,在 Kyberswap 协议中找到漏洞 后,实施了修复措施并减轻了所有风险,但是,由于缺乏模糊测试,错过了一种情况,即漏洞 仍然可以被利用 通过暴力破解池并找到特殊条件,导致 > 5400 万美元的损失。

SafeERC20

transfertransferFrom 函数中缺少返回值检查。

例如,Spartan Protocol

在 ERC20 的标准实现中,当余额不足时,transfer()transferFrom() 等函数会返回“false”而不是错误。这可能导致在没有收到足够资金的情况下创建代币。

为了解决这个问题,建议在使用 ERC20 代币时使用来自 OpenZeppelin 的 SafeERC20。此工具通过检查布尔返回值并在发生故障时还原交易来增强 ERC20 函数调用的安全性。

此外,SafeERC20 提供了用于增加或减少津贴的辅助方法,有助于缓解潜在的抢跑攻击。

来源: https://code4rena.com/reports/2021-07-spartan#h-03-result-of-transfer--transferfrom-not-checked

中心化风险

由于权力过大的角色,如果私钥泄露,可能会从协议中提取所有代币。例如,许多项目由于 Vanity 钱包 中 Profanity 地址生成器的漏洞而被黑客入侵。

Wintermute 的攻击是由 Profanity 算法中的缺陷引起的,该缺陷允许肇事者直接瞄准 Wintermute 用户的受损私钥。

为了在密码学中获得最大安全性,通常选择一个随机值作为 密码学伪随机数生成器 (CPRNG) 的种子,以创建私钥。但是,Profanity 使用一个 32 位数字作为其 CPRNG 的种子,从而使攻击者能够有条不紊地猜测值并重建私钥。

在 Wintermute 的案例中,这影响了他们的 DeFi 金库合约和热钱包,它们都是虚荣地址。

来源: https://www.halborn.com/blog/post/explained-the-wintermute-hack-september-2022

通货紧缩代币

通货紧缩代币是一种旨在随着时间的推移减少其总体供应量的数字货币或代币。这种减少通常通过燃烧来实现,其中涉及从流通中永久移除每笔交易的一小部分百分比。

目的是通过创造稀缺性来提高流通中剩余代币的价值,从而导致需求增加。

有两个复杂的交易被发送到以太坊主网,导致对两个 Balancer 池 的攻击。

攻击者使用智能合约来自动化行动。最初,从 dYdX 平台借用了 104,000 WETH 的 FlashLoan,随后将 WETH 来回兑换为 STA(STATERA) 24 次。由于 STA 的通货紧缩模型,这导致从池中耗尽 STA 余额,将其减少到 1 weiSTA。由于应用了费用,Balancer 池在每次兑换中收到的 STA 减少了 1%。

接下来,黑客多次用 1 weiSTA 兑换 WETH,导致 STA 退出池,而 WETH 则保持不变。类似地,WBTC、SNX 和 LINK 代币的余额也从池中耗尽。

在最后阶段,攻击者偿还了 dYdX 平台上的 104,000 WETH 的 FlashLoan。攻击者通过存入少量的 weiSTA 增加了他们在 Balancer 池中的份额。然后,通过 Uniswap V2 将从池中收集的代币兑换为 136,000 STA,随后将 136,000 STA 兑换为 109 WETH。

来源: https://blog.1inch.io/balancer-hack-2020/

更多例子:

滑点

AMM 协议容易受到可用代币流动性变化的影响,尤其是在高波动时期。这意味着预期的交易价格可能与实际执行的价格不同。

Derby Finance 中的漏洞源于在 swap 交易期间缺乏滑点保护,特别是在 Vault.claimTokens()MainVault.withdrawRewards() 函数中。

Swaps 库计算 swap 交易中的滑点参数,导致对最小输出值的评估不准确。这使协议面临潜在的 抢跑攻击,包括 三明治攻击,其中恶意行为者利用缺乏滑点保护的漏洞。

用户可以触发 withdrawRewards() 函数从协议中获得奖励,在此过程中,一部分奖励通过 Uniswap 池交换为 DERBY 代币并转移给用户。

该漏洞危及此交换过程的安全性,强调需要外部滑点计算,并依赖信誉良好的预言机来获得准确的市场价格,以有效降低风险。

来源https://github.com/sherlock-audit/2023-01-derby-judging/issues/64

更多例子

顺便说一句,要深入研究更多漏洞示例并探索有关它们的更多信息,请关注此 Telegram 频道 :)

UniswapV3 中的代币集成问题

在像 UniswapV3 这样的去中心化交易所的世界中,保持安全至关重要。目前,某些类型的代币存在一些问题,特别是 Fee-on-transfer 和 rebasing token,当在 UniswapV3 上使用它们时。

Fee-on-transfer 代币

因此,Fee-on-transfer 代币,它在每次交易中增加一点费用,与 UniswapV3 的路由合约无法顺利协作。就像他们没有正确地互相交谈。为了解决这个问题,创建这些代币的聪明人可能必须找到不同的方法,例如制作一个特殊的包装器或自定义的路由器。但问题是 - UniswapV3 将来不会创建支持这些代币的路由器。因此,创建者需要自己解决这个问题。

Rebasing 代币

现在,让我们来谈谈 rebasing token。这些有点不同,因为它们根据一些规则改变流通中的代币数量。它们对于在池中创建和交换很有用,但有一个问题。如果你正在把你的钱投入到一个池子中,并且存在负 rebase(意味着你失去了一些代币),那么就无法找回你所失去的东西。流动性提供者,即把钱投入其中的人,需要小心,因为他们可能会面临损失,而没有机会弥补。

审计时的建议

  1. 使用通用的 SCSVS 清单。

  2. 验证所有合约中 CEI 模式 的正确实现:检查条件,然后进行更改,然后再与其他合约交互。

  3. 审查所有与重入漏洞相关的案例。即使所有合约都在 nonReentrant 修饰符下 - 检查只读重入。

  4. 检查 Solidity 版本是否存在常见的编译器错误,检查单元测试的覆盖率以及是否存在模糊测试。

  5. 验证项目是否使用经过充分测试和安全的数学库。

  6. 验证合约中的数学运算是否与文档中声明的完全相同。

  7. 检查数学运算在小数位数较少以及小数位数较多时如何工作。

  8. 将协议与其 fork 进行比较,或与启发它的协议进行比较,检查常见的集成问题,以及新功能、缺失的验证。

  9. 检查所有数学计算中的舍入问题、溢出和下溢。

  10. 验证所有合约是否对输入参数进行了适当的验证,注意滑点、截止日期等。该协议应使用 SafeErc20 进行代币交互。

  11. 检查所有合约的访问控制。

  12. 验证非标准代币的案例。

  13. 检查池中价格操纵的可能性。

  14. 验证角色是否不过分强大,协议使用 multisig 来管理合约。

  15. 检查费用是否始终计算正确,并且永远不能为 100%。

  16. 检查如果协议有预言机 - 它是否建立在 TWAP 的架构之上。

  17. 验证对其他合约的所有调用,待处理的津贴。

总结

在本文中,我们对近年来自动化交易协议遇到的主要安全向量进行了分类。在审计过程中,特别注意这些向量是至关重要的方面。

  • 原文链接: mirror.xyz/millietez.eth...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
millietez.eth
millietez.eth
江湖只有他的大名,没有他的介绍。