Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: high
Valid

The full rewards are repeatedly applied to all vaults connected to a market.

Summary

The full rewards are repeatedly applied to all vaults connected to a market.

Vulnerability Details

function receiveWethReward(
Data storage self,
address asset,
UD60x18 receivedProtocolWethRewardX18,
UD60x18 receivedVaultsWethRewardX18
)
internal
{
// if a market credit deposit asset has been used to acquire the received weth, we need to reset its balance
if (asset != address(0)) {
// removes the given asset from the received market fees enumerable map as we assume it's been fully
// swapped to weth
self.receivedFees.remove(asset);
}
// increment the amount of pending weth reward to be distributed to fee recipients
self.availableProtocolWethReward =
ud60x18(self.availableProtocolWethReward).add(receivedProtocolWethRewardX18).intoUint128();
// increment the all time weth reward storage
self.wethRewardPerVaultShare =
ud60x18(self.wethRewardPerVaultShare).add(receivedVaultsWethRewardX18).intoUint128();
}

As we can see, the wethRewardPerVaultShare value is the accumulated reward for this market.

function getVaultAccumulatedValues(
Data storage self,
UD60x18 vaultDelegatedCreditUsdX18,
SD59x18 lastVaultDistributedRealizedDebtUsdPerShareX18,
SD59x18 lastVaultDistributedUnrealizedDebtUsdPerShareX18,
UD60x18 lastVaultDistributedUsdcCreditPerShareX18,
UD60x18 lastVaultDistributedWethRewardPerShareX18
)
internal
view
returns (
SD59x18 realizedDebtChangeUsdX18,
SD59x18 unrealizedDebtChangeUsdX18,
UD60x18 usdcCreditChangeX18,
UD60x18 wethRewardChangeX18
)
{
// calculate the vault's share of the total delegated credit, from 0 to 1
296: UD60x18 vaultCreditShareX18 = vaultDelegatedCreditUsdX18.div(getTotalDelegatedCreditUsd(self));
...
realizedDebtChangeUsdX18 = !lastVaultDistributedRealizedDebtUsdPerShareX18.isZero()
? sd59x18(self.realizedDebtUsdPerVaultShare).sub(lastVaultDistributedRealizedDebtUsdPerShareX18).mul(
vaultCreditShareX18.intoSD59x18()
)
: SD59x18_ZERO;
unrealizedDebtChangeUsdX18 = !lastVaultDistributedUnrealizedDebtUsdPerShareX18.isZero()
? sd59x18(self.unrealizedDebtUsdPerVaultShare).sub(lastVaultDistributedUnrealizedDebtUsdPerShareX18).mul(
vaultCreditShareX18.intoSD59x18()
)
: SD59x18_ZERO;
usdcCreditChangeX18 = !lastVaultDistributedUsdcCreditPerShareX18.isZero()
? ud60x18(self.usdcCreditPerVaultShare).sub(lastVaultDistributedUsdcCreditPerShareX18).mul(
vaultCreditShareX18
)
: UD60x18_ZERO;
// TODO: fix the vaultCreditShareX18 flow to multiply by `wethRewardChangeX18`
wethRewardChangeX18 = ud60x18(self.wethRewardPerVaultShare).sub(lastVaultDistributedWethRewardPerShareX18);
}

The wethRewardChange value is the new accumulated reward for this market.

