Skip to content

Commit 80911fe

Browse files
committed
refactor(liquidity-vault): refactoring so that redeemable-assets changes for rollover-vaults
1 parent 030d5ff commit 80911fe

6 files changed

Lines changed: 205 additions & 0 deletions

File tree

src/LiquidityVault.sol

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,33 @@ abstract contract LiquidityVault is
202202
return _getLiquidityVaultStorage()._redeemableAssets;
203203
}
204204

205+
function _updateAssets(address asset, bool isRedeemable, bool add) internal {
206+
// Fetch storage
207+
LiquidityVaultStorage storage $ = _getLiquidityVaultStorage();
208+
209+
if (isRedeemable) {
210+
if (add) {
211+
$._redeemableAssets.push(asset);
212+
$._redeemableAssetIndex[asset] = $._redeemableAssets.length;
213+
} else {
214+
$._redeemableAssets[$._redeemableAssetIndex[asset] - 1] = $._redeemableAssets[$._redeemableAssets.length - 1];
215+
$._redeemableAssetIndex[$._redeemableAssets[$._redeemableAssets.length - 1]] = $._redeemableAssetIndex[asset];
216+
$._redeemableAssets.pop();
217+
$._redeemableAssetIndex[asset] = 0;
218+
}
219+
} else {
220+
if (add) {
221+
$._depositableAssets.push(asset);
222+
$._depositableAssetIndex[asset] = $._depositableAssets.length;
223+
} else {
224+
$._depositableAssets[$._depositableAssetIndex[asset] - 1] = $._depositableAssets[$._depositableAssets.length - 1];
225+
$._depositableAssetIndex[$._depositableAssets[$._depositableAssets.length - 1]] = $._depositableAssetIndex[asset];
226+
$._depositableAssets.pop();
227+
$._depositableAssetIndex[asset] = 0;
228+
}
229+
}
230+
}
231+
205232
function _totalAssets() internal view virtual returns (uint256);
206233

207234
/// @inheritdoc ILiquidityVault

