闪电贷是智能合约之间的贷款,必须在同一笔交易中偿还。本文介绍了 ERC 3156 闪电贷规范,以及闪电贷放贷方和借款方可能遭受黑客攻击的方式。文章末尾提供了建议的安全练习。
下面是一个极其简单的闪电贷示例。

如果借款方不偿还贷款,带有“flash not paid back”消息的 require 语句将导致整个交易回滚。
只有合约才能使用闪电贷
EOA 钱包无法在单笔交易中调用函数获取闪电贷然后将代币转回。与闪电贷集成需要一个单独的智能合约。
闪电贷不需要抵押品
如果闪电贷的实现是正确的(这是一个很大的前提!),那么就不存在贷款无法偿还的风险,因为 revert 或失败的 require 语句将导致交易失败,Ether 也不会被转移。
闪电贷有什么用途?
套利
闪电贷最常见的用例是进行套利交易。例如,如果 Ether 在一个资金池中的交易价格为 1,200 美元,而在另一个 DeFi 应用程序中的交易价格为 1,300 美元,那么在第一个资金池中购买 Ether 并在第二个资金池中以 100 美元的利润出售将是非常理想的。然而,你首先需要资金来购买 Ether。闪电贷是解决这个问题的理想方案,因为你不需要闲置的 1,200 美元。你可以借入价值 1,200 美元的 Ether,以 1,300 美元的价格出售,然后偿还 1,200 美元,为自己保留 100 美元的利润(扣除费用后)。
贷款再融资
对于常规的 DeFi 贷款,通常需要某种抵押品。例如,如果你要借入 10,000 美元的稳定币,你需要存入价值 15,000 美元的 Ether 作为抵押品。
如果你的稳定币贷款利率为 5%,而你想在另一个借贷智能合约中以 4% 的利率进行再融资,你需要:
- 偿还 10,000 美元的稳定币
- 提取价值 15,000 美元的 Ether 抵押品
- 将价值 15,000 美元的 Ether 抵押品存入另一个协议
- 以较低的利率再次借入 10,000 美元的稳定币
如果你的 10,000 美元被占用在其他应用程序中,这就成问题了。借助闪电贷,你可以在不使用自己任何稳定币的情况下完成步骤 1-4。
更换抵押品
在上面的例子中,借款方使用了价值 15,000 美元的 Ether 作为抵押品。但是假设该协议在使用 wBTC 时提供更低的抵押率呢?借款方可以使用闪电贷和上述类似的一系列步骤来换出抵押品,而不是本金。
清算借款方
在 DeFi 贷款中,如果抵押品价值跌破某个阈值,抵押品就会被清算——即被强制出售以支付贷款成本。在上述例子中,如果 Ether 的价值跌至 12,000 美元,协议可能会允许某人在先偿还 10,000 美元贷款的情况下,以 11,500 美元的价格购买这些 Ether。
清算人可以使用闪电贷偿还 10,000 美元的稳定币贷款并获得价值 11,500 美元的资产。然后,他们可以在另一家交易所将其出售换取稳定币,随后偿还闪电贷。
提高其他 DeFi 应用程序的收益率
Uniswap 和 AAVE 通过交易费或借贷利息为存款人赚取收益。但由于它们在一个地方集中了大量资金,它们还可以通过提供闪电贷来赚取额外收入。由于相同的资本现在有了更多用途,这提高了资金利用率。
在单笔交易中构建杠杆循环
人们可以通过使用借贷协议来实现杠杆做多和做空。例如,为了对 ETH 进行杠杆做多,用户可以将 ETH 作为抵押品存入借贷池,借入稳定币,将稳定币兑换为 ETH,然后再将这些 ETH 存入借贷池,并不断重复该过程。抵押品和借入的 ETH 的总规模将大于初始金额,从而使借款方获得更大的 ETH 价格风险敞口。
为了对 ETH 进行杠杆做空,用户可以将稳定币存入借贷池,借入 ETH,将 ETH 兑换为稳定币,然后再将稳定币存入借贷池,并不断重复该过程。现在,用户背负了大量的 ETH 债务,如果 ETH 价格下跌,这笔债务将更容易偿还。
通过这种方式可以借入的资产总额为:
其中 是协议接受的最高贷款价值比。例如,如果协议要求存入价值 1,000 美元的稳定币才能借入价值 800 美元的 ETH,那么 LTV 就是 。因此,用户所能获得的 ETH 价格敞口最高可达其存款价值的 倍。也就是说,只需 1,000 美元的存款,他们就能获得价值 5,000 美元 ETH 的敞口。
除了通过循环执行所有这些交易(这可能会消耗大量 Gas)之外,用户还可以:
- 使用闪电贷借入价值 5,000 美元的稳定币
- 将稳定币兑换为价值 5,000 美元的 ETH
- 将 ETH 作为抵押品存入借贷池
- 从借贷池中借入价值 4,000 美元的稳定币
- 将他们自己的 1,000 美元稳定币与从借贷池中借入的 4,000 美元稳定币相加,以偿还闪电贷。
现在,用户拥有了价值 5,000 美元的 ETH 作为抵押品,并从借贷池中借入了价值 4,000 美元的稳定币。
黑客攻击智能合约
闪电贷可能最出名的是被黑帽黑客用来对协议进行漏洞利用。闪电贷的主要攻击向量是价格操纵和治理(投票)操纵。如果在防御不足的 DeFi 应用程序上使用闪电贷,攻击者可以大量买入某种资产以抬高其价格,或者获取大量投票代币以强行通过某项治理提案。
出于好奇,以下是闪电贷黑客攻击的列表。然而,漏洞是双向的。如果实现不当,闪电贷的放贷合约和借款合约同样容易遭受资金损失。
闪电贷黑客攻击示例
闪电贷攻击是最常见的漏洞利用手段之一,可能是因为具有 web2 背景的开发者对此尚不习惯。以下是一些更为臭名昭著的例子。
rekt.news/deus-dao-rekt/
rekt.news/jimbo-rekt/
rekt.news/platypus-finance-rekt/
rekt.news/beanstalk-rekt/
rekt.news/inverse-rekt2/
使用闪电贷攻击协议是一个独立的话题,本文主要关注闪电贷放贷和借款合约的不安全实现。
ERC3156 协议
ERC3156 旨在标准化获取闪电贷的接口。虽然工作流程相对简单,但确切的实现细节需要被统一,例如,我们应该将函数命名为 getFlashLoan、onFlashLoan 还是其他名称?以及它应该接受哪些参数?
ERC3156 接收方规范
该标准的第一个方面是借款方需要实现的接口,如下所示。借款方只需实现一个函数。

