Skip to content

Commit

Permalink
Merge pull request #168 from credbull/dev/audit-yield
Browse files Browse the repository at this point in the history
LiquidStone to accrue yield until the requestRedeem period
  • Loading branch information
lucasia authored Oct 30, 2024
2 parents d860230 + 0662e9b commit 968c1f3
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 12 deletions.
18 changes: 10 additions & 8 deletions packages/contracts/src/yield/LiquidContinuousMultiTokenVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions packages/contracts/test/src/timelock/TimelockAsyncUnlockTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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");
}

Expand Down Expand Up @@ -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"
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down

0 comments on commit 968c1f3

Please sign in to comment.