Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: medium
Invalid

Incorrect Distribution of Leftover Rewards Due to Unfair Allocation to Vaults

Summary

The _handleWethRewardDistribution function unfairly allocates 100% of leftover rewards to vaults instead of distributing them proportionally between the protocol and vaults based on their predefined shares (feeRecipientsSharesX18 and Constants.MAX_SHARES). This will destruct the receiving of collateral fees on the receiveMarketFeefunction and also on how the reward will be distributed to the market and protocol on the ConvertAccumulatedFeesToWethfunction.

Vulnerability Details

UD60x18 leftover = receivedWethX18.sub(receivedProtocolWethRewardX18).sub(receivedVaultsWethRewardX18);

Fixed-point arithmetic (UD60x18) can cause tiny rounding discrepancies, resulting in a leftover after initial reward distribution

The current implementation assigns the entire leftover to vaults

receivedVaultsWethRewardX18 = receivedVaultsWethRewardX18.add(leftover);

This ignores the protocol’s proportional claim to the leftover based on feeRecipientsSharesX18

Example Exploit Scenario

  • Parameters:

    • Constants.MAX_SHARES = 1e18 (UD60x18).

    • feeRecipientsSharesX18 = 0.3e18 (protocol’s 30% share).

    • receivedWethX18 = 1000e18.

    • Initial rewards:

      • Protocol: 1000e18 * 0.3e18 / 1e18 = 300e18.

      • Vaults: 1000e18 * (1e18 - 0.3e18) / 1e18 = 700e18.

    • Due to rounding, actual rewards might be:

      • Protocol: 299.999e18.

      • Vaults: 699.999e18.

    • leftover = 1000e18 - 299.999e18 - 699.999e18 = 2e18.

  • Current Outcome:

    • Vaults receive 699.999e18 + 2e18 = 701.999e18 (70.1999% of total).

    • Protocol receives 299.999e18 (29.9999% of total).

  • Expected Fair Distribution:

    • Protocol’s share of leftover: 2e18 * 0.3e18 / 1e18 = 0.6e18.

    • Vaults’ share of leftover: 2e18 - 0.6e18 = 1.4e18.

    • Final rewards:

      • Protocol: 299.999e18 + 0.6e18 = 300.599e18 (30.0599%).

      • Vaults: 699.999e18 + 1.4e18 = 701.399e18 (70.1399%).

Impact

Over time, the protocol loses its fair share of rewards, reducing its income.

Tools Used

Manual Code Review

Recommendations

Distribute the leftover proportionally using the original shares

function _handleWethRewardDistribution(
Market.Data storage market,
address assetOut,
UD60x18 receivedWethX18
)
internal
{
// cache the total fee recipients shares as UD60x18
UD60x18 feeRecipientsSharesX18 = ud60x18(MarketMakingEngineConfiguration.load().totalFeeRecipientsShares);
// calculate the weth rewards for protocol and vaults
UD60x18 receivedProtocolWethRewardX18 = receivedWethX18.mul(feeRecipientsSharesX18);
UD60x18 receivedVaultsWethRewardX18 =
receivedWethX18.mul(ud60x18(Constants.MAX_SHARES).sub(feeRecipientsSharesX18));
// calculate leftover reward
UD60x18 leftover = receivedWethX18.sub(receivedProtocolWethRewardX18).sub(receivedVaultsWethRewardX18); // The leftover needs to be distributed properly
+ // Calculate protocol’s share of the leftover
+ UD60x18 protocolLeftoverShare = leftover.mul(feeRecipientsSharesX18).div(ud60x18(Constants.MAX_SHARES));
+ UD60x18 vaultsLeftoverShare = leftover.sub(protocolLeftoverShare);
// Update rewards
+ receivedProtocolWethRewardX18 = receivedProtocolWethRewardX18.add(protocolLeftoverShare);
+ receivedVaultsWethRewardX18 = receivedVaultsWethRewardX18.add(vaultsLeftoverShare);
// add leftover reward to vault reward
- receivedVaultsWethRewardX18 = receivedVaultsWethRewardX18.add(leftover);
// adds the weth received for protocol and vaults rewards using the assets previously paid by the engine
// as fees, and remove its balance from the market's `receivedMarketFees` map
market.receiveWethReward(assetOut, receivedProtocolWethRewardX18, receivedVaultsWethRewardX18);
// recalculate markes' vaults credit delegations after receiving fees to push reward distribution
Vault.recalculateVaultsCreditCapacity(market.getConnectedVaultsIds());
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 6 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.