Bug Bounty Win: How We Placed Top 5 in the Cyfrin CodeHawks Contest
AuditSecurityBug BountyContest

Bug Bounty Win: How We Placed Top 5 in the Cyfrin CodeHawks Contest

30 de abril de 2024zealynx

Table of Contents

Introduction

In 2024 alone,over $2.2 billion was stolen in hacks and exploits. This is why Bug bounties have quickly become one of the most crucial and effective security measures in Web3. They do not take the place of delibrate smart contract security auditing / auditors. Rather they provide a cost-effective way for projects to identify and address vulnerabilities in smart contracts and other Web3 applications before malicious actors can exploit them.
At Zealynx, we like to participate in these bounties because they sharpen our skills, keep us updated with real-world attack vectors, and push us to stay ahead in blockchain security.
In this article, you will find a detailed explanation of how we detected a bug that earned us a substantial reward in the Beanstalk contest.
We will be discussing how we approached the code, the strategies we used to spot vulnerabilities, and the specific techniques that led to discovering the bug: Failure in Maintaining Gauge Points.
The goal of this article is to encourage you, the reader, to delve into as many contests as possible. Along the way, we’ll provide key resources, advice, and lessons learned from our experience.
Let’s get started.

Choosing the Right Bug Bounty Contest

Let’s share a bit more info about the situation.
Before we started the contest, we had just completed an intense fuzzing test campaign (Check the result in our GitHub Repo). This meant we had only 2-3 days to participate in the CodeHawks contest before diving into our next campaign.
The timing was an additional challenge but also a unique opportunity to showcase our skills. We jumped right in with an initial planning call and our signature Notion template, which we use for every smart contract auditing project. The goal was to efficiently organize and distribute tasks.
Moreover, there were multiple bug bounty reports and contests running at the same time, so we had to make a quick decision on which contest to prioritize. After evaluating the options, we chose the Beanstalk contest, as it seemed both interesting and aligned with our expertise in smart contract auditing. We wasted no time and got straight to work on identifying vulnerabilities.

Smart Contract Audit Tools and Testing Methods: Our Process and Best Practices

The project mainly used Hardhat, which was complex for testing, so we adapted it to Foundry. We used static analysis tools like Slitherin, Aderyn, Wake, and Olympix to get a first impression and study the weak points of the project.
Next, we began the manual analysis and testing campaign. During this phase, we analyzed each part of the contracts to understand the logic of each function. We verified if all invariants were met by conducting fuzz tests on each function. For this part, we used Foundry as our main tool.
Free Resource: check out our article "How to Write a Detector in Aderyn Step by Step" for more details on how to find bugs with Aderyn.
One of the fundamental pillars of our audits is the use of fuzzing tests. We use them because they allow us to explore countless possible scenarios that would take much longer with a manual method and would be much more complex. Of course, manual reviews are still necessary, and we use them to verify that everything works correctly.

We Won $13k?!

Before diving into the code (and the more technical aspect of this article), we would like to thank the CodeHawks team for their incredible work.
A few days before the final contest results were announced, we received an update showing a reward of 13,000 USDC. However, when we reached out to the Cyfrin team for confirmation, they kindly clarified that these weren’t the final results yet.
Read to the end to find out the final reward amount. 👇
Free Resource: Check out their latest website update - CodeHawks Update

Challenges and What's Next

Next, we will break down the contract and the function where we discovered the issue. You can either read it all or skip to the sections that interest you the most.

Understanding the Contract:

