Table of contents
- Introduction
- Context
- The Time Problem
- Why We Chose the CodeHawks Contest Over Others
- Tools and Types of Tests Used
- Our Surprise When We Saw the Initial Results
- Challenges and What's Next
- Understanding the Contract:
- What is the problem with this function?
- What is the solution?
- Demonstrating the Problem with a POC
- Final Reward
- Resources
Introduction
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 and what we did to detect this error:
The intention of this article is to encourage people to delve into and participate in as many contests as possible. It's truly an opportunity to put into practice all the knowledge you have been learning over time.
At the end of the article, we will provide a series of resources and advice to help you as much as possible.
Context
We'd like to share a bit about the situation we were in before starting the contest.
On the very same day, we began looking into the contest, we had just finished an intense testing campaign (Check the result in our GitHub Repo). The reality was that we only had 2-3 days to participate in the contest before starting our next testing campaign.
This short time frame was an additional challenge, but it was also an opportunity to put our skills and acquired knowledge to the test.
The Time Problem
This was our biggest challenge. As we mentioned earlier, we only had 2-3 days to work on the contest. Even though we knew it would be quite difficult, we decided to go for it.
We had an initial call to plan things out. We started with our new Notion template, which we use by default for every new project, and organized ourselves in a way that allowed us to distribute the work as efficiently and structured as possible.
Why We Chose the CodeHawks Contest Over Others
During this short period, we had to decide which contest to participate in among all the ones available at the time.
The decision was to start with the Beanstalk contest because it seemed quite interesting and was a topic we were somewhat familiar with.
So, after making our decision, we got to work right away.
Tools and Types of Tests Used
We decided to use a series of tools for this test to cover different perspectives.
Firstly, the project mainly used Hardhat, which didn't make things easy when it came to writing tests in a more straightforward, powerful, and visual manner. Therefore, we adapted it to Foundry to be able to work with it minimally and effectively.
These are the tools we used in the contest:
Static Analysis Tools: We initially used tools like Slitherin, Aderyn, Wake, and Olympix to start studying the weak points of the project based on their data. This helped us get a first impression.
(For more details on how to find bugs with Aderyn check out our article "How to Write a Detector in Aderyn Step by Step")
Manual Analysis and Testing Campaign: Next, we began the manual analysis and testing campaign. During this phase, we analyzed each part of the contracts, understanding 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.
Why These Tools Are Important to Us
For us, one of the fundamental pillars of our audits is the use of fuzzing tests. Once we started using them in our audits, it marked a significant improvement. These types of tests allow us to explore countless possible scenarios that would take much longer with a manual method and would be much more complex.
This doesn't mean that the manual review is unnecessary. On the contrary, we first review the protocol manually and then use these tests to verify that everything works correctly.
Our Surprise When We Saw the Initial Results
This part was quite funny. A few days before the final contest results were announced, we received an update on our profile indicating that we had won a reward of 13,000 USDC. Naturally, we were thrilled and quickly shared the news among ourselves. We immediately reached out to the Cyfrin team to confirm, and they were very attentive and kind.
However, they explained that, unfortunately, those were not the final results yet and that the last count still needed to be done before the final outcome was announced.
So, we decided to wait a few more days for the final results.
At the end of this article, we will reveal the final reward amount we received.
CodeHawks Team
Before diving into the code, we would like to thank the CodeHawks team for their incredible work. They were very attentive and treated us exceptionally well with a personal and pleasant approach.
We also want to invite everyone to check out the new update on their website. It is now much more visual, pleasant, and beautiful, greatly enhancing the user experience.
Challenges and What's Next
Next, we will break down the contract and the function where we discovered the issue. We invite you to jump to the section that interests you most from the following points:
Understanding the Contract
What is the problem with this function?
What is the solution?
Demonstrating the Problem with a POC
Test Results
Final Reward
Understanding the Contract:
In this section, we will be breaking down the entire logic of the contract line by line to simplify and explain it as clearly as possible.
/*
* SPDX-License-Identifier: MIT
*/
pragma solidity =0.7.6;
pragma experimental ABIEncoderV2;
import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol";
import {LibGauge} from "contracts/libraries/LibGauge.sol";
/**
* @title GaugePointFacet
* @author Brean
* @notice Calculates the gaugePoints for whitelisted Silo LP tokens.
*/
contract GaugePointFacet {
using SafeMath for uint256;
uint256 private constant ONE_POINT = 1e18;
uint256 private constant MAX_GAUGE_POINTS = 1000e18;
uint256 private constant UPPER_THRESHOLD = 10001;
uint256 private constant LOWER_THRESHOLD = 9999;
uint256 private constant THRESHOLD_PRECISION = 10000;
/**
* @notice DefaultGaugePointFunction
* is the default function to calculate the gauge points
* of an LP asset.
*
* @dev If % of deposited BDV is .01% within range of optimal,
* keep gauge points the same.
*
* Cap gaugePoints to MAX_GAUGE_POINTS to avoid runaway gaugePoints.
*/
function defaultGaugePointFunction(
uint256 currentGaugePoints,
uint256 optimalPercentDepositedBdv,
uint256 percentOfDepositedBdv
) external pure returns (uint256 newGaugePoints) {
if (
percentOfDepositedBdv >
optimalPercentDepositedBdv.mul(UPPER_THRESHOLD).div(THRESHOLD_PRECISION)
) {
// gauge points cannot go below 0.
if (currentGaugePoints <= ONE_POINT) return 0;
newGaugePoints = currentGaugePoints.sub(ONE_POINT);
} else if (
percentOfDepositedBdv <
optimalPercentDepositedBdv.mul(LOWER_THRESHOLD).div(THRESHOLD_PRECISION)
) {
newGaugePoints = currentGaugePoints.add(ONE_POINT);
// Cap gaugePoints to MAX_GAUGE_POINTS if it exceeds.
if (newGaugePoints > MAX_GAUGE_POINTS) return MAX_GAUGE_POINTS;
}
}
}
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
if (
percentOfDepositedBdv >
optimalPercentDepositedBdv.mul(UPPER_THRESHOLD).div(THRESHOLD_PRECISION)
)
Explanation:
Compares
percentOfDepositedBdv
withoptimalPercentDepositedBdv
adjusted by theUPPER_THRESHOLD
(100.01% of the optimal).Upper Threshold Calculation:
Multiplies
optimalPercentDepositedBdv
byUPPER_THRESHOLD
(10001).Divides the result by
THRESHOLD_PRECISION
(10000).
Example:
If optimalPercentDepositedBdv
is 50: [ 50 \times 10001 / 10000 = 50.005 ]
The condition is: If percentOfDepositedBdv
> 50.005, then it is met.
Action:
if (currentGaugePoints <= ONE_POINT) return 0;
newGaugePoints = currentGaugePoints.sub(ONE_POINT);
If
currentGaugePoints
is less than or equal toONE_POINT
, it is set to 0.Otherwise,
currentGaugePoints
is reduced byONE_POINT
.
2. Condition for Increasing Points
else if (
percentOfDepositedBdv <
optimalPercentDepositedBdv.mul(LOWER_THRESHOLD).div(THRESHOLD_PRECISION)
)
Explanation:
Compares
percentOfDepositedBdv
withoptimalPercentDepositedBdv
adjusted by theLOWER_THRESHOLD
(99.99% of the optimal).Lower Threshold Calculation:
Multiplies
optimalPercentDepositedBdv
byLOWER_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:
newGaugePoints = currentGaugePoints.add(ONE_POINT);
if (newGaugePoints > MAX_GAUGE_POINTS) return MAX_GAUGE_POINTS;
ONE_POINT
is added tocurrentGaugePoints
.If
newGaugePoints
exceedsMAX_GAUGE_POINTS
, it is set toMAX_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 thanoptimalPercentDepositedBdv
adjusted by theUPPER_THRESHOLD
, and when it is less thanoptimalPercentDepositedBdv
adjusted by theLOWER_THRESHOLD
.Equality Case Not Handled: If
percentOfDepositedBdv
is exactly equal tooptimalPercentDepositedBdv
, 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:
else {
return currentGaugePoints;
}
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
.
function testnew_GaugePointAdjustment() public {
uint256 currentGaugePoints = 1189;
uint256 optimalPercentDepositedBdv = 64;
uint256 percentOfDepositedBdv = 64;
uint256 newGaugePoints = gaugePointFacet.defaultGaugePointFunction(
currentGaugePoints,
optimalPercentDepositedBdv,
percentOfDepositedBdv
);
assertTrue(newGaugePoints <= MAX_GAUGE_POINTS, "New gauge points exceed the maximum allowed");
assertEq(newGaugePoints, currentGaugePoints, "Gauge points adjustment does not match expected outcome");
}
Setup:
currentGaugePoints
is set to 1189.optimalPercentDepositedBdv
is set to 64.percentOfDepositedBdv
is set to 64.
Result Verification:
Expected:
newGaugePoints
should equalcurrentGaugePoints
becausepercentOfDepositedBdv
equalsoptimalPercentDepositedBdv
.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.
function testGaugePointAdjustmentUnifiedFuzzing(
uint256 currentGaugePoints,
uint256 optimalPercentDepositedBdv,
uint256 percentOfDepositedBdv
) public {
currentGaugePoints = bound(currentGaugePoints, 1, MAX_GAUGE_POINTS - 1);
optimalPercentDepositedBdv = bound(optimalPercentDepositedBdv, 1, 100);
percentOfDepositedBdv = bound(percentOfDepositedBdv, 1, 100);
uint256 expectedGaugePoints = currentGaugePoints;
if (percentOfDepositedBdv * THRESHOLD_PRECISION > optimalPercentDepositedBdv * UPPER_THRESHOLD) {
expectedGaugePoints = currentGaugePoints > ONE_POINT ? currentGaugePoints - ONE_POINT : 0;
} else if (percentOfDepositedBdv * THRESHOLD_PRECISION < optimalPercentDepositedBdv * LOWER_THRESHOLD) {
expectedGaugePoints = currentGaugePoints + ONE_POINT <= MAX_GAUGE_POINTS ? currentGaugePoints + ONE_POINT : MAX_GAUGE_POINTS;
}
uint256 newGaugePoints = gaugePointFacet.defaultGaugePointFunction(
currentGaugePoints,
optimalPercentDepositedBdv,
percentOfDepositedBdv
);
assertTrue(newGaugePoints <= MAX_GAUGE_POINTS, "New gauge points exceed the maximum allowed");
assertEq(newGaugePoints, expectedGaugePoints, "Gauge points adjustment does not match expected outcome");
}
Expected Gauge Points Calculation:
Initially,
expectedGaugePoints
is set tocurrentGaugePoints
.Threshold Check:
If
percentOfDepositedBdv
is above the upper threshold,expectedGaugePoints
is decreased byONE_POINT
or set to 0.If
percentOfDepositedBdv
is below the lower threshold,expectedGaugePoints
is increased byONE_POINT
, capped atMAX_GAUGE_POINTS
.
Assertions:
Ensures
newGaugePoints
does not exceedMAX_GAUGE_POINTS
.Verifies
newGaugePoints
matchesexpectedGaugePoints
.
Test Results
To run the tests correctly, follow these steps:
export FORKING_RPC=https://eth-mainnet.g.alchemy.com/v2/{API}
forge test --mc GaugePointFacetTest --mt testGaugesssPointAdjustmentUnifiedFuzzing -vvv
[FAIL. Reason: assertion failed; counterexample: calldata=0xdace5ffa0000000000000000000000000465ff2e9c9fd7f2b5a78cfaa0671d046c517d5d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 args=[25110567842437950750745261937466550563734912349 [2.511e46], 0, 1]] testGaugesssPointAdjustmentUnifiedFuzzing(uint256,uint256,uint256) (runs: 0, μ: 0, ~: 0)
Logs:
Bound Result 505308988514485682721
Bound Result 1
Bound Result 1
Error: Gauge points adjustment does not match expected outcome
Error: a == b not satisfied [uint]
Left: 0
Right: 505308988514485682721
Traces:
[30286] DefaultTestContract::testGaugesssPointAdjustmentUnifiedFuzzing(25110567842437950750745261937466550563734912349 [2.511e46], 0, 1)
├─ [0] console::log("Bound Result", 505308988514485682721 [5.053e20]) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("Bound Result", 1) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("Bound Result", 1) [staticcall]
│ └─ ← [Stop]
├─ [826] GaugePointFacet::defaultGaugePointFunction(505308988514485682721 [5.053e20], 1, 1) [staticcall]
│ └─ ← [Return] 0
├─ emit log_named_string(key: "Error", val: "Gauge points adjustment does not match expected outcome")
├─ emit log(val: "Error: a == b not satisfied [uint]")
├─ emit log_named_uint(key: " Left", val: 0)
├─ emit log_named_uint(key: " Right", val: 505308988514485682721 [5.053e20])
├─ [0] VM::store(VM: [0x7109709ECfa91a80626fF3989D68f67F5b1DD12D], 0x6661696c65640000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000001)
│ └─ ← [Return]
└─ ← [Stop]
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 9.52ms (3.61ms CPU time)
Final Reward
I know many of you probably skipped most of the article to see the final reward amount 😝.
In the end, we received a total of 8223.41 USDC, an amount that motivated us even more to continue doing this kind of work.
It was truly an enriching and very interesting experience.
Resources
As the final part of the test, we encourage you to carefully study and research certain resources that, in our opinion, can be very helpful for learning about auditing, testing, development, and the application of tools, among other things.
We hope you enjoyed the article as much as we enjoyed writing it.