From 0662e9bb06c16f87fa62d7694e2ea3feecca4ed5 Mon Sep 17 00:00:00 2001 From: Ian Lucas Date: Wed, 30 Oct 2024 16:03:59 -0400 Subject: [PATCH] fix: Update LiquidStone to accrue yield until the requestRedeem period only. --- .../yield/LiquidContinuousMultiTokenVault.sol | 18 +++--- .../timelock/TimelockAsyncUnlockTest.t.sol | 14 +++++ .../LiquidContinuousMultiTokenVaultTest.t.sol | 61 ++++++++++++++++++- ...uidContinuousMultiTokenVaultTestBase.t.sol | 5 +- 4 files changed, 86 insertions(+), 12 deletions(-) diff --git a/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol b/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol index 61c2986f..b135bf78 100644 --- a/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol +++ b/packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol @@ -374,15 +374,17 @@ contract LiquidContinuousMultiTokenVault is _yieldStrategy = yieldStrategy; } - /// @dev yield based on the associated yieldStrategy - function calcYield(uint256 principal, uint256 fromPeriod, uint256 toPeriod) public view returns (uint256 yield) { - // no yield earned when depositing and requesting redeem within the notice period. - // e.g. deposit day 1, immediately request redeem on day 1. should give 0 returns. - if (toPeriod <= fromPeriod + noticePeriod()) { - return 0; - } + /// @dev yield accrues up to the `requestRedeemPeriod` (as opposed to the `redeemPeriod`) + function calcYield(uint256 principal, uint256 depositPeriod, uint256 redeemPeriod) + public + view + returns (uint256 yield) + { + uint256 requestRedeemPeriod = redeemPeriod > noticePeriod() ? redeemPeriod - noticePeriod() : 0; + + if (requestRedeemPeriod <= depositPeriod) return 0; // no yield when deposit and requestRedeems are the same period - return _yieldStrategy.calcYield(address(this), principal, fromPeriod, toPeriod); + return _yieldStrategy.calcYield(address(this), principal, depositPeriod, requestRedeemPeriod); } /// @dev price is not used in Vault calculations. however, 1 asset = 1 share, implying a price of 1 diff --git a/packages/contracts/test/src/timelock/TimelockAsyncUnlockTest.t.sol b/packages/contracts/test/src/timelock/TimelockAsyncUnlockTest.t.sol index 60e72a68..29acb68b 100644 --- a/packages/contracts/test/src/timelock/TimelockAsyncUnlockTest.t.sol +++ b/packages/contracts/test/src/timelock/TimelockAsyncUnlockTest.t.sol @@ -508,6 +508,20 @@ contract TimelockAsyncUnlockTest is Test { ); } + function test__TimelockAsyncUnlock__RequestUnlockInvalidArrayLengthReverts() public { + uint256[] memory depositPeriods_ = new uint256[](2); + uint256[] memory amounts_ = new uint256[](1); + + vm.expectRevert( + abi.encodeWithSelector( + TimelockAsyncUnlock.TimelockAsyncUnlock__InvalidArrayLength.selector, + depositPeriods_.length, + amounts_.length + ) + ); + asyncUnlock.requestUnlock(alice, depositPeriods_, amounts_); + } + function _asSingletonArray(uint256 element) internal pure returns (uint256[] memory array) { array = new uint256[](1); array[0] = element; diff --git a/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol b/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol index 56cd27d2..accba477 100644 --- a/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol +++ b/packages/contracts/test/src/yield/LiquidContinuousMultiTokenVaultTest.t.sol @@ -63,7 +63,7 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT LiquidContinuousMultiTokenVault liquidVault = _liquidVault; // _createLiquidContinueMultiTokenVault(_vaultParams); TestParamSet.TestParam memory testParams = - TestParamSet.TestParam({ principal: 2_000 * _scale, depositPeriod: 10, redeemPeriod: 70 }); + TestParamSet.TestParam({ principal: 2_000 * _scale, depositPeriod: 10, redeemPeriod: 71 }); uint256 assetStartBalance = _asset.balanceOf(alice); @@ -283,13 +283,15 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT function test__LiquidContinuousMultiTokenVault__50k_Returns() public view { uint256 deposit = 50_000 * _scale; + uint256 tenorPlusNoticePeriod = _liquidVault.TENOR() + _liquidVault.noticePeriod(); + // verify returns - uint256 actualYield = _liquidVault.calcYield(deposit, 0, _liquidVault.TENOR()); + uint256 actualYield = _liquidVault.calcYield(deposit, 0, tenorPlusNoticePeriod); assertEq(416_666666, actualYield, "interest not correct for $50k deposit after 30 days"); // verify principal + returns uint256 actualShares = _liquidVault.convertToShares(deposit); - uint256 actualReturns = _liquidVault.convertToAssetsForDepositPeriod(actualShares, 0, _liquidVault.TENOR()); + uint256 actualReturns = _liquidVault.convertToAssetsForDepositPeriod(actualShares, 0, tenorPlusNoticePeriod); assertEq(50_416_666666, actualReturns, "principal + interest not correct for $50k deposit after 30 days"); } @@ -843,4 +845,57 @@ contract LiquidContinuousMultiTokenVaultTest is LiquidContinuousMultiTokenVaultT uint256 assets = _liquidVault.redeem(shares, alice, alice); assertEq(principal, assets, "assets should be the same as principal"); } + + function test__LiquidContinuousMultiTokenVault__CalcYieldEdgeCases() public view { + uint256 principal = 1_000_000_000 * _scale; + uint256 zeroPeriod = 0; + uint256 hundredPeriod = 100; + uint256 noticePeriod = _liquidVault.noticePeriod(); + + // check scenarios with zero returns + assertEq( + 0, + _liquidVault.calcYield(principal, zeroPeriod, zeroPeriod), + "no returns when redeeming at deposit period - deposit at 0" + ); + assertEq( + 0, + _liquidVault.calcYield(principal, hundredPeriod, hundredPeriod), + "no returns when redeeming at deposit period - deposit at 100" + ); + + assertEq( + 0, + _liquidVault.calcYield(principal, zeroPeriod, zeroPeriod + noticePeriod), + "no returns when redeeming at notice period - deposit at 0" + ); + assertEq( + 0, + _liquidVault.calcYield(principal, hundredPeriod, hundredPeriod + noticePeriod), + "no returns when redeeming at notice period - deposit at 100" + ); + + assertEq( + 0, + _liquidVault.calcYield(principal, 1, zeroPeriod), + "zero yield redeem less than deposit period - redeem at 0" + ); + assertEq( + 0, + _liquidVault.calcYield(principal, hundredPeriod, hundredPeriod - 1), + "zero yield redeem less than deposit period - redeem at 99" + ); + + // check scenarios with returns + assertLt( + 0, + _liquidVault.calcYield(principal, zeroPeriod, zeroPeriod + noticePeriod + 1), + "redeem > notice period should have yield" + ); + assertLt( + 0, + _liquidVault.calcYield(principal, hundredPeriod, hundredPeriod + noticePeriod + 1), + "redeem > notice period should have yield" + ); + } } diff --git a/packages/contracts/test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol b/packages/contracts/test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol index 6181b41f..4a525b71 100644 --- a/packages/contracts/test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol +++ b/packages/contracts/test/test/yield/LiquidContinuousMultiTokenVaultTestBase.t.sol @@ -261,8 +261,11 @@ abstract contract LiquidContinuousMultiTokenVaultTestBase is IMultiTokenVaultTes { LiquidContinuousMultiTokenVault liquidVault = LiquidContinuousMultiTokenVault(address(vault)); + // LiquidStone stops accruing yield at the requestRedeem period + uint256 requestRedeemPeriod = testParam.redeemPeriod - _liquidVault.noticePeriod(); + return liquidVault._yieldStrategy().calcYield( - address(vault), testParam.principal, testParam.depositPeriod, testParam.redeemPeriod + address(vault), testParam.principal, testParam.depositPeriod, requestRedeemPeriod ); }