黑客分析:Platypus Finance,2023年2月

  • Immunefi
  • 发布于 2023-04-19 18:15
  • 阅读 8

2023年2月16日,Platypus Finance协议遭受黑客攻击,损失约850万美元的稳定币抵押品,原因是其稳定币USP的偿付能力检查机制存在逻辑错误。攻击者能够利用该漏洞,借助闪电贷抵押品进行借款,然后在未偿还债务的情况下提取抵押品。本文深入分析了Platypus Finance合约中的漏洞,并创建了漏洞利用PoC,强调了适当验证的重要性,尤其是在打破正常流程的特殊函数方面。

介绍

Platypus Finance 协议在 2023 年 2 月 16 日遭到黑客攻击,由于逻辑错误漏洞,导致约 850 万美元的稳定币抵押品损失。抵押品持有合约中 USP(Platypus 的稳定币)偿付能力检查机制中的一个缺陷允许攻击者利用闪电贷抵押品进行借款,然后在不偿还债务的情况下提取。

攻击者继续将借来的 USP 兑换为 Platypus 池中其他稳定币的现有流动性。你可以在这里查看攻击交易。

在本文中,我们将分析 Platypus Finance 合约中被利用的漏洞,然后我们将创建我们自己的逻辑错误漏洞版本,并在本地 fork 上进行测试。你可以在这里查看完整的 PoC。

本文由 Immunefi 智能合约分类员 gmhacker.eth (@realgmhacker) 撰写。

背景

Platypus Finance 是 Avalanche 区块链上的一个 AMM 协议,专门用于交换稳定币。他们引入了资产负债管理的概念,即流动性提供者在提款时可以认领他们提供的相同代币的确切数量加上代币排放。

在 2023 年 2 月初,Platypus 团队宣布推出 USP,Platypus Finance 的新型原生超额抵押稳定币。用户可以从 Platypus 池中存入 LP 代币来铸造 USP 代币,从而为协议带来更高的资本效率。

根本原因

粗略了解 Platypus Finance 协议是什么以及 USP 如何工作后,我们可以深入研究实际的智能合约代码,以探索 2023 年 2 月黑客攻击中利用的根本原因漏洞。为此,我们需要深入研究 MasterPlatypusV4 合约的代码,这是 Platypus 类似 Masterchef 的协调器。我们特别感兴趣的是 emergencyWithdraw 函数。

代码片段 1:MasterPlatypusV4.sol中的 emergencyWithdraw 函数

MasterPlatypus 合约中的 emergencyWithdraw 函数允许用户从给定的池中提取他们的 LP 代币,而无需关心奖励。一个专为“仅限紧急情况”设计的函数——这是黑客潜伏的经典场所。

此函数唯一做的检查是用户是否偿付能力,使用 PlatypusTreasure.isSolvent。该函数使用一个名为 _isSolvent 的内部函数。让我们看一下。

代码片段 2:PlatypusTreasure.sol中的 _isSolvent 函数

重要的是,我们知道 emergencyWithdraw 只关心返回数据的布尔值 solvent 参数。如果用户的债务小于或等于其 USP 借款限额,则此变量为真。换句话说,如果用户有足够的抵押品来支付其债务,则认为该用户具有偿付能力。

回到 MasterPlatypus 合约,我们看到仅进行此检查是一个很大的问题。用户具有偿付能力意味着其抵押品可以支付其债务。但是,提取抵押品不应留下未付债务!使用 emergencyWithdraw 函数,任何有债务的用户都可以提取其所有抵押品 LP 代币,而无需支付先前用该相同抵押品借用的 USP,从而使协议陷入债务。

概念验证

现在我们了解了危及 Platypus Finance 协议的漏洞,我们可以制定我们自己的漏洞利用交易的概念验证(PoC)。我们将遵循黑客的示例,并从 AAVE 闪电贷资金,以便我们可以借入大量 USP。