In this section, we will be breaking down the entire logic of the contract line by line.
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 Calculates the gaugePoints for whitelisted Silo LP tokens.
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 * is the default function to calculate the gauge points
29 * of an LP asset.
30 *
31 * @dev If % of deposited BDV is .01% within range of optimal,
32 * keep gauge points the same.
33 *
34 * Cap gaugePoints to MAX_GAUGE_POINTS to avoid runaway gaugePoints.
35 */
36 function defaultGaugePointFunction(
37 uint256 currentGaugePoints,
38 uint256 optimalPercentDepositedBdv,
39 uint256 percentOfDepositedBdv
40 ) external pure returns (uint256 newGaugePoints) {
41 if (
42 percentOfDepositedBdv >
43 optimalPercentDepositedBdv.mul(UPPER_THRESHOLD).div(THRESHOLD_PRECISION)
44 ) {
45 // gauge points cannot go below 0.
46 if (currentGaugePoints <= ONE_POINT) return 0;
47 newGaugePoints = currentGaugePoints.sub(ONE_POINT);
48 } else if (
49 percentOfDepositedBdv <
50 optimalPercentDepositedBdv.mul(LOWER_THRESHOLD).div(THRESHOLD_PRECISION)
51 ) {
52 newGaugePoints = currentGaugePoints.add(ONE_POINT);
53
54 // Cap gaugePoints to MAX_GAUGE_POINTS if it exceeds.
55 if (newGaugePoints > MAX_GAUGE_POINTS) return MAX_GAUGE_POINTS;
56 }
57 }
58}

Explanation of the defaultGaugePointFunction

The defaultGaugePointFunction is used to adjust the gauge points of Liquidity Provider (LP) tokens based on the percentage of Base Deposited Value (BDV) deposited.

Input Parameters

  • currentGaugePoints: The current gauge points of the LP token.
  • optimalPercentDepositedBdv: The optimal percentage of BDV that should be deposited.
  • percentOfDepositedBdv: The current percentage of BDV that is deposited.

Constants Used

  • ONE_POINT: Represents one gauge point (1e18).
  • MAX_GAUGE_POINTS: The maximum number of gauge points allowed (1000e18).
  • UPPER_THRESHOLD: Upper threshold (10001, representing 100.01%).
  • LOWER_THRESHOLD: Lower threshold (9999, representing 99.99%).
  • THRESHOLD_PRECISION: Threshold precision (10000).

Function Flow

The function has two main conditional blocks (if statements) that determine how to adjust the gauge points based on the percentage of BDV deposited.

Function Breakdown

Let's break down each block of the function to understand exactly what it does and when each part is executed.

1. Condition for Reducing Points

1if (
2 percentOfDepositedBdv >
3 optimalPercentDepositedBdv.mul(UPPER_THRESHOLD).div(THRESHOLD_PRECISION)
4)
Explanation:
Compares percentOfDepositedBdv with optimalPercentDepositedBdv adjusted by the UPPER_THRESHOLD (100.01% of the optimal).

Upper Threshold Calculation:
  • Multiplies optimalPercentDepositedBdv by UPPER_THRESHOLD (10001).
  • Divides the result by THRESHOLD_PRECISION (10000).

Example:
If optimalPercentDepositedBdv is 50:
150 × 10001 / 10000 = 50.005
The condition is:
1If percentOfDepositedBdv > 50.005, then it is met.

Action:
1if (currentGaugePoints <= ONE_POINT) return 0;
2newGaugePoints = currentGaugePoints.sub(ONE_POINT);
Explanation:
If currentGaugePoints is less than or equal to ONE_POINT, it is set to 0.
Otherwise, currentGaugePoints is reduced by ONE_POINT.

2. Condition for Increasing Points

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

Explanation:

Compares percentOfDepositedBdv with optimalPercentDepositedBdv adjusted by the LOWER_THRESHOLD (99.99% of the optimal).

Lower Threshold Calculation:

  • Multiplies optimalPercentDepositedBdv by LOWER_THRESHOLD (9999).
  • Divides the result by THRESHOLD_PRECISION (10000).

Example:

If optimalPercentDepositedBdv is 50:
[ 50 \times 9999 / 10000 = 49.995 ]
The condition is:
If percentOfDepositedBdv < 49.995, then it is met.

Action:

1newGaugePoints = currentGaugePoints.add(ONE_POINT);
2if (newGaugePoints > MAX_GAUGE_POINTS) return MAX_GAUGE_POINTS;
  • ONE_POINT is added to currentGaugePoints.
  • If newGaugePoints exceeds MAX_GAUGE_POINTS, it is set to MAX_GAUGE_POINTS.

What is the problem with this function?

The defaultGaugePointFunction has an issue in handling the adjustment of gauge points when the percentage of Base Deposited Value (BDV) deposited is exactly equal to the optimal percentage (optimalPercentDepositedBdv). In this case, the function lacks an explicit condition to manage this scenario, which can result in unintended behavior.