我们在此描述函数参数:
initiator
这是发起闪电贷的地址。你可能需要在此处进行某种验证,以防止不受信任的地址在你的合约上发起闪电贷。通常,该地址会是你,但你不应理所当然地这么认为!
onFlashLoan 函数预期应由闪电贷合约调用,而不是由 initiator 调用。你应该在 onFlashLoan() 函数内部检查 msg.sender 是否为闪电贷合约,因为此函数是 external 的,任何人都可以调用它。
initiator 既不是 msg.sender 也不是闪电贷合约。它是触发闪电贷放贷合约去调用接收方的 onFlashLoan 函数的那个地址。
token
这是你要借入的 ERC20 代币地址。提供闪电贷的合约通常会持有多种可以用于闪电贷的代币。ERC3156 闪电贷标准不支持原生 Ether 的闪电贷,但可以通过借出 WETH 并让借款方解包 WETH 来实现。因为借款合约不一定是调用闪电贷放贷方的合约,所以可能需要告知借款合约正在闪电借出哪种代币。
fee
fee 是为该笔贷款需要支付多少代币作为费用。它以绝对金额计算,而不是百分比。
data
如果你的闪电贷接收合约没有硬编码在接收闪电贷时执行特定操作,你可以使用 data 参数将其行为参数化。例如,如果你的合约用于交易池套利,你可以指定要与哪些资金池进行交易。
返回值
该合约必须返回 keccak256("ERC3156FlashBorrower.onFlashLoan"),具体原因我们将在后文讨论。
借款方参考实现
为了让代码片段更短,这是对 ERC 3156 规范中代码修改后的版本。请注意,该合约仍然对闪电贷放贷方寄予了完全的信任。**如果闪电贷放贷方以某种方式被攻破,以下合约可能会被传入伪造的 amount、fee 和 initiator 数据从而遭受漏洞利用。**如果放贷方是不可变的,这就不成问题,但如果放贷方是可升级的,这就可能成为一种攻击向量。

ERC3156 放贷方规范
下面是 ERC3156 规范中定义的放贷方接口:

上述接口中的参数含义与前一节中描述的相同,因此这里不再赘述。
flashLoan() 函数需要完成几个重要的操作:
- 可能会有人使用闪电贷合约不支持的代币来调用
flashLoan()。应当对此进行检查。 - 可能会有人使用大于
maxFlashLoan的数量来调用flashLoan()。这也应该进行检查。 data会直接转发给调用方。
更重要的是,flashLoan() 必须将代币转移给接收方并且将代币转回。它不应依赖于借款方主动将代币转回以完成还款。这背后的理由将在下一节中讨论。我们将可以在 EIP 3156 规范中找到的参考实现复制到了这里,以强调其中的重要部分:

注意,参考实现假设 ERC20 代币在成功时返回 true,但并非所有代币都是如此,因此如果使用不符合规范的 ERC20 代币,请使用 SafeTransfer 库。
安全注意事项
借款方的访问控制与输入验证
借款智能合约必须设置相应的控制措施,仅允许闪电贷放贷合约成为 onFlashLoan() 的调用方。否则,闪电贷放贷方以外的其他角色就可能调用 onFlashLoan() 并引发意外行为。
此外,任何人都可以调用 flashloan()(以任意借款方为目标)并传递任意 data。为了确保 data 不是恶意的,闪电贷接收合约应仅允许一组受限的 initiator。
重入锁(Reentrancy locks)非常重要
根据定义,ERC 3156 无法遵循检查-生效-交互模式来防止重入。它必须通知借款方其已收到代币(发起外部调用),然后将代币转回。因此,应在合约中添加 nonReentrant 锁。
重要的是,必须由放贷方将代币转回,或者部署重入锁。
在上述实现中,放贷方将代币从借款方那里转回(扣款)。借款方并不是主动将贷款转回给放贷方。这对于避免“侧门”攻击非常重要,即借款方作为放贷方将资金存入协议。这时,资金池会发现其余额已恢复到之前的水平,但借款方却突然成了一个拥有大额存款的放贷方。
UniswapV2 的闪电贷在完成闪电贷后并不会主动将代币转回。不过,它使用了一个重入锁,以确保借款方无法以放贷方的名义将资金存入协议来“偿还贷款”。
对于借款方,确保只有闪电贷放贷合约才能调用 onFlashLoan
闪电贷放贷方被硬编码为仅调用接收方的 onFlashLoan() 函数,而不会调用任何其他函数。如果借款方有办法指定闪电贷放贷方调用哪个函数,那么闪电贷就可能被操纵去转移其拥有的其他代币(通过调用 ERC20.transfer),或者将其代币余额的授权授予恶意地址。
因为此类操作需要显式调用 ERC20 的 transfer 或 approve,所以如果闪电贷放贷方只能调用 onFlashLoan(),这种情况就不会发生。
这种漏洞利用在现实世界中确有发生,Rekt News 在此处记录了一个 MEV 机器人被黑客攻击的事件。
使用 token.balanceOf(address(this)) 可能会被操纵
在上述实现中,除了用于确定最大闪电贷规模外,我们并没有使用 balanceOf(address(this))。因为这可以被其他人通过直接向合约转移代币来篡改,从而干扰逻辑。我们确认闪电贷已归还的方式是放贷方转回了贷款金额 + fee。虽然有一些使用 balanceOf(address(this)) 来检查还款的有效方法,但这必须与重入检查结合使用,以避免将还款伪装成存款。
为什么闪电贷借款方需要返回 keccak256(“ERC3156FlashBorrower.onFlashLoan”);
这样做是为了处理这样一种情况:带有 fallback 函数的合约(非闪电贷放贷合约)已向闪电贷放贷合约进行了授权。有人可能会以该合约作为接收方不断发起闪电贷。随后会发生以下情况:
- 受害者合约获得一笔闪电贷
- 受害者合约被调用
onFlashLoan()函数,触发了 fallback 函数但未回滚。由于 fallback 函数会响应任何不匹配合约中其他函数的调用,因此它会响应对onFlashLoan()的调用。 - 闪电贷放贷方从借款方提取代币 +
fee
如果此操作循环发生,带有 fallback 函数的受害者合约将被掏空。EOA 钱包也会发生同样的情况,因为对钱包地址调用 onFlashLoan 不会回滚。
仅仅检查 onFlashLoan 函数不发生回滚是不够的。闪电贷放贷方还会检查是否返回了 keccack256("ERC3156FlashBorrower.onFlashLoan"),以此确认借款方确实有意借入代币并支付 fee。
与闪电贷相关的练习题
以下来自 DamnVulnerableDeFi 和 Mr Steal Yo Crypto 的问题可以帮助你练习上述攻击向量。理解闪电贷的最佳方式之一是了解在实现它们时不该做什么。
- Naive Receiver (你的目标是掏空借款方,而不是闪电贷放贷方)
- Side Entrance
- Truster
温习一下有关 ERC 4626 的知识,然后练习以下问题:
- Unstoppable (这个较难,所以最后做。你的目标是使合约瘫痪,而不是窃取资金)。
- Flash Loaner (来自 Mr Steal Yo Crypto。请确保你理解 ERC 4626)
所有这些问题都与攻击放贷方或借款方有关,而不是利用闪电贷去攻击其他目标。
在 RareSkills 了解更多
本材料是我们高级 Solidity Bootcamp 的一部分。请查看该项目以了解更多信息。
最初发布于 2023 年 11 月 7 日