我们将首先选择一个具有存档访问权限的 RPC 提供程序。 在本次演示中,我们将使用 Ankr 提供的免费公共 RPC 聚合器。 我们选择块号 26343613 作为我们的 fork 块,即 hack 交易之前 1 个块。

我们的 PoC 需要经过多个步骤才能成功。 以下是将在我们的攻击 PoC 中实施的内容的高级概述:

  1. 从 AAVE 闪电贷 4400 万 USDC。
  2. 将借来的 USDC 存入 Platypus 池以获取 LP 代币
  3. 将 LP 代币作为抵押品存入 MasterPlatypus 合约。
  4. 尽可能多地借用 USP 以抵押 LP 抵押品。
  5. 执行 emergencyWithdraw 以取回 LP 抵押品,而无需偿还债务。
  6. 使用 LP 代币提取最初从 AAVE 借来的 USDC。 这些资金将用于在交易结束时偿还闪电贷。
  7. 尽可能多地将 USP 兑换为 Platypus 池流动性,形式为其他稳定币。

让我们一次编写一个步骤,最后看看整个 PoC 的样子。 我们将使用 Foundry

攻击

代码片段 3:interfaces.sol,包含我们需要的接口

让我们首先创建我们的 interfaces.sol 文件,我们将在其中定义我们将在协议合约和 AAVE 上使用的各种函数。 我们正在处理 4 个不同的关键合约 ABI:AaveLendingPoolPlatypusPoolMasterPlatypusV4PlatypusTreasure

AaveLendingPool 合约是我们的 PoC 的闪电贷提供商。 PlatypusPool 合约负责在提供流动性时发行 LP 代币,以及交换协议池中存在的不同代币。 MasterPlatypusV4 合约允许存入 LP 代币以累积奖励,并且还实现了易受攻击的 emergencyWithdraw 函数。 最后,PlatypusTreasure 允许根据提供的抵押品借用 USP。

除了这些接口之外,我们将使用标准的 ERC20 接口,该接口在 forge-std 库中提供,以及 OpenZeppelin 的 EnumerableMap,以简化我们的 PoC 逻辑,因为我们没有 gas 约束。

代码片段 4:没有攻击函数的 Attacker 合约

正如我们从上面的代码片段中看到的那样,我们在我们的合约中将 Avalanche 区块链上的各种不同地址定义为常量。 具体来说,我们正在定义我们将要使用的所有 ERC20 代币、AAVE 的池和相关的 Platypus 合约。 我们还将创建一个 AddressToUintMap 变量 tokenToAmount,它将在构造函数中使用,以将代币设置为交换金额。 这只是一个 struct,用于帮助在 Platypus 池中最终将 USP 兑换为其他稳定币流动性。

代码片段 5:attack 函数

我们攻击的入口点是 attack 函数。 它只做一项简单的工作——向 AAVE 请求闪电贷。 我们传递我们想要获取的代币——USDC——以及资金数量——44m。 AAVE 借贷池将把这些资金转移给我们,然后在我们的合约上执行一个回调函数。 这意味着 AAVE 期望我们实现一个特定的接口函数,称为 executeOperation

代码片段 6:executeOperation 函数

