stake.link

stake.link
DeFiHardhatBridge
27,500 USDC
View results
Submission Details
Severity: high
Invalid

Potential Division-by-Zero error in LinearBoostController::getBoostAmount, resulting in unexpected rewards and disruption of intented logic

Summary

A potential division by zero vulnerability exists in the getBoostAmount function.
The function calculates boost amounts using the following formula:

(_amount * uint256(maxBoost) * uint256(_lockingDuration)) / uint256(maxLockingDuration)

However, it lacks a check for zero before performing the division. If maxBoost and/or maxLockingDuration is set to zero, either externally through the setMaxBoost and setMaxLockingDuration function respectively or in future contract updates, the division will result in an error, potentially halting the contract's execution and leading to financial losses.

Vulnerability Details

  1. setMaxBoost:

Comment for setMaxBoost:

// a multiplier of 1 would mean that a staker's balance is doubled if they lock for the max locking duration

Code for getBoostAmount:

function getBoostAmount(uint256 _amount, uint64 _lockingDuration) external view returns (uint256) {
if (_lockingDuration > maxLockingDuration) revert MaxLockingDurationExceeded();
@> return (_amount * uint256(maxBoost) * uint256(_lockingDuration)) / uint256(maxLockingDuration);
}

The comment suggests that a maxBoost of 1 doubles the staker's balance, but it doesn't specify what happens when maxBoost is 0.
The code simply multiplies _amount by maxBoost and _lockingDuration, and then divides by maxLockingDuration.
If maxBoost is 0, the entire calculation becomes 0, regardless of the other values.

  1. setMaxLockingDuration

Same as setMaxBoost. The comment doesn't explicitly state what happens when maxLockingDuration is zero.
Thus using the value of maxLockingDuration after running this function, might result in getBoostAmount returning unexpected value.

  1. POC (made using Foundry) :

// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
pragma experimental ABIEncoderV2;
import "forge-std/Test.sol";
import {LinearBoostController} from "../src/LinearBoostController.sol";
contract LinearBoostControllerTest is Test {
LinearBoostController private controller;
address private owner;
function setUp() public {
owner = address(this); // Test contract is the owner
controller = new LinearBoostController(365 days, 2); // Initialize with some values
}
//////////
/// EXPECTING THIS TEST TO RESULT IN 0. SINCE DIVIDING 0 BY ANY NUMBER RESULTS IN ZERO.
//////////
function testDivisionByZero_ForMaxBoost() public {
// Setting maxBoost to zero to simulate the vulnerability
vm.prank(owner); // Simulate the call from the owner's address
controller.setMaxBoost(0);
// Now attempt to call getBoostAmount, which should fail due to zero getting divided:
uint256 amount = 1000; // Example token amount
uint64 lockingDuration = 100; // Example locking duration
// Storing the value from boostAmount, to check if we get 0:
uint256 boostAmount = controller.getBoostAmount(amount, lockingDuration);
// Asserting that the result is indeed 0:
assertEq(boostAmount, 0);
}
//////////
/// EXPECTING THIS TO REVERT. SINCE A NUMBER CANNOT BE DIVIDED BY ZERO.
//////////
function testDividingByZero_ForMaxLockingDuration() public {
// Setting maxLockingDuration to zero to simulate the vulnerability
vm.prank(owner); // Simulate the call from the owner's address
controller.setMaxLockingDuration(0);
// Now attempt to call getBoostAmount, which should fail due to division by Zero:
uint256 amount = 1000; // Example token amount
uint64 lockingDuration = 100; // Example locking duration
// Expect a revert due to division by Zero:
vm.expectRevert();
controller.getBoostAmount(amount, lockingDuration);
}
}

Impact

If maxBoost and/or maxLockingDuration unexpectedly becomes 0, it could disrupt the protocol's reward calculations and distribution, leading to unexpected results and potential economic losses.

Tools Used

Manual Review, AI

Recommendations

  • Explicitly Handle Zero maxBoost, maxLockingDuration:
    Add a check within getBoostAmount to return 0 if getBoostAmount is 0, clarifying the behavior.

function getBoostAmount(uint256 _amount, uint64 _lockingDuration) external view returns (uint256) {
if (_lockingDuration > maxLockingDuration) revert MaxLockingDurationExceeded();
+ if (maxBoost == 0 || maxLockingDuration == 0) {
+ return 0; // Explicitly handle zero values
+ }
return (_amount * uint256(maxBoost) * uint256(_lockingDuration)) / uint256(maxLockingDuration);
}

The code first checks if either maxBoost or maxLockingDuration is equal to zero. If either condition is true, it immediately returns 0, explicitly indicating that no boost is applicable in these cases.

This approach makes the contract's behavior more transparent and predictable, even when these values are zero.

  • Prevent Zero maxBoost, maxLockingDuration:
    Add a check within setMaxBoost, and setMaxLockingDuration to revert if _maxBoost and _maxLockingDuration is 0, ensuring it's never set to 0.

function setMaxBoost(uint64 _maxBoost) external onlyOwner {
+ require(_maxBoost> 0, "_maxBoostcannot be zero");
maxBoost = _maxBoost;
emit SetMaxBoost(_maxBoost);
}

and

function setMaxLockingDuration(uint64 _maxLockingDuration) external onlyOwner {
+ require(_maxLockingDuration> 0, "_maxLockingDuration cannot be zero");
maxLockingDuration = _maxLockingDuration;
emit SetMaxLockingDuration(_maxLockingDuration);
}
  • Clarify Documentation:
    Update the comment to explicitly state the intended behavior when maxBoost and maxLockingDuration is 0.

Updates

Lead Judging Commences

0kage Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.