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 (asset != address(0)) {
self.receivedFees.remove(asset);
}
self.availableProtocolWethReward =
ud60x18(self.availableProtocolWethReward).add(receivedProtocolWethRewardX18).intoUint128();
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
)
{
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;
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()) {
(
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);
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());
}