一旦 AAVE 调用我们的回调函数,我们就准备好使用我们新收到的代币来执行攻击逻辑。 我们调用的函数将始终使用 ERC20.transferFrom 为我们完成代币的转移,因此我们需要在整个函数中多次调用 ERC20.approve。 让我们将我们的逻辑放入我们最初计划的步骤中:

  1. 闪电贷 44m USDC。 好吧,我们现在位于 AAVE 借贷池执行的回调中,所以我们已经完成了。
  2. 将 USDC 存入 Platypus 池以获得 LP 代币。 我们调用 PlatypusPool.deposit 来实现这一点。 我们指定代币地址和我们想要存入的数量。 我们知道这些是 USDC 和 44m,但我们也知道 AAVE 会在函数输入 assetamount 中传递这些值,所以我们抽象了这一点。 你会看到 block.timestamp + 1 minutes 在这里被多次用于 deadline 输入。 这是此类交易中的一个常见输入,本质上是为了避免验证者或搜索者持有已签署交易并在满足某些市场条件后执行它们的情况。 达到 deadline 后,交易将不再有效。 由于我们只是构建一个 PoC,该值是无关紧要的,但我们可能会传递 1 分钟的增量。
  3. 将 LP 代币存入 MasterPlatypus 合约。 为此,我们需要调用 MasterPlatypusV4.deposit。 我们既需要 poolId,也需要我们拥有的 LP 代币数量,但我们也需要批准这些代币的支出,所以自然我们需要 LP 代币的地址。 代币数量从上一步的存款中返回。 我们可以通过调用 PlatypusPool.assetOf(USDC) 找到 LP 代币地址,并通过调用 MasterPlatypusV4.getPoolId(LPtoken) 获得池的 id。 我们拥有在 MasterPlatypus 合约上调用 deposit 所需的所有必要信息。
  4. 借用 USP。 我们将调用 PlatypusTreasure.borrow。 因为我们希望借入尽可能多的 USP 代币,因为我们的抵押品允许,我们需要检查我们的借款限额。 我们通过 PlatypusTreasure 合约中的函数 positionView 查询此值。 它将返回一个 struct——PositionView,其中包含我们想要的信息,所以我们只需要将其传递给 borrow 函数。
  5. 执行 emergencyWithdraw——我们漏洞利用的最重要部分,但也是最简单的部分。 我们需要做的就是调用 MasterPlatypusV4.emergencyWithdraw
  6. 提取原始 USDC。 由于我们再次拥有 LP 代币,我们可以进入 PlatypusPool 合约并使用 withdraw 函数赎回底层代币——USDC。
  7. 用 USP 交换其他稳定币——为了使我们的 PoC 代码更简洁,我们循环遍历我们最初存储的 AddressToUintMap 数据。 对于这些记录中的每一个,我们调用 PlatypusPool.swap 以尝试用特定数量的 USP 换取希望数量可观的其他稳定币。 值得注意的是,每个代币的特定数量和 swap 函数输入与原始黑客使用的值相同。

结束我们的逻辑,我们调用 ERC20.approve,在 AAVE 需要转移的 USDC 数量上调用它,以便从我们这里偿还闪电贷。 我们还需要在我们的回调函数中返回 true,以便闪电贷不会恢复,按照借贷池合约的规则。

这完成了整个漏洞利用。 如果我们添加 Foundry 日志,我们的 PoC 仅为 136 行代码。 如果我们针对 fork 块号运行此 PoC,我们将获得以下资产:

  • USDC:2,403,762
  • USDCe:1,946,900
  • USDT:1,552,550
  • USDTe:1,217,581
  • BUSD:687,369
  • DAIe:691,984
  • USP 剩余:33,044,533

结论

Platypus Finance 漏洞是引发 2023 年的黑客攻击之一。 这次攻击强调了适当验证的重要性,尤其是在涉及到打破正常流程的特殊函数时。

在这种特殊情况下,我们了解到,对于最初用于紧急情况的此类函数来说,实施所有必要的检查并进行良好测试至关重要。 该漏洞打破了当一个人在市场上有债务时,抵押品不能被完全提取的假设。 应该在用户流程中绘制所有可能的状态转换的帮助下,对这样一个关键假设进行广泛的测试。

在令人惊讶的事件中,在漏洞利用发生后不到 24 小时,Platypus 团队在 BlockSec 的帮助下,设法对攻击者完成了反向 hack,追回了约 250 万美元的被盗资金。 另一个疯狂的故事,只有 DeFi 世界才能想到。

这就是我们整个 PoC 的样子。

所有代码

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

0 条评论

请先 登录 后评论
Immunefi
Immunefi
The leading bug bounty platform for blockchain with the world's largest bug bounties.