本文详细介绍了Zealynx团队如何在Beanstalk代码审计竞赛中发现一个关键漏洞,该漏洞涉及defaultGaugePointFunction函数在处理percentOfDepositedBdv等于optimalPercentDepositedBdv时缺乏明确条件判断的问题。
仅在 2024 年,就有超过 $22 亿 在黑客攻击和漏洞利用中被盗。这就是为什么 Bug 赏金迅速成为 Web3 中最关键和有效的安全措施之一。它们不能取代经过深思熟虑的智能合约安全审计/审计员。相反,它们为项目提供了一种经济高效的方式,以便在恶意行为者可以利用智能合约和其他 Web3 应用程序中的漏洞之前识别和解决这些漏洞。
在 Zealynx,我们喜欢参与这些赏金活动,因为它们可以磨练我们的技能,使我们及时了解现实世界的攻击向量,并促使我们在区块链安全领域保持领先地位。
在本文中,你将找到关于我们如何检测到一个 bug 的详细解释,该 bug 为我们在 Beanstalk 竞赛中赢得了丰厚的奖励。
我们将讨论我们如何处理代码,我们用来发现漏洞的策略,以及导致我们发现该 bug 的具体技术:未能维持 Gauge Points。
本文的目标是鼓励你,读者,尽可能地深入研究尽可能多的竞赛。在此过程中,我们将提供关键资源、建议以及从我们的经验中吸取的教训。
让我们开始吧。
让我们分享一些关于情况的更多信息。
在我们开始竞赛之前,我们刚刚完成了一项密集的模糊测试活动(在我们的 GitHub Repo 中查看结果)。这意味着我们只有 2-3 天的时间来参与 CodeHawks 竞赛,然后才能投入到我们的下一个活动中。
时间安排是一个额外的挑战,但也是展示我们技能的独特机会。我们立刻投入了一次初步的计划电话会议和我们标志性的 Notion 模板,我们将其用于每个智能合约审计项目。目标是高效地组织和分配任务。
此外,当时有多个 bug 赏金报告和竞赛同时进行,因此我们必须快速决定优先参与哪个竞赛。在评估了各种选项后,我们选择了 Beanstalk 竞赛,因为它看起来既有趣又符合我们在智能合约审计方面的专业知识。我们没有浪费时间,直接开始识别漏洞。
该项目主要使用 Hardhat,这对于测试来说非常复杂,因此我们将其调整为 Foundry。我们使用静态分析工具,如 Slitherin, Aderyn, Wake, 和 Olympix,以获得第一印象并研究项目的薄弱环节。
接下来,我们开始了手动分析和测试活动。在此阶段,我们分析了合约的每个部分,以了解每个函数的逻辑。我们通过对每个函数进行模糊测试来验证是否满足所有不变量。对于这部分,我们使用 Foundry 作为我们的主要工具。
免费资源:查看我们的文章 "如何在 Aderyn 中逐步编写 Detector",以了解更多关于如何使用 Aderyn 查找 bug 的详细信息。
我们审计的基本支柱之一是使用模糊测试。我们使用它们是因为它们允许我们探索无数可能的场景,这些场景如果使用手动方法会花费更长的时间,并且会更加复杂。当然,手动审查仍然是必要的,我们使用它们来验证一切是否正常工作。
在深入研究代码(以及本文更技术性的方面)之前,我们想感谢 CodeHawks 团队 的出色工作。
在最终竞赛结果公布的前几天,我们收到了一个更新,显示奖励为 13,000 USDC。然而,当我们联系 Cyfrin 团队进行确认时,他们友好地澄清说这些还不是最终结果。
阅读到最后以找出最终奖励金额。👇
免费资源:查看他们的最新网站更新 - CodeHawks Update
接下来,我们将分解合约以及我们发现问题的函数。你可以阅读所有内容或跳到你最感兴趣的部分。
在本节中,我们将逐行分解合约的整个逻辑。
1/*
2 * SPDX-License-Identifier: MIT
3 */
4
5pragma solidity =0.7.6;
6pragma experimental ABIEncoderV2;
7
8import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol";
9import {LibGauge} from "contracts/libraries/LibGauge.sol";
10
11/**
12 * @title GaugePointFacet
13 * @author Brean
14 * @notice 计算白名单 Silo LP 代币的 gaugePoints。
15 */
16contract GaugePointFacet {
17 using SafeMath for uint256;
18
19 uint256 private constant ONE_POINT = 1e18;
20 uint256 private constant MAX_GAUGE_POINTS = 1000e18;
21
22 uint256 private constant UPPER_THRESHOLD = 10001;
23 uint256 private constant LOWER_THRESHOLD = 9999;
24 uint256 private constant THRESHOLD_PRECISION = 10000;
25
26 /**
27 * @notice DefaultGaugePointFunction
28 * 是计算 LP 资产的 gauge points 的默认函数。
29 *
30 * @dev 如果已存入 BDV 的百分比在最佳范围的 0.01% 内,
31 * 则保持 gauge points 不变。
32 *
33 * 将 gaugePoints 限制为 MAX_GAUGE_POINTS,以避免失控的 gaugePoints。
34 */
35 function defaultGaugePointFunction(
36 uint256 currentGaugePoints,
37 uint256 optimalPercentDepositedBdv,
38 uint256 percentOfDepositedBdv
39 ) external pure returns (uint256 newGaugePoints) {
40 if (
41 percentOfDepositedBdv >
42 optimalPercentDepositedBdv.mul(UPPER_THRESHOLD).div(THRESHOLD_PRECISION)
43 ) {
44 // gauge points 不能低于 0。
45 if (currentGaugePoints <= ONE_POINT) return 0;
46 newGaugePoints = currentGaugePoints.sub(ONE_POINT);
47 } else if (
48 percentOfDepositedBdv <
49 optimalPercentDepositedBdv.mul(LOWER_THRESHOLD).div(THRESHOLD_PRECISION)
50 ) {
51 newGaugePoints = currentGaugePoints.add(ONE_POINT);
52
53 // 如果 gaugePoints 超过,则将其限制为 MAX_GAUGE_POINTS。
54 if (newGaugePoints > MAX_GAUGE_POINTS) return MAX_GAUGE_POINTS;
55 }
56 }
57}
defaultGaugePointFunction 用于根据已存入的基础存款价值 (BDV) 的百分比调整流动性提供者 (LP) 代币的 gauge points。
currentGaugePoints:LP 代币的当前 gauge points。optimalPercentDepositedBdv:应存入的 BDV 的最佳百分比。percentOfDepositedBdv:已存入的 BDV 的当前百分比。ONE_POINT:表示一个 gauge point (1e18)。MAX_GAUGE_POINTS:允许的最大 gauge points 数 (1000e18)。UPPER_THRESHOLD:上限阈值 (10001,表示 100.01%)。LOWER_THRESHOLD:下限阈值 (9999,表示 99.99%)。THRESHOLD_PRECISION:阈值精度 (10000)。该函数有两个主要的条件块 (if 语句),用于根据已存入的 BDV 的百分比确定如何调整 gauge points。
让我们分解函数的每个块,以准确了解它做什么以及何时执行每个部分。
1if (
2 percentOfDepositedBdv >
3 optimalPercentDepositedBdv.mul(UPPER_THRESHOLD).div(THRESHOLD_PRECISION)
4)
解释:
将 percentOfDepositedBdv 与 optimalPercentDepositedBdv 进行比较,该值由 UPPER_THRESHOLD(最佳值的 100.01%)调整。
上限阈值计算:
optimalPercentDepositedBdv 乘以 UPPER_THRESHOLD (10001)。THRESHOLD_PRECISION (10000)。示例:
如果 optimalPercentDepositedBdv 为 50:
150 × 10001 / 10000 = 50.005
条件为:
1如果 percentOfDepositedBdv > 50.005,则满足条件。
操作:
1if (currentGaugePoints <= ONE_POINT) return 0;
2newGaugePoints = currentGaugePoints.sub(ONE_POINT);
解释:
如果 currentGaugePoints 小于或等于 ONE_POINT,则将其设置为 0。
否则,currentGaugePoints 减少 ONE_POINT。
1else if (
2 percentOfDepositedBdv <
3 optimalPercentDepositedBdv.mul(LOWER_THRESHOLD).div(THRESHOLD_PRECISION)
4)
解释:
将 percentOfDepositedBdv 与 optimalPercentDepositedBdv 进行比较,该值由 LOWER_THRESHOLD(最佳值的 99.99%)调整。
optimalPercentDepositedBdv 乘以 LOWER_THRESHOLD (9999)。THRESHOLD_PRECISION (10000)。示例:
如果 optimalPercentDepositedBdv 为 50:
[ 50 \times 9999 / 10000 = 49.995 ]
条件为:
如果 percentOfDepositedBdv < 49.995,则满足条件。
操作:
1newGaugePoints = currentGaugePoints.add(ONE_POINT);
2if (newGaugePoints > MAX_GAUGE_POINTS) return MAX_GAUGE_POINTS;
ONE_POINT 添加到 currentGaugePoints。newGaugePoints 超过 MAX_GAUGE_POINTS,则将其设置为 MAX_GAUGE_POINTS。当已存入的基础存款价值 (BDV) 的百分比完全等于最佳百分比 (optimalPercentDepositedBdv) 时,defaultGaugePointFunction 在处理 gauge points 的调整时存在问题。在这种情况下,该函数缺少一个显式条件来管理这种情况,这可能会导致意外的行为。
问题的详细信息
percentOfDepositedBdv 大于由 UPPER_THRESHOLD 调整的 optimalPercentDepositedBdv 以及小于由 LOWER_THRESHOLD 调整的 optimalPercentDepositedBdv 的条件。percentOfDepositedBdv 完全等于 optimalPercentDepositedBdv,则不会满足任何条件,这可能会导致 gauge points 被意外调整为 0,而不是保持其当前值。解决方案是什么?
要解决此问题,需要添加一个显式条件来处理 percentOfDepositedBdv 等于 optimalPercentDepositedBdv 的情况。可以通过添加一个 else 子句来实现,如果未满足任何先前的条件,则该子句返回未更改的 currentGaugePoints。
在函数末尾添加以下条件:
1else {
2 return currentGaugePoints;
3}
通过 POC 演示问题
为了有效地演示该问题,我们编写了两个测试:一个简单的测试用例和一个模糊测试。这些测试表明,当已存入的 BDV 的百分比完全等于最佳百分比时,defaultGaugePointFunction 存在问题。
简单测试用例
此测试专门针对 percentOfDepositedBdv 等于 optimalPercentDepositedBdv 的情况。
1function testnew_GaugePointAdjustment() public {
2 uint256 currentGaugePoints = 1189;
3 uint256 optimalPercentDepositedBdv = 64;
4 uint256 percentOfDepositedBdv = 64;
5
6 uint256 newGaugePoints = gaugePointFacet.defaultGaugePointFunction(
7 currentGaugePoints,
8 optimalPercentDepositedBdv,
9 percentOfDepositedBdv
10 );
11
12 assertTrue(newGaugePoints <= MAX_GAUGE_POINTS, "New gauge points 超过允许的最大值");
13 assertEq(newGaugePoints, currentGaugePoints, "Gauge points 调整与预期的结果不匹配");
14}
设置:
currentGaugePoints 设置为 1189。optimalPercentDepositedBdv 设置为 64。percentOfDepositedBdv 设置为 64。结果验证:
newGaugePoints 应等于 currentGaugePoints,因为 percentOfDepositedBdv 等于 optimalPercentDepositedBdv。else 条件,newGaugePoints 可能为 0,这是不正确的。模糊测试 此测试使用一系列值来确保稳健性,并检查 gauge points 的正确调整。
1function testGaugePointAdjustmentUnifiedFuzzing(
2 uint256 currentGaugePoints,
3 uint256 optimalPercentDepositedBdv,
4 uint256 percentOfDepositedBdv
5) public {
6 currentGaugePoints = bound(currentGaugePoints, 1, MAX_GAUGE_POINTS - 1);
7 optimalPercentDepositedBdv = bound(optimalPercentDepositedBdv, 1, 100);
8 percentOfDepositedBdv = bound(percentOfDepositedBdv, 1, 100);
9
10 uint256 expectedGaugePoints = currentGaugePoints;
11
12 if (percentOfDepositedBdv * THRESHOLD_PRECISION > optimalPercentDepositedBdv * UPPER_THRESHOLD) {
13 expectedGaugePoints = currentGaugePoints > ONE_POINT ? currentGaugePoints - ONE_POINT : 0;
14 } else if (percentOfDepositedBdv * THRESHOLD_PRECISION < optimalPercentDepositedBdv * LOWER_THRESHOLD) {
15 expectedGaugePoints = currentGaugePoints + ONE_POINT <= MAX_GAUGE_POINTS ? currentGaugePoints + ONE_POINT : MAX_GAUGE_POINTS;
16 }
17
18 uint256 newGaugePoints = gaugePointFacet.defaultGaugePointFunction(
19 currentGaugePoints,
20 optimalPercentDepositedBdv,
21 percentOfDepositedBdv
22 );
23
24 assertTrue(newGaugePoints <= MAX_GAUGE_POINTS, "New gauge points 超过允许的最大值");
25 assertEq(newGaugePoints, expectedGaugePoints, "Gauge points 调整与预期的结果不匹配");
26}
预期 Gauge Points 计算:
最初,expectedGaugePoints 设置为 currentGaugePoints。
阈值检查:
percentOfDepositedBdv 高于上限阈值,则 expectedGaugePoints 减小 ONE_POINT 或设置为 0。percentOfDepositedBdv 低于下限阈值,则 expectedGaugePoints 增加 ONE_POINT,上限为 MAX_GAUGE_POINTS。断言:
newGaugePoints 不超过 MAX_GAUGE_POINTS。newGaugePoints 与 expectedGaugePoints 匹配。测试结果
要正确运行测试,请按照以下步骤操作:
1export FORKING_RPC=https://eth-mainnet.g.alchemy.com/v2/{API}
2forge test --mc GaugePointFacetTest --mt testGaugesssPointAdjustmentUnifiedFuzzing -vvv
1[FAIL. Reason: assertion failed; counterexample: calldata=0xdace5ffa0000000000000000000000000465ff2e9c9fd7f2b5a78cfaa0671d046c517d5d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 args=[25110567842437950750745261937466550563734912349 [2.511e46], 0, 1]] testGaugesssPointAdjustmentUnifiedFuzzing(uint256,uint256,uint256) (runs: 0, μ: 0, ~: 0)
2Logs:
3 Bound Result 505308988514485682721
4 Bound Result 1
5 Bound Result 1
6 Error: Gauge points 调整与预期的结果不匹配
7 Error: a == b not satisfied [uint]
8 Left: 0
9 Right: 505308988514485682721
10
11Traces:
12 [30286] DefaultTestContract::testGaugesssPointAdjustmentUnifiedFuzzing(25110567842437950750745261937466550563734912349 [2.511e46], 0, 1)
13 ├─ [0] console::log("Bound Result", 505308988514485682721 [5.053e20]) [staticcall]
14 │ └─ ← [Stop]
15 ├─ [0] console::log("Bound Result", 1) [staticcall]
16 │ └─ ← [Stop]
17 ├─ [0] console::log("Bound Result", 1) [staticcall]
18 │ └─ ← [Stop]
19 ├─ [826] GaugePointFacet::defaultGaugePointFunction(505308988514485682721 [5.053e20], 1, 1) [staticcall]
20 │ └─ ← [Return] 0
21 ├─ emit log_named_string(key: "Error", val: "Gauge points 调整与预期的结果不匹配")
22 ├─ emit log(val: "Error: a == b not satisfied [uint]")
23 ├─ emit log_named_uint(key: " Left", val: 0)
24 ├─ emit log_named_uint(key: " Right", val: 505308988514485682721 [5.053e20])
25 ├─ [0] VM::store(VM: [0x7109709ECfa91a80626fF3989D68f67F5b1DD12D], 0x6661696c65640000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000001)
26 │ └─ ← [Return]
27 └─ ← [Stop]
28
29Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 9.52ms (3.61ms CPU time)
感谢你阅读到最后(或者跳到这里来了解我们收到了多少 😝)。
最终,我们收到了总计 8223.41 USDC。我们在这上面度过了美好的时光,而经济奖励只是其中的一项好处。
在我们结束之前,我们鼓励你仔细研究和探索某些资源,这些资源对于学习智能合约审计、模糊测试、智能合约开发以及智能合约审计工具的应用等内容非常有帮助。
我们在下表中提供了它们。
- 原文链接: zealynx.io/blogs/Find-Ou...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!