Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: medium
Valid

Working Supply Overwrite Vulnerability in BoostController

Summary

The updateUserBoost function in the BoostController contract incorrectly overwrites workingSupply of the entire pool with the newBoost value of the most recent user's update. This is not the intended design, the intended design is to overwrite the working supply of that particular user not the entire pool. This behavior does not accurately track the cumulative workingSupply of the pool, as it disregards the contributions of other users. Additionally, there is no restriction or time lock on when this function can be called, allowing users to repeatedly replenish lost boosts (e.g., from delegation) and redelegate them, potentially leading to manipulation of the pool's rewards distribution.

Affected code: [BoostController::updateUserBoost](https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/boost/BoostController.sol#L198)

Vulnerability Details

The issue lies in the following line of the updateUserBoost function where this line retrieves the boost details of the pool

PoolBoost storage poolBoost = poolBoosts[pool];

and this line overwrites the workingSupply of the pool with the newBoost value of the user being updated, instead of adjusting it incrementally. As a result, the workingSupply of the pool no longer reflects the cumulative boost contributions of all users. Instead, it only reflects the boost amount of the most recently updated user.

poolBoost.workingSupply = newBoost; // Set working supply directly to new boost

Furthermore, the function lacks any restrictions or time locks on when it can be called. This means that a user can repeatedly call the function to replenish lost boosts (e.g., from delegation) and redelegate them, potentially manipulating the pool's rewards distribution.

Impact

  1. Incorrect Pool Accounting:

    • The workingSupply of the pool is no longer accurate, as it does not account for the working supply boost contributions of all users. This can lead to incorrect calculations and rewards distribution within the pool.

  2. Loss of User Contributions:

    • When a user updates their boost, the contributions of other users are effectively erased, as the workingSupply is overwritten. This can lead to unfair rewards distribution and potential loss of funds for users.

  3. Potential Exploitation:

    • An attacker could repeatedly call the updateUserBoost function to overwrite the workingSupply with their own boost amount, potentially manipulating the pool's rewards distribution.

  4. No Time Lock or Restrictions:

    • The lack of restrictions or time locks on the function allows users to repeatedly replenish lost boosts and redelegate them, leading to potential manipulation of the pool's rewards distribution.

Proof of Concept (PoC)

The following test case demonstrates the issue:

describe("Working Supply Bug", () => {
it.only("should demonstrate working supply overwrite bug", async () => {
// Initial setup
await veToken.mint(user1.address, ethers.parseEther("4000"));
await veToken.mint(user2.address, ethers.parseEther("1000"));
// Update boost for user1
await boostController.connect(user1).updateUserBoost(user1.address, mockPool.getAddress());
// Get user1's boost amount and working supply
const [user1BoostBps, user1BoostAmount] = await boostController.calculateBoost(
user1.address,
mockPool.getAddress(),
10000
);
const poolBoostAfterUser1 = await boostController.getPoolBoost(mockPool.getAddress());
console.log("After User1 Update:");
console.log("User1 Boost Amount:", user1BoostAmount.toString());
console.log("Pool Working Supply:", poolBoostAfterUser1.workingSupply.toString());
// Working supply should equal user1's boost
expect(poolBoostAfterUser1.workingSupply).to.equal(user1BoostAmount);
// Update boost for user2
await boostController.connect(user2).updateUserBoost(user2.address, mockPool.getAddress());
// Get user2's boost amount and new working supply
const [user2BoostBps, user2BoostAmount] = await boostController.calculateBoost(
user2.address,
mockPool.getAddress(),
10000
);
const poolBoostAfterUser2 = await boostController.getPoolBoost(mockPool.getAddress());
console.log("\nAfter User2 Update:");
console.log("User2 Boost Amount:", user2BoostAmount.toString());
console.log("Pool Working Supply:", poolBoostAfterUser2.workingSupply.toString());
// Log user1's working balance after user2's boost
const workingBalanceUser1AfterUser2 = await boostController.getWorkingBalance(user1.address, mockPool.getAddress());
console.log("User1 Working Balance After User2 Update:", workingBalanceUser1AfterUser2.toString());
// Working supply is overwritten with user2's boost instead of being cumulative
expect(poolBoostAfterUser2.workingSupply).to.equal(user2BoostAmount);
const expectedWorkingSupply = user1BoostAmount + user2BoostAmount;
console.log("\nExpected Working Supply:", expectedWorkingSupply.toString());
console.log("Actual Working Supply:", poolBoostAfterUser2.workingSupply.toString());
});
});

Recommended Fix

To fix this issue, the workingSupply should be updated incrementally based on the difference between the new and old boost amounts, similar to how totalBoost is updated. Additionally, a time lock or restriction should be added to prevent users from repeatedly calling the function to manipulate the pool's rewards distribution. Here is the corrected code:

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;
// Calculate new boost based on current veToken balance
uint256 newBoost = _calculateBoost(user, pool, 10000); // Base amount
userBoost.amount = newBoost;
userBoost.lastUpdateTime = block.timestamp;
// Update pool totals safely
if (newBoost >= oldBoost) {
poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
poolBoost.workingSupply = poolBoost.workingSupply + (newBoost - oldBoost);
} else {
poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost);
poolBoost.workingSupply = poolBoost.workingSupply - (oldBoost - newBoost);
}
poolBoost.lastUpdateTime = block.timestamp;
emit BoostUpdated(user, pool, newBoost);
emit PoolBoostUpdated(pool, poolBoost.totalBoost, poolBoost.workingSupply);
}

2 Time Lock or Restriction:

  • A time lock or restriction should be added to prevent users from repeatedly calling the function to manipulate the pool's rewards distribution. This can be implemented by adding a cooldown period or limiting the number of times a user can call the function within a certain time frame.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

BoostController::updateUserBoost overwrites workingSupply with single user's boost value instead of accumulating, breaking reward multipliers and allowing last updater to capture all benefits

Support

FAQs

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

Give us feedback!