Detail of the Problem

  • Conditions Handle Inequality: The function has conditions to handle when percentOfDepositedBdv is greater than optimalPercentDepositedBdv adjusted by the UPPER_THRESHOLD, and when it is less than optimalPercentDepositedBdv adjusted by the LOWER_THRESHOLD.
  • Equality Case Not Handled: If percentOfDepositedBdv is exactly equal to optimalPercentDepositedBdv, neither condition is met, potentially leading to an unintended adjustment of gauge points to 0 instead of maintaining their current value.

What is the solution?

To solve this problem, an explicit condition needs to be added to handle the case where percentOfDepositedBdv is equal to optimalPercentDepositedBdv. This can be achieved by adding an else clause that returns the currentGaugePoints unchanged if none of the previous conditions are met.

Implementation of the Solution

Add the following condition at the end of the function:
1else {
2 return currentGaugePoints;
3}

Demonstrating the Problem with a POC

To demonstrate the problem effectively, we wrote two tests: a simple test case and a fuzz test. These tests show that the defaultGaugePointFunction has a problem when the percentage of BDV deposited is exactly equal to the optimal percentage.

Simple Test Case

This test specifically targets the scenario where percentOfDepositedBdv is equal to 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 exceed the maximum allowed");
13 assertEq(newGaugePoints, currentGaugePoints, "Gauge points adjustment does not match expected outcome");
14}

Setup:

  • currentGaugePoints is set to 1189.
  • optimalPercentDepositedBdv is set to 64.
  • percentOfDepositedBdv is set to 64.

Result Verification:

  • Expected: newGaugePoints should equal currentGaugePoints because percentOfDepositedBdv equals optimalPercentDepositedBdv.
  • Actual: Without the else condition, newGaugePoints could be 0, which is incorrect.

Fuzz Test

This test uses a range of values to ensure robustness and checks for the correct adjustment of 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 exceed the maximum allowed");
25 assertEq(newGaugePoints, expectedGaugePoints, "Gauge points adjustment does not match expected outcome");
26}

Expected Gauge Points Calculation:

  • Initially, expectedGaugePoints is set to currentGaugePoints.
  • Threshold Check:
    • If percentOfDepositedBdv is above the upper threshold, expectedGaugePoints is decreased by ONE_POINT or set to 0.
    • If percentOfDepositedBdv is below the lower threshold, expectedGaugePoints is increased by ONE_POINT, capped at MAX_GAUGE_POINTS.

Assertions:

  • Ensures newGaugePoints does not exceed MAX_GAUGE_POINTS.
  • Verifies newGaugePoints matches expectedGaugePoints.

Test Results

To run the tests correctly, follow these steps:
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 adjustment does not match expected outcome
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 adjustment does not match expected outcome")
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)

Final Bug Bounty Reward

Thank you for reading to the end (Or skipping here to know how much we received 😝).
In the end, we received a total of 8223.41 USDC. We had a great time working on it and the financial rewards were just one of the benefits.
Before we sign off, we encourage you to carefully study and research certain resources that are incredibly helpful for learning about smart contract auditing, fuzz testing, smart contract development, and the application of smart contract audit tools, among other things.
We have provided them in the table below.

Useful Resources

CATEGORYRESOURCE
ZEALYNXZealynx GitHub
Public Fuzzing Campaigns
YouTube Channel
NOTIONTomoLabo
I'm SunSec
matta.’s Ethereum security road-map
Security Researcher Dashboard
Audit Resources
Web3 Security DAO
Blockchain Security Guide
Smart Contract Verification
INFORMATIONOfficerCia
ConsenSys Diligence
Known Attacks
Smart Contract Best Practices
DASP
Solidity Security Blog
OfficerCia Blog
SWC Registry
ROADMAPTable of Contents
YouTube Video
GitHub Roadmap
COURSESUpdraft Cyfrin
Alchemy University
Secureum Substack
YAcademy
Smart Contract Hacking
YOUTUBEAndy Li
Spearbit
Trail of Bits
YAcademy DAO
Smart Contract Programmer
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
Smart Contract Hacking
Owen Thurm - How We Made Top 5 in a CodeHawks Audit

oog
zealynx

Subscribe to Our Newsletter

Stay updated with our latest security insights and blog posts

© 2024 Zealynx