Skip to content
Request an audit

‹ All findings

ZivoeYDL::earningsTrancheuse() always assumes that daysBetweenDistributions have passed, which might not be the case

MediumZivoe·Sherlock · Lending · 8th April 2024CodebaseM-13

Summary

ZivoeYDL::earningsTrancheuse() calculates the target yield, senior and junior proportions based on daysBetweenDistributions. However, it is not strictly enforced that ZivoeYDL::distributeYield() is called exactly at the daysBetweenDistributions mark, leading to less yield for tranches.

Vulnerability Detail

There is a yield target for senior and junior tranches over a daysBetweenDistributions period that when exceeded, sends the rewards to residuals. However, the current code will not strictly follow the expected APY for senior tranches, as the earning of tranches are calculated based on daysBetweenDistributions, but ZivoeYDL::distributeYield() may be called significantly after the daysBetweenDistributions mark.

Add the following test to Test_ZivoeYDL.sol to confirm that when the senior tranche yield reached the expected target for 30 days, if the time that ZivoeYDL::distributeYield() was called increased, the yield distributed to the senior tranch remains the same.

solidity
function test_POC_ZivoeYDL_distributeYield_notEnforcingTranchesPeriod() public {    
    uint256 amtSenior = 1500 ether; // Minimum amount $1,000 USD for each coin.
    uint256 amount = 1000 ether;

    // Simulating the ITO will "unlock" the YDL
    simulateITO_byTranche_optionalStake(amtSenior, true);

    // uncomment this line instead to confirm that it's the same value below
    //hevm.warp(YDL.lastDistribution() + YDL.daysBetweenDistributions() * 86400);
    hevm.warp(YDL.lastDistribution() + YDL.daysBetweenDistributions() * 86400 + 1 days);

    // Deal DAI to the YDL
    mint("DAI", address(YDL), amount);

    YDL.distributeYield();

    console.log(IERC20(DAI).balanceOf(address(stSTT)));
}

Impact

Guaranteed less APY for junior and senior tranches. The amount depends on how much time the call to ZivoeYDL::distributeYield() was delayed since block.timestamp reached lastDistribution + daysBetweenDistributions * 86400.

Code Snippet

distributeYield()

solidity
function distributeYield() external nonReentrant {
    ...
    require(
        block.timestamp >= lastDistribution + daysBetweenDistributions * 86400, 
        "ZivoeYDL::distributeYield() block.timestamp < lastDistribution + daysBetweenDistributions * 86400"
    );
    ...

ZivoeYDL::earningsTrancheuse()

solidity
function earningsTrancheuse(uint256 yP, uint256 yD) public view returns (
    uint256[] memory protocol, uint256 senior, uint256 junior, uint256[] memory residual
) {
    ...
    uint256 _seniorProportion = MATH.seniorProportion(
        IZivoeGlobals_YDL(GBL).standardize(yD, distributedAsset),
        MATH.yieldTarget(emaSTT, emaJTT, targetAPYBIPS, targetRatioBIPS, daysBetweenDistributions),
        emaSTT, emaJTT, targetAPYBIPS, targetRatioBIPS, daysBetweenDistributions
    );
    ...
}

Tool used

Manual Review

Vscode

Foundry

Recommendation

Modify the ZivoeMath::seniorProportion() and ZivoeMath::yieldTarget() implementations to use seconds instead of days for T. Then, in Zivoe::YDL::earningsTrancheuse(), instead of daysBetweenDistributions when calculating the yield target and senior proportion, use block.timestamp - lastDistribution. Also, in ZivoeYDL::distributeYield(), lastDistribution = block.timestamp; must be moved after earningsTrancheuse() is called.