ZivoeYDL::earningsTrancheuse() always assumes that daysBetweenDistributions have passed, which might not be the case
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.
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
function distributeYield() external nonReentrant {
...
require(
block.timestamp >= lastDistribution + daysBetweenDistributions * 86400,
"ZivoeYDL::distributeYield() block.timestamp < lastDistribution + daysBetweenDistributions * 86400"
);
...ZivoeYDL::earningsTrancheuse()
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.