29052 - [SC - Medium] Pool funds could be locked due to Division by zero
Submitted on Mar 5th 2024 at 22:04:05 UTC by @DuckAstronomer for Boost | ZeroLend
Report ID: #29052
Report type: Smart Contract
Report severity: Medium
Target: https://github.com/zerolend/governance
Impacts:
Theft of unclaimed yield
Temporary freezing of funds for at least 1 hour
Description
Vulnerability Details
Affected asset: governance-main/contracts/voter/gauge/GaugeIncentiveController.sol
The rewardPerToken()
function in GaugeIncentiveController
verifies totalSupply
for zero, but utilizes derivedSupply
for calculation of the rewardPerToken value. If derivedSupply
equals zero, rewardPerToken()
reverts due to division by zero.
https://github.com/zerolend/governance/blob/main/contracts/voter/gauge/GaugeIncentiveController.sol#L48
The handleAction()
function in GaugeIncentiveController
serves as a callback for AToken to invoke upon changes in the user's balance (such as minting or burning). Within handleAction()
, it initially triggers _updateReward()
, which subsequently calls rewardPerToken()
.
https://github.com/zerolend/governance/blob/main/contracts/voter/gauge/GaugeIncentiveController.sol#L87
https://github.com/zerolend/governance/blob/main/contracts/voter/gauge/GaugeIncentiveController.sol#L109
https://github.com/zerolend/governance/blob/main/contracts/voter/gauge/GaugeIncentiveController.sol#L117
In scenarios where totalSupply
is not equal to zero, and if derivedSupply
does equal zero, any alterations in AToken (e.g., minting or burning) will result in a revert. This effectively locks the user's funds.
The value of derivedSupply
is determined through governance-main/contracts/voter/eligibility/EligibilityCriteria.sol
.
https://github.com/zerolend/governance/blob/main/contracts/voter/gauge/GaugeIncentiveController.sol#L68
For instance, to qualify, a user must mint AToken and stake over 5% of ZeroLend tokens. https://github.com/zerolend/governance/blob/main/contracts/voter/eligibility/EligibilityCriteria.sol#L50
In summary, an attacker could block other users' funds and receive rewards from the gauge by being the first to invoke handleAction()
or updateUser()
.
The attack scenario is the following:
A pool is assigned a new gauge (gauge change occurs). At that point
totalSupply
andderivedSupply
are 0.The attacker becomes the first AToken minter after gauge change (or just calls
updateUser(address who)
).Attacker doesn't stake 5% of ZeroLend, so
totalSupply > 0
, butderivedSupply == 0
.Subsequent AToken actions within the pool will trigger reverts.
Consequently, the attacker locks users' funds in the pool and earns rewards from the gauge as the sole staker.
Proof of Concept
To run the Poc:
Put the code from below to the
governance-main/test/Gauge.poc.2.ts
file.Generate random private key.
Modify
governance-main/contracts/voter/eligibility/MockEligibilityCriteria.sol
file so thecheckEligibility()
function returns 0 instead of 1e18.Issue the following command:
PoC scenario:
Ant is the first who mints AToken in a pool with new Gauge.
Ant isn't eligible for reward, but it has AToken amount. So,
totalSupply > 0
, butderivedSupply == 0
.Whale aren't able to mint AToken due to division by zero panic.