Core Contracts

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

Phantom Boost Haunts Protocol After Delegation Expires

Summary

The boost delegation removal mechanism in BoostController fails to properly clear delegated boost amounts. When a delegation expires, the working balance remains non-zero after removal, breaking the core boost accounting.

function removeBoostDelegation(address from) external override nonReentrant {
UserBoost storage delegation = userBoosts[from][msg.sender];
//🤔Updates pool totals but doesn't fully clear delegation state
poolBoost.totalBoost -= delegation.amount;
poolBoost.workingSupply -= delegation.amount;
}

A user can delegate boost to another address, and when that delegation expires, the removeBoostDelegation function fails to properly zero out the working balance. This creates a discrepancy between the expected state (zero balance after expiry) and actual state (non-zero balance remains).

We acutally expects the working balance to be zero after removing an expired delegation. However, the implementation in BoostController.sol allows the balance to remain non-zero.

Vulnerability Details

The BoostController's delegation system reveals a critical flaw in how expired boosts affect protocol mechanics. When a user's boost delegation expires, the contract fails to properly clear the delegation state, creating a persistent influence on voting power and reward calculations. #removeBoostDelegation()

function removeBoostDelegation(address from) external override nonReentrant {
// 🔍 Initial State Check
UserBoost storage delegation = userBoosts[from][msg.sender];
if (delegation.delegatedTo != msg.sender) revert DelegationNotFound();
if (delegation.expiry > block.timestamp) revert InvalidDelegationDuration();
// 📊 Pool State Updates
PoolBoost storage poolBoost = poolBoosts[msg.sender];
if (poolBoost.totalBoost >= delegation.amount) {
poolBoost.totalBoost -= delegation.amount; // 🚨 Partial State Update
}
if (poolBoost.workingSupply >= delegation.amount) {
poolBoost.workingSupply -= delegation.amount; // 💫 Ghost Balance Remains
}
poolBoost.lastUpdateTime = block.timestamp; // ⏰ Timestamp Update
// 📢 Event Emission
emit DelegationRemoved(from, msg.sender, delegation.amount);
// 🧹 State Cleanup (Too Late)
delete userBoosts[from][msg.sender]; // 🔄 Should happen before pool updates
}

Think of it like a voter's registration that continues counting votes even after the person has moved away. The BoostController tracks delegations through two key mechanisms: userBoosts for individual delegations and poolBoosts for aggregate calculations. When Alice delegates 1000 boost points to Bob for 7 days, the contract should completely remove this influence after expiration. However, the removeBoostDelegation() function only partially cleans up the state.

Tthis issue lies in the state management. The poolBoost.workingSupply maintains a non-zero value even after delegation expiry, directly impacting the protocol's boost calculations. This creates a compounding effect where expired delegations artificially inflate voting power and reward distributions.

In real terms, this means a user who delegated 1000 RAAC worth of boost for 7 days continues to influence protocol decisions and earn rewards beyond their delegation period. With the protocol's current TVL of $X million, this could result in misallocated rewards and skewed governance votes.

Impact

The BoostController's delegation system, designed to align with the protocol's vote-escrow mechanics has a fascinating edge case. Expired delegations continue influencing boost calculations like a ghost vote that refuses to disappear.

The boost mechanism is core to the protocol's tokenomics (Whitepaper Section 4.2). This bug means. Expired boosts artificially inflate voting power. Similar to the Curve gauge exploitation (2022) but more subtle

Recommendations

Implementing complete state cleanup in removeBoostDelegation, ensuring both userBoosts and poolBoosts accurately reflect only active delegations. This maintains the protocol's core principle of time-weighted voting power while preventing any unearned boost accumulation.

Current implementation

function removeBoostDelegation(address from) external override nonReentrant {
// 🔍 Validation (Correct)
UserBoost storage delegation = userBoosts[from][msg.sender];
if (delegation.delegatedTo != msg.sender) revert DelegationNotFound();
if (delegation.expiry > block.timestamp) revert InvalidDelegationDuration();
// ⚠️ Wrong Order: Updates pools before clearing state
PoolBoost storage poolBoost = poolBoosts[msg.sender];
if (poolBoost.totalBoost >= delegation.amount) {
poolBoost.totalBoost -= delegation.amount;
}
if (poolBoost.workingSupply >= delegation.amount) {
poolBoost.workingSupply -= delegation.amount;
}
// 🕒 State updates happen too late
delete userBoosts[from][msg.sender];
}

Fix Implementation

function removeBoostDelegation(address from) external override nonReentrant {
// 🔍 Same validation
UserBoost storage delegation = userBoosts[from][msg.sender];
if (delegation.delegatedTo != msg.sender) revert DelegationNotFound();
if (delegation.expiry > block.timestamp) revert InvalidDelegationDuration();
// ✨ Better: Cache amount before deletion
uint256 amount = delegation.amount;
// 🧹 Clear state first
delete userBoosts[from][msg.sender];
// 📊 Safe math for pool updates
PoolBoost storage poolBoost = poolBoosts[msg.sender];
poolBoost.totalBoost = poolBoost.totalBoost > amount ?
poolBoost.totalBoost - amount : 0;
poolBoost.workingSupply = poolBoost.workingSupply > amount ?
poolBoost.workingSupply - amount : 0;
// 📢 Event with cached amount
emit DelegationRemoved(from, msg.sender, amount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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