Finding description and impact
The updateUserBoost
function is publicly accessible and can be called by anyone. This means that:
A malicious actor can repeatedly call this function on behalf of another user, causing unnecessary gas fees.
Spamming updateUserBoost
could lead to increased gas costs for users when they perform legitimate transactions.
The contract’s gas efficiency could be impacted if multiple users are constantly forced into unnecessary state updates.
Impact:
Denial-of-service (DoS) via gas exhaustion: Attackers can spam this function to make users waste gas.
Unwanted user experience degradation: Users will notice frequent, unexpected boosts being updated on their behalf.
Potential chain congestion: If an attacker automates calling updateUserBoost
for multiple users, it could result in unnecessary blockchain load.
Relevant Code
The function updateUserBoost
is currently external
and lacks a restriction on who can call it:
function updateUserBoost(address user, address pool) external override nonReentrant whenNotPaused {
if (paused()) revert EmergencyPaused();
if (user == address(0)) revert InvalidPool();
if (!supportedPools[pool]) revert PoolNotSupported();
UserBoost storage userBoost = userBoosts[user][pool];
PoolBoost storage poolBoost = poolBoosts[pool];
uint256 oldBoost = userBoost.amount;
uint256 newBoost = _calculateBoost(user, pool, 10000);
userBoost.amount = newBoost;
userBoost.lastUpdateTime = block.timestamp;
if (newBoost >= oldBoost) {
poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
} else {
poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost);
}
poolBoost.workingSupply = newBoost;
poolBoost.lastUpdateTime = block.timestamp;
emit BoostUpdated(user, pool, newBoost);
emit PoolBoostUpdated(pool, poolBoost.totalBoost, poolBoost.workingSupply);
}
Attack Scenario
Gas Drain Attack: A malicious bot can repeatedly call updateUserBoost(user, pool)
, forcing a user’s boost to be recalculated unnecessarily, draining gas.
User Frustration: If a user notices unexpected updates and gas fees, they may lose trust in the system.
Potential Network Congestion: If spammed at scale, it could lead to network bloat and unnecessary state changes.
Recommended Mitigation Steps
To prevent arbitrary external actors from calling updateUserBoost
on behalf of users, restrict access:
Option 1: Restrict Calls to the User Themselves or an Authorized Role
Modify the function to allow only the user or an authorized role (e.g., MANAGER_ROLE
) to call it:
function updateUserBoost(address user, address pool) external override nonReentrant whenNotPaused {
if (msg.sender != user && !hasRole(MANAGER_ROLE, msg.sender)) revert Unauthorized();
if (paused()) revert EmergencyPaused();
if (user == address(0)) revert InvalidPool();
if (!supportedPools[pool]) revert PoolNotSupported();
UserBoost storage userBoost = userBoosts[user][pool];
PoolBoost storage poolBoost = poolBoosts[pool];
uint256 oldBoost = userBoost.amount;
uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
uint256 newBoost = _calculateBoost(user, pool, userBalance);
userBoost.amount = newBoost;
userBoost.lastUpdateTime = block.timestamp;
if (newBoost >= oldBoost) {
poolBoost.totalBoost += (newBoost - oldBoost);
} else {
poolBoost.totalBoost -= (oldBoost - newBoost);
}
poolBoost.workingSupply = newBoost;
poolBoost.lastUpdateTime = block.timestamp;
emit BoostUpdated(user, pool, newBoost);
emit PoolBoostUpdated(pool, poolBoost.totalBoost, poolBoost.workingSupply);
}
Option 2: Add a Cooldown Period to Prevent Spamming
If the function is intended to be open-access, implement a time delay (e.g., 1 hour) using lastUpdateTime
before allowing another update:
uint256 public constant BOOST_UPDATE_DELAY = 1 hours;
function updateUserBoost(address user, address pool) external override nonReentrant whenNotPaused {
if (msg.sender != user && !hasRole(MANAGER_ROLE, msg.sender)) revert Unauthorized();
UserBoost storage userBoost = userBoosts[user][pool];
if (block.timestamp < userBoost.lastUpdateTime + BOOST_UPDATE_DELAY) revert UpdateTooFrequent();
if (paused()) revert EmergencyPaused();
if (user == address(0)) revert InvalidPool();
if (!supportedPools[pool]) revert PoolNotSupported();
PoolBoost storage poolBoost = poolBoosts[pool];
uint256 oldBoost = userBoost.amount;
uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
uint256 newBoost = _calculateBoost(user, pool, userBalance);
userBoost.amount = newBoost;
userBoost.lastUpdateTime = block.timestamp;
if (newBoost >= oldBoost) {
poolBoost.totalBoost += (newBoost - oldBoost);
} else {
poolBoost.totalBoost -= (oldBoost - newBoost);
}
poolBoost.workingSupply = newBoost;
poolBoost.lastUpdateTime = block.timestamp;
emit BoostUpdated(user, pool, newBoost);
emit PoolBoostUpdated(pool, poolBoost.totalBoost, poolBoost.workingSupply);
}