漏洞赏金:我们如何在Cyfrin CodeHawks竞赛中名列前茅

  • zealynx
  • 发布于 2024-05-01 10:15
  • 阅读 9

本文详细介绍了Zealynx团队如何在Beanstalk代码审计竞赛中发现一个关键漏洞,该漏洞涉及defaultGaugePointFunction函数在处理percentOfDepositedBdv等于optimalPercentDepositedBdv时缺乏明确条件判断的问题。

介绍

仅在 2024 年,就有超过 $22 亿 在黑客攻击和漏洞利用中被盗。这就是为什么 Bug 赏金迅速成为 Web3 中最关键和有效的安全措施之一。它们不能取代经过深思熟虑的智能合约安全审计/审计员。相反,它们为项目提供了一种经济高效的方式,以便在恶意行为者可以利用智能合约和其他 Web3 应用程序中的漏洞之前识别和解决这些漏洞。

在 Zealynx,我们喜欢参与这些赏金活动,因为它们可以磨练我们的技能,使我们及时了解现实世界的攻击向量,并促使我们在区块链安全领域保持领先地位。

在本文中,你将找到关于我们如何检测到一个 bug 的详细解释,该 bug 为我们在 Beanstalk 竞赛中赢得了丰厚的奖励。

我们将讨论我们如何处理代码,我们用来发现漏洞的策略,以及导致我们发现该 bug 的具体技术:未能维持 Gauge Points

本文的目标是鼓励你,读者,尽可能地深入研究尽可能多的竞赛。在此过程中,我们将提供关键资源、建议以及从我们的经验中吸取的教训。

让我们开始吧。

Beanstalk GitHub 存储库

选择正确的 Bug 赏金竞赛

让我们分享一些关于情况的更多信息。

在我们开始竞赛之前,我们刚刚完成了一项密集的模糊测试活动(在我们的 GitHub Repo 中查看结果)。这意味着我们只有 2-3 天的时间来参与 CodeHawks 竞赛,然后才能投入到我们的下一个活动中。

时间安排是一个额外的挑战,但也是展示我们技能的独特机会。我们立刻投入了一次初步的计划电话会议和我们标志性的 Notion 模板,我们将其用于每个智能合约审计项目。目标是高效地组织和分配任务。

此外,当时有多个 bug 赏金报告和竞赛同时进行,因此我们必须快速决定优先参与哪个竞赛。在评估了各种选项后,我们选择了 Beanstalk 竞赛,因为它看起来既有趣又符合我们在智能合约审计方面的专业知识。我们没有浪费时间,直接开始识别漏洞。

智能合约审计工具和测试方法:我们的流程和最佳实践

该项目主要使用 Hardhat,这对于测试来说非常复杂,因此我们将其调整为 Foundry。我们使用静态分析工具,如 Slitherin, Aderyn, Wake, 和 Olympix,以获得第一印象并研究项目的薄弱环节。

接下来,我们开始了手动分析和测试活动。在此阶段,我们分析了合约的每个部分,以了解每个函数的逻辑。我们通过对每个函数进行模糊测试来验证是否满足所有不变量。对于这部分,我们使用 Foundry 作为我们的主要工具。

免费资源:查看我们的文章 "如何在 Aderyn 中逐步编写 Detector",以了解更多关于如何使用 Aderyn 查找 bug 的详细信息。

我们审计的基本支柱之一是使用模糊测试。我们使用它们是因为它们允许我们探索无数可能的场景,这些场景如果使用手动方法会花费更长的时间,并且会更加复杂。当然,手动审查仍然是必要的,我们使用它们来验证一切是否正常工作。

我们赢了 13,000 美元?!

在深入研究代码(以及本文更技术性的方面)之前,我们想感谢 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 的解释

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。

函数分解

让我们分解函数的每个块,以准确了解它做什么以及何时执行每个部分。

减少 Points 的条件

1if (
2 percentOfDepositedBdv >
3 optimalPercentDepositedBdv.mul(UPPER_THRESHOLD).div(THRESHOLD_PRECISION)
4)

解释:

percentOfDepositedBdvoptimalPercentDepositedBdv 进行比较,该值由 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

增加 Points 的条件

1else if (
2 percentOfDepositedBdv <
3 optimalPercentDepositedBdv.mul(LOWER_THRESHOLD).div(THRESHOLD_PRECISION)
4)

解释:

percentOfDepositedBdvoptimalPercentDepositedBdv 进行比较,该值由 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
  • 验证 newGaugePointsexpectedGaugePoints 匹配。

测试结果

要正确运行测试,请按照以下步骤操作:

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)

最终 Bug 赏金奖励

感谢你阅读到最后(或者跳到这里来了解我们收到了多少 😝)。

最终,我们收到了总计 8223.41 USDC。我们在这上面度过了美好的时光,而经济奖励只是其中的一项好处。

在我们结束之前,我们鼓励你仔细研究和探索某些资源,这些资源对于学习智能合约审计、模糊测试、智能合约开发以及智能合约审计工具的应用等内容非常有帮助。

我们在下表中提供了它们。

有用资源

类别 资源
ZEALYNX Zealynx GitHub
公开模糊测试活动
YouTube 频道
NOTION TomoLabo
I'm SunSec
matta.’s Ethereum 安全路线图
安全研究员仪表板
审计资源
Web3 安全 DAO
区块链安全指南
智能合约验证
信息 OfficerCia
ConsenSys Diligence
已知攻击
智能合约最佳实践
DASP
Solidity 安全博客
OfficerCia 博客
SWC 注册表
路线图 目录
YouTube 视频
GitHub 路线图
课程 Updraft Cyfrin
Alchemy University
Secureum Substack
YAcademy
智能合约黑客攻击
YOUTUBE Andy Li
Spearbit
Trail of Bits
YAcademy DAO
智能合约程序员
Building Ideas IO
PatrickAlphaC
Nader Dabit
Fuzzing Labs
Ethereum Engineering Group
Tincho on Ethereum
Console Cowboys
Eat The Blocks
Johnny Time
Dapp University
Secureum Videos
Mudit Gupta Blockchain
智能合约黑客攻击
Owen Thurm - 我们如何在 CodeHawks 审计中名列前茅
  • 原文链接: zealynx.io/blogs/Find-Ou...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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