Summary
The YieldFacet::distributeYield()
does not update the Vault's zethYieldRate
before distributing the yield earned to shorter's ShortRecord
positions.
As a result, the shorter will receive the zETH
and Ditto
rewards less than expected.
Vulnerability Details
The distributeYield()
does not update the Vault's zethYieldRate
before distributing the yield earned to the shorter's ShortRecord
positions.
Once the _distributeYield()
is executed, the non-updated zethYieldRate
will be consumed. Subsequently, the computed yield
(zETH
) and dittoYieldShares
will be less than expected.
Finally, the shorter will receive the zETH
and Ditto
rewards less than expected.
function distributeYield(address[] calldata assets) external nonReentrant {
uint256 length = assets.length;
uint256 vault = s.asset[assets[0]].vault;
@> (uint88 yield, uint256 dittoYieldShares) = _distributeYield(assets[0]);
for (uint256 i = 1; i < length;) {
if (s.asset[assets[i]].vault != vault) revert Errors.DifferentVaults();
@> (uint88 amtYield, uint256 amtDittoYieldShares) = _distributeYield(assets[i]);
@> yield += amtYield;
@> dittoYieldShares += amtDittoYieldShares;
unchecked {
++i;
}
}
@> _claimYield(vault, yield, dittoYieldShares);
emit Events.DistributeYield(vault, msg.sender, yield, dittoYieldShares);
}
function _distributeYield(address asset)
private
onlyValidAsset(asset)
returns (uint88 yield, uint256 dittoYieldShares)
{
uint256 vault = s.asset[asset].vault;
@> uint80 zethYieldRate = s.vault[vault].zethYieldRate;
...
while (true) {
STypes.ShortRecord storage short = s.shortRecords[asset][msg.sender][id];
bool isNotRecentlyModified =
timestamp - short.updatedAt > Constants.YIELD_DELAY_HOURS;
if (short.status != SR.Cancelled && isNotRecentlyModified) {
@> uint88 shortYield =
@> short.collateral.mulU88(zethYieldRate - short.zethYieldRate);
@> yield += shortYield;
short.zethYieldRate = zethYieldRate;
uint256 cRatio = short.getCollateralRatioSpotPrice(oraclePrice);
if (cRatio <= initialCR) {
@> dittoYieldShares += shortYield;
} else {
@> dittoYieldShares += shortYield.mul(initialCR).div(cRatio);
}
}
if (short.nextId > Constants.HEAD) {
id = short.nextId;
} else {
break;
}
}
}
-
The distributeYield() distributes yield earned to the shorter's ShortRecord positions without updating the Vault's zethYieldRate
: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/YieldFacet.sol#L58-L69
-
The distributeYield() credits the zETH and Ditto rewards to a shorter
: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/YieldFacet.sol#L71
-
Since the Vault's zethYieldRate is not updated, the _distributeYield() will consume the non-updated zethYieldRate
: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/YieldFacet.sol#L83
-
Consequently, the computed yield (zETH) and dittoYieldShares will be less than expected
: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/YieldFacet.sol#L101-L114
Impact
As a result, the shorter will receive the zETH
and Ditto
rewards less than expected.
Tools Used
Manual Review
Recommendations
Update the Vault's zethYieldRate
before distributing the yield earned to the shorter's ShortRecord
positions.
function distributeYield(address[] calldata assets) external nonReentrant {
uint256 length = assets.length;
uint256 vault = s.asset[assets[0]].vault;
+ vault.updateYield();
+ emit Events.UpdateYield(vault);
// distribute yield for the first order book
(uint88 yield, uint256 dittoYieldShares) = _distributeYield(assets[0]);
// distribute yield for remaining order books
for (uint256 i = 1; i < length;) {
if (s.asset[assets[i]].vault != vault) revert Errors.DifferentVaults();
(uint88 amtYield, uint256 amtDittoYieldShares) = _distributeYield(assets[i]);
yield += amtYield;
dittoYieldShares += amtDittoYieldShares;
unchecked {
++i;
}
}
// claim all distributed yield
_claimYield(vault, yield, dittoYieldShares);
emit Events.DistributeYield(vault, msg.sender, yield, dittoYieldShares);
}