src/RolloverVault.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
88
import {IGeneralManager} from "@core/interfaces/IGeneralManager/IGeneralManager.sol";
99
import {IOriginationPool} from "@core/interfaces/IOriginationPool/IOriginationPool.sol";
1010
import {IOriginationPoolScheduler} from "@core/interfaces/IOriginationPoolScheduler/IOriginationPoolScheduler.sol";
11+
1112
/**
1213
* @title FulfillmentVault
1314
* @author @SocksNFlops
@@ -179,6 +180,8 @@ contract RolloverVault is LiquidityVault, IRolloverVault {
179180
if ($._poolIndex[originationPool] == 0) {
180181
$._poolIndex[originationPool] = $._originationPools.length + 1;
181182
$._originationPools.push(originationPool);
183+
// Update the assets
184+
_updateAssets(originationPool, true, true);
182185
// Emit the origination pool added event
183186
emit OriginationPoolAdded(originationPool);
184187
}
@@ -207,6 +210,9 @@ contract RolloverVault is LiquidityVault, IRolloverVault {
207210
delete $._poolIndex[originationPool];
208211
$._originationPools.pop();
209212

213+
// Update the assets
214+
_updateAssets(originationPool, true, false);
215+
210216
// Emit the redeem event
211217
uint256 ogPoolBalance = IOriginationPool(originationPool).balanceOf(address(this));
212218
emit OriginationPoolRedeemed(originationPool, ogPoolBalance);

test/LiquidityVault.t.sol

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,4 +395,40 @@ contract LiquidityVaultTest is Test {
395395
assertEq(depositableAsset.balanceOf(user), 0, "Depositable asset balance of user should be 0");
396396
assertEq(redeemableAsset.balanceOf(user), depositAmount, "Redeemable asset balance of user should be depositAmount");
397397
}
398+
399+
function test_updateAsset_add(address asset, bool isRedeemable) public {
400+
// Update the assets
401+
MockLiquidityVault(address(liquidityVault)).updateAssets(asset, isRedeemable, true);
402+
403+
if (isRedeemable) {
404+
assertEq(liquidityVault.redeemableAssets().length, 2, "Redeemable assets should have length 2");
405+
assertEq(liquidityVault.redeemableAssets()[0], address(redeemableAsset), "redeemableAssets[0] should be the redeemable asset");
406+
assertEq(liquidityVault.redeemableAssets()[1], asset, "redeemableAssets[1] should be the asset passed in");
407+
} else {
408+
assertEq(liquidityVault.depositableAssets().length, 2, "Depositable assets should have length 2");
409+
assertEq(liquidityVault.depositableAssets()[0], address(depositableAsset), "depositableAssets[0] should be the depositable asset");
410+
assertEq(liquidityVault.depositableAssets()[1], asset, "depositableAssets[1] should be the asset passed in");
411+
}
412+
}
413+
414+
function test_updateAsset_remove(bool isRedeemable) public {
415+
// Update the assets
416+
address asset = isRedeemable ? address(redeemableAsset) : address(depositableAsset);
417+
MockLiquidityVault(address(liquidityVault)).updateAssets(asset, isRedeemable, false);
418+
419+
if (isRedeemable) {
420+
assertEq(liquidityVault.redeemableAssets().length, 0, "Redeemable assets should have length 2");
421+
} else {
422+
assertEq(liquidityVault.depositableAssets().length, 0, "Depositable assets should have length 0");
423+
}
424+
}
425+
426+
function test_updateAsset_removeMissingAsset(address asset, bool isRedeemable) public {
427+
// Make sure the asset is not the depositable asset or redeemable asset
428+
vm.assume(asset != address(depositableAsset) && asset != address(redeemableAsset));
429+
430+
// Attempt to remove a missing asset
431+
vm.expectRevert();
432+
MockLiquidityVault(address(liquidityVault)).updateAssets(asset, isRedeemable, false);
433+
}
398434
}

test/RolloverVault.t.sol

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ import {Router} from "../src/Router.sol";
2727
import {MockPriceOracle} from "./mocks/MockPriceOracle.sol";
2828
import {CreationRequest, BaseRequest} from "@core/types/orders/OrderRequests.sol";
2929
import {IOriginationPoolScheduler} from "@core/interfaces/IOriginationPoolScheduler/IOriginationPoolScheduler.sol";
30+
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
3031

3132
contract RolloverVaultTest is BaseTest {
33+
using Math for uint256;
3234
HyperCore public hyperCore;
3335

3436
using PrecompileLib for address;
@@ -393,6 +395,13 @@ contract RolloverVaultTest is BaseTest {
393395
vm.stopPrank();
394396
}
395397

398+
// Check that redeemable assets are set correctly
399+
{
400+
assertEq(rolloverVault.redeemableAssets().length, 2, "RolloverVault should have 2 redeemable assets");
401+
assertEq(rolloverVault.redeemableAssets()[0], address(usdx), "RolloverVault should have usdx as the first redeemable asset");
402+
assertEq(rolloverVault.redeemableAssets()[1], address(consol), "RolloverVault should have consol as the second redeemable asset");
403+
}
404+
396405
// Keeper pauses the rolloverVault and deposits the entire usdx balance into the origination pool
397406
{
398407
vm.startPrank(keeper);
@@ -401,6 +410,14 @@ contract RolloverVaultTest is BaseTest {
401410
vm.stopPrank();
402411
}
403412

413+
// Check that redeemable assets have been updated correctly
414+
{
415+
assertEq(rolloverVault.redeemableAssets().length, 3, "RolloverVault should have 3 redeemable assets");
416+
assertEq(rolloverVault.redeemableAssets()[0], address(usdx), "RolloverVault should have usdx as the first redeemable asset");
417+
assertEq(rolloverVault.redeemableAssets()[1], address(consol), "RolloverVault should have consol as the second redeemable asset");
418+
assertEq(rolloverVault.redeemableAssets()[2], address(originationPool), "RolloverVault should have the origination pool as the third redeemable asset");
419+
}
420+
404421
// Skip time ahead to the origination pool's redemption period
405422
vm.warp(originationPool.redemptionPhaseTimestamp());
406423

@@ -411,9 +428,120 @@ contract RolloverVaultTest is BaseTest {
411428
rolloverVault.redeemOriginationPool(address(originationPool));
412429
vm.stopPrank();
413430

431+
// Check that redeemable assets have been updated correctly
432+
{
433+
assertEq(rolloverVault.redeemableAssets().length, 2, "RolloverVault should have 2 redeemable assets");
434+
assertEq(rolloverVault.redeemableAssets()[0], address(usdx), "RolloverVault should have usdx as the first redeemable asset");
435+
assertEq(rolloverVault.redeemableAssets()[1], address(consol), "RolloverVault should have consol as the second redeemable asset");
436+
}
437+
414438
// Validate that the origination pool has been removed
415439
assertFalse(rolloverVault.isTracked(address(originationPool)), "Origination pool should not be tracked");
416440
assertEq(rolloverVault.originationPools().length, 0, "Origination pool should not be tracked");
417441
assertEq(originationPool.balanceOf(address(rolloverVault)), 0, "Origination pool should have no balance");
418442
}
443+
444+
function test_totalAssets_whileTrackingOriginationPool(uint256 depositAmount) public {
445+
// Ensure the depositAmount is at least $1 but less than the origination pool limit
446+
depositAmount = uint256(bound(depositAmount, 1e18, originationPool.poolLimit()));
447+
448+
// User deposits depositAmount of usdx into the rolloverVault
449+
{
450+
vm.startPrank(user);
451+
uint256 usdtAmount = usdx.convertUnderlying(address(usdt), depositAmount);
452+
deal(address(usdt), user, usdtAmount);
453+
usdt.approve(address(usdx), usdtAmount);
454+
usdx.deposit(address(usdt), usdtAmount);
455+
usdx.approve(address(rolloverVault), depositAmount);
456+
rolloverVault.deposit(address(usdx), depositAmount);
457+
vm.stopPrank();
458+
}
459+
460+
// Query total assets in the rolloverVault
461+
uint256 totalAssetsBefore = rolloverVault.totalAssets();
462+
463+
// Keeper pauses the rolloverVault and deposits the entire usdx balance into the origination pool
464+
{
465+
vm.startPrank(keeper);
466+
rolloverVault.setPaused(true);
467+
rolloverVault.depositOriginationPool(address(originationPool), depositAmount);
468+
vm.stopPrank();
469+
}
470+
471+
// Query total assets in the rolloverVault
472+
uint256 totalAssetsAfter = rolloverVault.totalAssets();
473+
474+
// Validate that total assets have not changed
475+
assertEq(totalAssetsAfter, totalAssetsBefore, "Total assets should not have changed");
476+
}
477+
478+
function test_withdraw_whileTrackingOriginationPool(uint256 depositAmount) public {
479+
// Ensure the depositAmount is at least $1 but less than the origination pool limit
480+
depositAmount = uint256(bound(depositAmount, 1e18, originationPool.poolLimit()));
481+
482+
// Validate that the user has 0 usdx balance to start with
483+
assertEq(usdx.balanceOf(user), 0, "User should have 0 usdx balance to start with");
484+
485+
// User deposits depositAmount of usdx into the rolloverVault
486+
{
487+
vm.startPrank(user);
488+
uint256 usdtAmount = usdx.convertUnderlying(address(usdt), depositAmount);
489+
deal(address(usdt), user, usdtAmount);
490+
usdt.approve(address(usdx), usdtAmount);
491+
usdx.deposit(address(usdt), usdtAmount);
492+
usdx.approve(address(rolloverVault), usdx.balanceOf(user));
493+
rolloverVault.deposit(address(usdx), usdx.balanceOf(user));
494+
vm.stopPrank();
495+
}
496+
497+
// Validate that the user has 0 usdx balance
498+
assertEq(usdx.balanceOf(user), 0, "User should have 0 usdx balance");
499+
500+
// Keeper pauses the rolloverVault and deposits the entire usdx balance into the origination pool
501+
{
502+
vm.startPrank(keeper);
503+
rolloverVault.setPaused(true);
504+
rolloverVault.depositOriginationPool(address(originationPool), depositAmount);
505+
vm.stopPrank();
506+
}
507+
508+
// Keeper unpauses the rolloverVault
509+
{
510+
vm.startPrank(keeper);
511+
rolloverVault.setPaused(false);
512+
vm.stopPrank();
513+
}
514+
515+
// Calculate expected redemption amounts
516+
uint256 expectedUsdxRedemption = Math.mulDiv(rolloverVault.balanceOf(user), usdx.balanceOf(address(rolloverVault)), rolloverVault.totalSupply());
517+
uint256 expectedOgpRedemption = Math.mulDiv(rolloverVault.balanceOf(user), originationPool.balanceOf(address(rolloverVault)), rolloverVault.totalSupply());
518+
519+
// Expected total assets amount
520+
uint256 expectedTotalAssets = Math.mulDiv(rolloverVault.balanceOf(user), rolloverVault.totalAssets(), rolloverVault.totalSupply());
521+
522+
// User withdraws their entire balance of the rolloverVault
523+
{
524+
vm.startPrank(user);
525+
rolloverVault.redeem(rolloverVault.balanceOf(user));
526+
vm.stopPrank();
527+
}
528+
529+
// Validate that the user has the expected redemption amounts
530+
assertApproxEqAbs(usdx.balanceOf(user), expectedUsdxRedemption, 1, "User should get expectedUsdxRedemption usdx out of the rolloverVault");
531+
assertApproxEqAbs(originationPool.balanceOf(user), expectedOgpRedemption, 1, "User should get expectedOgpRedemption origination pool out of the rolloverVault");
532+
533+
// Skip ahead to the origination pool's redemption period
534+
vm.warp(originationPool.redemptionPhaseTimestamp());
535+
536+
// User redeems the entire origination pool balance
537+
{
538+
vm.startPrank(user);
539+
originationPool.redeem(originationPool.balanceOf(user));
540+
vm.stopPrank();
541+
}
542+
543+
// Validate that the user has the expected redemption amounts
544+
assertEq(originationPool.balanceOf(user), 0, "User should have 0 origination pool balance");
545+
assertApproxEqAbs(usdx.balanceOf(user), expectedTotalAssets, 1, "User should have expectedTotalAssets of usdx");
546+
}
419547
}

test/mocks/MockLiquidityVault.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,8 @@ contract MockLiquidityVault is LiquidityVault {
1717
}
1818
return total;
1919
}
20+
21+
function updateAssets(address asset, bool isRedeemable, bool isAdd) public {
22+
_updateAssets(asset, isRedeemable, isAdd);
23+
}
2024
}

test/mocks/MockLiquidityVault2.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ contract MockLiquidityVault2 is LiquidityVault {
1818
return total;
1919
}
2020

21+
function updateAssets(address asset, bool isRedeemable, bool isAdd) public {
22+
_updateAssets(asset, isRedeemable, isAdd);
23+
}
24+
2125
function newFunction() public pure returns (bool) {
2226
return true;
2327
}

0 commit comments

Comments
 (0)