function _recalculateConnectedMarketsState(
Data storage self,
uint128[] memory connectedMarketsIdsCache,
bool shouldRehydrateCache
)
private
returns (
uint128[] memory rehydratedConnectedMarketsIdsCache,
SD59x18 vaultTotalRealizedDebtChangeUsdX18,
SD59x18 vaultTotalUnrealizedDebtChangeUsdX18,
UD60x18 vaultTotalUsdcCreditChangeX18,
UD60x18 vaultTotalWethRewardChangeX18
)
{
...
for (uint256 i; i < connectedMarketsIdsCache.length; i++) {
...
if (!market.getTotalDelegatedCreditUsd().isZero()) {
// get the vault's accumulated debt, credit and reward changes from the market to update its stored
// values
(
ctx.realizedDebtChangeUsdX18,
ctx.unrealizedDebtChangeUsdX18,
ctx.usdcCreditChangeX18,
326: ctx.wethRewardChangeX18
) = market.getVaultAccumulatedValues(
ud60x18(creditDelegation.valueUsd),
sd59x18(creditDelegation.lastVaultDistributedRealizedDebtUsdPerShare),
sd59x18(creditDelegation.lastVaultDistributedUnrealizedDebtUsdPerShare),
ud60x18(creditDelegation.lastVaultDistributedUsdcCreditPerShare),
ud60x18(creditDelegation.lastVaultDistributedWethRewardPerShare)
);
}
...
350: vaultTotalWethRewardChangeX18 = vaultTotalWethRewardChangeX18.add(ctx.wethRewardChangeX18);
// update the last distributed debt, credit and reward values to the vault's credit delegation to the
// given market id, in order to keep next calculations consistent
creditDelegation.updateVaultLastDistributedValues(
sd59x18(market.realizedDebtUsdPerVaultShare),
sd59x18(market.unrealizedDebtUsdPerVaultShare),
ud60x18(market.usdcCreditPerVaultShare),
ud60x18(market.wethRewardPerVaultShare)
);
}
}

When calculating the reward for a vault, all rewards from the market are added.
In other words, all rewards from a market are distributed among all vaults connected to that market.

Impact

The reward calculation is incorrect.
Stakers receive more rewards than they should, resulting in the vault losing funds.

Recommendations

function getVaultAccumulatedValues(
Data storage self,
UD60x18 vaultDelegatedCreditUsdX18,
SD59x18 lastVaultDistributedRealizedDebtUsdPerShareX18,
SD59x18 lastVaultDistributedUnrealizedDebtUsdPerShareX18,
UD60x18 lastVaultDistributedUsdcCreditPerShareX18,
UD60x18 lastVaultDistributedWethRewardPerShareX18
)
internal
view
returns (
SD59x18 realizedDebtChangeUsdX18,
SD59x18 unrealizedDebtChangeUsdX18,
UD60x18 usdcCreditChangeX18,
UD60x18 wethRewardChangeX18
)
{
// calculate the vault's share of the total delegated credit, from 0 to 1
296: UD60x18 vaultCreditShareX18 = vaultDelegatedCreditUsdX18.div(getTotalDelegatedCreditUsd(self));
...
realizedDebtChangeUsdX18 = !lastVaultDistributedRealizedDebtUsdPerShareX18.isZero()
? sd59x18(self.realizedDebtUsdPerVaultShare).sub(lastVaultDistributedRealizedDebtUsdPerShareX18).mul(
vaultCreditShareX18.intoSD59x18()
)
: SD59x18_ZERO;
unrealizedDebtChangeUsdX18 = !lastVaultDistributedUnrealizedDebtUsdPerShareX18.isZero()
? sd59x18(self.unrealizedDebtUsdPerVaultShare).sub(lastVaultDistributedUnrealizedDebtUsdPerShareX18).mul(
vaultCreditShareX18.intoSD59x18()
)
: SD59x18_ZERO;
usdcCreditChangeX18 = !lastVaultDistributedUsdcCreditPerShareX18.isZero()
? ud60x18(self.usdcCreditPerVaultShare).sub(lastVaultDistributedUsdcCreditPerShareX18).mul(
vaultCreditShareX18
)
: UD60x18_ZERO;
// TODO: fix the vaultCreditShareX18 flow to multiply by `wethRewardChangeX18`
- wethRewardChangeX18 = ud60x18(self.wethRewardPerVaultShare).sub(lastVaultDistributedWethRewardPerShareX18);
+ wethRewardChangeX18 = ud60x18(self.wethRewardPerVaultShare).sub(lastVaultDistributedWethRewardPerShareX18).mul(vaultCreditShareX18.intoSD59x18());
}
Updates

Lead Judging Commences

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

`wethRewardPerVaultShare` is incremented by `receivedVaultWethReward` amount which is not divided by number of shares.

Support

FAQs

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