From 93257e9aa22024283721f391f8f2f1092b18f93c Mon Sep 17 00:00:00 2001 From: aalavandhan1984 <6264334+aalavandhan@users.noreply.github.com> Date: Tue, 18 Apr 2023 08:49:49 -0400 Subject: [PATCH 1/4] Public method to recover individual assets" --- .../contracts/_interfaces/IVault.sol | 3 + .../contracts/vaults/RolloverVault.sol | 140 ++++++--- spot-contracts/test/helpers.ts | 7 +- .../test/strategies/CDRPricingStrategy.ts | 67 ++-- .../NonEquityCDRLBPricingStrategy.ts | 56 +++- .../test/vaults/RolloverVault_recover.ts | 291 +++++++++++++++++- 6 files changed, 466 insertions(+), 98 deletions(-) diff --git a/spot-contracts/contracts/_interfaces/IVault.sol b/spot-contracts/contracts/_interfaces/IVault.sol index 22339e40..b12289c9 100644 --- a/spot-contracts/contracts/_interfaces/IVault.sol +++ b/spot-contracts/contracts/_interfaces/IVault.sol @@ -49,6 +49,9 @@ interface IVault { /// @notice Recovers deployed funds. function recover() external; + /// @notice Recovers a given deployed asset. + function recover(IERC20Upgradeable token) external; + /// @notice Deposits the underlying asset from {msg.sender} into the vault and mints notes. /// @param amount The amount tokens to be deposited into the vault. /// @return The amount of notes. diff --git a/spot-contracts/contracts/vaults/RolloverVault.sol b/spot-contracts/contracts/vaults/RolloverVault.sol index 19bf7eff..566ad440 100644 --- a/spot-contracts/contracts/vaults/RolloverVault.sol +++ b/spot-contracts/contracts/vaults/RolloverVault.sol @@ -164,18 +164,28 @@ contract RolloverVault is /// @inheritdoc IVault /// @dev Its safer to call `recover` before `deploy` so the full available balance can be deployed. /// Reverts if there are no funds to deploy. - function deploy() public nonReentrant whenNotPaused { + function deploy() public override nonReentrant whenNotPaused { TrancheData memory td = _tranche(perp.getDepositBond()); - if (_rollover(perp, td) == 0) { + if (_rollover(perp, td) <= 0) { revert NoDeployment(); } } /// @inheritdoc IVault - function recover() public nonReentrant whenNotPaused { + function recover() public override nonReentrant whenNotPaused { _redeemTranches(); } + /// @inheritdoc IVault + /// @dev Reverts when attempting to recover a tranche which is not part of the deployed list. + /// In the case of immature redemption, this method will recover other sibling tranches as well. + function recover(IERC20Upgradeable token) external override nonReentrant whenNotPaused { + if (!_deployed.contains(address(token))) { + revert UnexpectedAsset(token); + } + _redeemTranche(ITranche(address(token))); + } + /// @inheritdoc IVault function deposit(uint256 amount) external override nonReentrant whenNotPaused returns (uint256) { uint256 totalSupply_ = totalSupply(); @@ -283,6 +293,7 @@ contract RolloverVault is // Private write methods /// @dev Deposits underlying balance into the provided bond and receives tranche tokens in return. + /// And performs some book-keeping to keep track of the vault's assets. function _tranche(IBondController bond) private returns (TrancheData memory) { // Get bond's tranche data TrancheData memory td = bond.getTrancheData(); @@ -291,7 +302,7 @@ contract RolloverVault is uint256 balance = underlying.balanceOf(address(this)); // Ensure initial deposit remains unspent - if (balance == 0) { + if (balance <= 0) { return td; } @@ -301,7 +312,7 @@ contract RolloverVault is // sync holdings for (uint8 i = 0; i < td.trancheCount; i++) { - _syncDeployedAsset(td.tranches[i]); + _syncAndAddDeployedAsset(td.tranches[i]); } _syncAsset(underlying); @@ -309,6 +320,7 @@ contract RolloverVault is } /// @dev Rolls over freshly tranched tokens from the given bond for older tranches (close to maturity) from perp. + /// And performs some book-keeping to keep track of the vault's assets. /// @return The amount of perps rolled over. function _rollover(IPerpetualTranche perp_, TrancheData memory td) private returns (uint256) { // NOTE: The first element of the list is the mature tranche, @@ -335,7 +347,7 @@ contract RolloverVault is : 0; // trancheIntoPerp tokens are NOT exhausted but tokenOutOfPerp is exhausted - if (tokenOutAmtAvailable == 0) { + if (tokenOutAmtAvailable <= 0) { // Rollover is a no-op, so skipping to next tokenOutOfPerp perpTokenIdx++; continue; @@ -345,7 +357,7 @@ contract RolloverVault is uint256 trancheInAmtAvailable = trancheIntoPerp.balanceOf(address(this)); // trancheInAmtAvailable is exhausted - if (trancheInAmtAvailable == 0) { + if (trancheInAmtAvailable <= 0) { // Rollover is a no-op, so skipping to next trancheIntoPerp vaultTokenIdx++; continue; @@ -360,7 +372,7 @@ contract RolloverVault is ); // trancheIntoPerp isn't accepted by perp, likely because it's yield=0, refer perp docs for more info - if (rd.perpRolloverAmt == 0) { + if (rd.perpRolloverAmt <= 0) { // Rollover is a no-op, so skipping to next trancheIntoPerp vaultTokenIdx++; continue; @@ -371,9 +383,9 @@ contract RolloverVault is perp_.rollover(trancheIntoPerp, tokenOutOfPerp, trancheInAmtAvailable); // sync holdings - _syncDeployedAsset(trancheIntoPerp); + _syncAndRemoveDeployedAsset(trancheIntoPerp); if (tokenOutOfPerp != underlying) { - _syncDeployedAsset(tokenOutOfPerp); + _syncAndAddDeployedAsset(tokenOutOfPerp); } _syncAsset(perp_); _syncAsset(underlying); @@ -385,37 +397,17 @@ contract RolloverVault is return totalPerpRolledOver; } - /// @notice Redeems the deployed tranche tokens for the underlying asset. + /// @dev Redeems all deployed tranches for the underlying asset and + /// performs internal book-keeping to keep track of the vault assets. function _redeemTranches() private { uint256 deployedCount_ = _deployed.length(); if (deployedCount_ <= 0) { return; } + // execute redemption on each deployed asset for (uint256 i = 0; i < deployedCount_; i++) { - ITranche tranche = ITranche(_deployed.at(i)); - IBondController bond = IBondController(tranche.bond()); - - // if bond has matured, redeem the tranche token - if (bond.timeToMaturity() <= 0) { - if (!bond.isMature()) { - bond.mature(); - } - bond.redeemMature(address(tranche), tranche.balanceOf(address(this))); - } - // else redeem using proportional balances, redeems all tranches part of the bond - else { - TrancheData memory td; - uint256[] memory trancheAmts; - (td, trancheAmts) = bond.computeRedeemableTrancheAmounts(address(this)); - - // NOTE: It is guaranteed that if one tranche amount is zero, all amounts are zeros. - if (trancheAmts[0] == 0) { - continue; - } - - bond.redeem(trancheAmts); - } + _execTrancheRedemption(ITranche(_deployed.at(i))); } // sync holdings @@ -423,23 +415,65 @@ contract RolloverVault is // as deletions involve swapping the deleted element to the // end of the set and removing the last element. for (uint256 i = deployedCount_; i > 0; i--) { - _syncDeployedAsset(IERC20Upgradeable(_deployed.at(i - 1))); + _syncAndRemoveDeployedAsset(IERC20Upgradeable(_deployed.at(i - 1))); } _syncAsset(underlying); } - /// @dev Logs the token balance held by the vault. - /// @return The Vault's token balance. - function _syncAsset(IERC20Upgradeable token) private returns (uint256) { - uint256 balance = token.balanceOf(address(this)); - emit AssetSynced(token, balance); + /// @dev Redeems the given tranche for the underlying asset and + /// performs internal book-keeping to keep track of the vault assets. + function _redeemTranche(ITranche tranche) private { + TrancheData memory td = _execTrancheRedemption(tranche); - return balance; + // sync holdings + // Note: Immature redemption, may remove sibling tranches from the deployed list. + for (uint8 i = 0; i < td.trancheCount; i++) { + _syncAndRemoveDeployedAsset(td.tranches[i]); + } + _syncAsset(underlying); + } + + /// @dev Low level method that redeems the given deployed tranche tokens for the underlying asset. + /// When the tranche is not up for redemption, its a no-op. + /// This function should NOT be called directly, use `_redeemTranches` or `_redeemTranche` + /// which wrap this function with the internal book-keeping necessary to keep track of the vault's assets. + function _execTrancheRedemption(ITranche tranche) private returns (TrancheData memory) { + IBondController bond = IBondController(tranche.bond()); + TrancheData memory td; + + // if bond has matured, redeem the tranche token + if (bond.timeToMaturity() <= 0) { + if (!bond.isMature()) { + bond.mature(); + } + + td = bond.getTrancheData(); + uint256 trancheBalance = tranche.balanceOf(address(this)); + if (trancheBalance <= 0) { + return td; + } + + bond.redeemMature(address(tranche), trancheBalance); + } + // else redeem using proportional balances, redeems all tranches part of the bond + else { + uint256[] memory trancheAmts; + (td, trancheAmts) = bond.computeRedeemableTrancheAmounts(address(this)); + + // NOTE: It is guaranteed that if one tranche amount is zero, all amounts are zeros. + if (trancheAmts[0] <= 0) { + return td; + } + + bond.redeem(trancheAmts); + } + + return td; } - /// @dev Syncs balance and keeps the deployed assets list up to date. + /// @dev Syncs balance and adds the given asset into the deployed list if the vault has a balance. /// @return The Vault's token balance. - function _syncDeployedAsset(IERC20Upgradeable token) private returns (uint256) { + function _syncAndAddDeployedAsset(IERC20Upgradeable token) private returns (uint256) { uint256 balance = _syncAsset(token); bool isHeld = _deployed.contains(address(token)); @@ -448,7 +482,16 @@ contract RolloverVault is _deployed.add(address(token)); } - if (balance == 0 && isHeld) { + return balance; + } + + /// @dev Syncs balance and removes the given asset from the deployed list if the vault has no balance. + /// @return The Vault's token balance. + function _syncAndRemoveDeployedAsset(IERC20Upgradeable token) private returns (uint256) { + uint256 balance = _syncAsset(token); + bool isHeld = _deployed.contains(address(token)); + + if (balance <= 0 && isHeld) { // Removes token into the deployed assets list. _deployed.remove(address(token)); } @@ -456,6 +499,15 @@ contract RolloverVault is return balance; } + /// @dev Logs the token balance held by the vault. + /// @return The Vault's token balance. + function _syncAsset(IERC20Upgradeable token) private returns (uint256) { + uint256 balance = token.balanceOf(address(this)); + emit AssetSynced(token, balance); + + return balance; + } + //-------------------------------------------------------------------------- // Private read methods diff --git a/spot-contracts/test/helpers.ts b/spot-contracts/test/helpers.ts index 073a5309..49c9f0d2 100644 --- a/spot-contracts/test/helpers.ts +++ b/spot-contracts/test/helpers.ts @@ -179,11 +179,14 @@ export const advancePerpQueue = async (perp: Contract, time: number): Promise => { +export const advancePerpQueueUpToBondMaturity = async (perp: Contract, bond: Contract): Promise => { await perp.updateState(); const matuirtyDate = await bond.maturityDate(); await TimeHelpers.setNextBlockTimestamp(matuirtyDate.toNumber()); - await perp.updateState(); +}; + +export const advancePerpQueueToBondMaturity = async (perp: Contract, bond: Contract): Promise => { + await advancePerpQueueUpToBondMaturity(perp, bond); await TimeHelpers.increaseTime(1); return perp.updateState(); }; diff --git a/spot-contracts/test/strategies/CDRPricingStrategy.ts b/spot-contracts/test/strategies/CDRPricingStrategy.ts index 99eb4991..ada3d362 100644 --- a/spot-contracts/test/strategies/CDRPricingStrategy.ts +++ b/spot-contracts/test/strategies/CDRPricingStrategy.ts @@ -56,44 +56,55 @@ describe("CDRPricingStrategy", function () { let bond: Contract, tranches: Contract[]; beforeEach(async function () { bond = await createBondWithFactory(bondFactory, collateralToken, [500, 500], 86400); - await depositIntoBond(bond, toFixedPtAmt("1000"), deployer); tranches = await getTranches(bond); }); - describe("when pricing the tranche", function () { - describe("when bond not mature", function () { - it("should return the price", async function () { - expect(await pricingStrategy.computeTranchePrice(tranches[0].address)).to.eq("100000000"); - }); + describe("when bond is empty", function () { + it("should return zero", async function () { + expect(await pricingStrategy.computeTranchePrice(tranches[0].address)).to.eq("100000000"); }); + }); - describe("when bond is mature", function () { - beforeEach(async function () { - await TimeHelpers.increaseTime(86400); - await bond.mature(); // NOTE: Any rebase after maturity goes directly to the tranches - }); + describe("when bond has assets", function () { + beforeEach(async function () { + await depositIntoBond(bond, toFixedPtAmt("1000"), deployer); + }); - describe("when cdr = 1", async function () { + describe("when pricing the tranche", function () { + describe("when bond not mature", function () { it("should return the price", async function () { expect(await pricingStrategy.computeTranchePrice(tranches[0].address)).to.eq("100000000"); }); }); - describe("when cdr > 1", async function () { + describe("when bond is mature", function () { beforeEach(async function () { - await rebase(collateralToken, rebaseOracle, 0.1); + await TimeHelpers.increaseTime(86400); + await bond.mature(); // NOTE: Any rebase after maturity goes directly to the tranches }); - it("should return the price", async function () { - expect(await pricingStrategy.computeTranchePrice(tranches[0].address)).to.eq("110000000"); + + describe("when cdr = 1", async function () { + it("should return the price", async function () { + expect(await pricingStrategy.computeTranchePrice(tranches[0].address)).to.eq("100000000"); + }); }); - }); - describe("when cdr < 1", async function () { - beforeEach(async function () { - await rebase(collateralToken, rebaseOracle, -0.1); + describe("when cdr > 1", async function () { + beforeEach(async function () { + await rebase(collateralToken, rebaseOracle, 0.1); + }); + it("should return the price", async function () { + expect(await pricingStrategy.computeTranchePrice(tranches[0].address)).to.eq("110000000"); + }); }); - it("should return the price", async function () { - expect(await pricingStrategy.computeTranchePrice(tranches[0].address)).to.eq("90000000"); + + describe("when cdr < 1", async function () { + beforeEach(async function () { + await rebase(collateralToken, rebaseOracle, -0.1); + }); + it("should return the price", async function () { + expect(await pricingStrategy.computeTranchePrice(tranches[0].address)).to.eq("90000000"); + }); }); }); }); @@ -101,6 +112,18 @@ describe("CDRPricingStrategy", function () { }); describe("computeMatureTranchePrice", function () { + describe("when debt is zero", function () { + it("should return zero", async function () { + expect( + await pricingStrategy.computeMatureTranchePrice( + collateralToken.address, + toFixedPtAmt("0"), + toFixedPtAmt("0"), + ), + ).to.eq("100000000"); + }); + }); + describe("when cdr = 1", async function () { it("should return the price", async function () { expect( diff --git a/spot-contracts/test/strategies/NonEquityCDRLBPricingStrategy.ts b/spot-contracts/test/strategies/NonEquityCDRLBPricingStrategy.ts index 67cba37f..7c684a00 100644 --- a/spot-contracts/test/strategies/NonEquityCDRLBPricingStrategy.ts +++ b/spot-contracts/test/strategies/NonEquityCDRLBPricingStrategy.ts @@ -49,49 +49,71 @@ describe("NonEquityCDRLBPricingStrategy", function () { let bond: Contract, tranches: Contract[]; beforeEach(async function () { bond = await createBondWithFactory(bondFactory, collateralToken, [500, 500], 86400); - await depositIntoBond(bond, toFixedPtAmt("1000"), deployer); tranches = await getTranches(bond); }); - describe("when bond not mature", function () { + describe("when bond has no assets", function () { it("should return the price", async function () { expect(await pricingStrategy.computeTranchePrice(tranches[0].address)).to.eq("100000000"); }); }); - describe("when bond is mature", function () { + describe("when bond has assets", function () { beforeEach(async function () { - await TimeHelpers.increaseTime(86400); - await bond.mature(); // NOTE: Any rebase after maturity goes directly to the tranches + await depositIntoBond(bond, toFixedPtAmt("1000"), deployer); }); - - describe("when cdr = 1", async function () { + describe("when bond not mature", function () { it("should return the price", async function () { expect(await pricingStrategy.computeTranchePrice(tranches[0].address)).to.eq("100000000"); }); }); - describe("when cdr > 1", async function () { + describe("when bond is mature", function () { beforeEach(async function () { - await rebase(collateralToken, rebaseOracle, 0.1); + await TimeHelpers.increaseTime(86400); + await bond.mature(); // NOTE: Any rebase after maturity goes directly to the tranches }); - it("should return the price", async function () { - expect(await pricingStrategy.computeTranchePrice(tranches[0].address)).to.eq("100000000"); + + describe("when cdr = 1", async function () { + it("should return the price", async function () { + expect(await pricingStrategy.computeTranchePrice(tranches[0].address)).to.eq("100000000"); + }); }); - }); - describe("when cdr < 1", async function () { - beforeEach(async function () { - await rebase(collateralToken, rebaseOracle, -0.1); + describe("when cdr > 1", async function () { + beforeEach(async function () { + await rebase(collateralToken, rebaseOracle, 0.1); + }); + it("should return the price", async function () { + expect(await pricingStrategy.computeTranchePrice(tranches[0].address)).to.eq("100000000"); + }); }); - it("should return the price", async function () { - expect(await pricingStrategy.computeTranchePrice(tranches[0].address)).to.eq("100000000"); + + describe("when cdr < 1", async function () { + beforeEach(async function () { + await rebase(collateralToken, rebaseOracle, -0.1); + }); + it("should return the price", async function () { + expect(await pricingStrategy.computeTranchePrice(tranches[0].address)).to.eq("100000000"); + }); }); }); }); }); describe("computeMatureTranchePrice", function () { + describe("when debt is zero", async function () { + it("should return the price", async function () { + expect( + await pricingStrategy.computeMatureTranchePrice( + collateralToken.address, + toFixedPtAmt("100"), + toFixedPtAmt("0"), + ), + ).to.eq("100000000"); + }); + }); + describe("when cdr = 1", async function () { it("should return the price", async function () { expect( diff --git a/spot-contracts/test/vaults/RolloverVault_recover.ts b/spot-contracts/test/vaults/RolloverVault_recover.ts index c306a640..0da10b73 100644 --- a/spot-contracts/test/vaults/RolloverVault_recover.ts +++ b/spot-contracts/test/vaults/RolloverVault_recover.ts @@ -6,6 +6,7 @@ import { smock } from "@defi-wonderland/smock"; import { setupCollateralToken, mintCollteralToken, + createBondWithFactory, setupBondFactory, depositIntoBond, bondAt, @@ -15,6 +16,7 @@ import { toPriceFixedPtAmt, getDepositBond, advancePerpQueue, + advancePerpQueueUpToBondMaturity, advancePerpQueueToBondMaturity, advancePerpQueueToRollover, checkReserveComposition, @@ -162,11 +164,11 @@ describe("RolloverVault", function () { await network.provider.send("hardhat_reset"); }); - describe("#recover", function () { + describe("#recover()", function () { describe("when no asset is deployed", function () { it("should be a no-op", async function () { - await vault.recover(); - await expect(vault.recover()).not.to.be.reverted; + await vault["recover()"](); + await expect(vault["recover()"]()).not.to.be.reverted; expect(await vault.deployedCount()).to.eq(0); }); }); @@ -205,7 +207,7 @@ describe("RolloverVault", function () { }); describe("when its not mature", function () { it("should be a no-op", async function () { - await expect(vault.recover()).not.to.be.reverted; + await expect(vault["recover()"]()).not.to.be.reverted; await checkVaultAssetComposition( vault, [collateralToken, currentTranchesIn[2], perp], @@ -220,12 +222,12 @@ describe("RolloverVault", function () { await advancePerpQueueToBondMaturity(perp, currentBondIn); }); it("should recover", async function () { - await expect(vault.recover()).not.to.be.reverted; + await expect(vault["recover()"]()).not.to.be.reverted; expect(await vault.deployedCount()).to.eq(0); await checkVaultAssetComposition(vault, [collateralToken, perp], [toFixedPtAmt("10"), toFixedPtAmt("0")]); }); it("should sync assets", async function () { - const tx = vault.recover(); + const tx = vault["recover()"](); await expect(tx).to.emit(vault, "AssetSynced").withArgs(collateralToken.address, toFixedPtAmt("10")); await expect(tx).to.emit(vault, "AssetSynced").withArgs(currentTranchesIn[2].address, toFixedPtAmt("0")); }); @@ -261,7 +263,7 @@ describe("RolloverVault", function () { describe("when no redemption", function () { it("should be a no-op", async function () { - await expect(vault.recover()).not.to.be.reverted; + await expect(vault["recover()"]()).not.to.be.reverted; await checkVaultAssetComposition( vault, [collateralToken, currentTranchesIn[1], currentTranchesIn[2], perp], @@ -278,12 +280,12 @@ describe("RolloverVault", function () { await advancePerpQueueToBondMaturity(perp, currentBondIn); }); it("should recover", async function () { - await expect(vault.recover()).not.to.be.reverted; + await expect(vault["recover()"]()).not.to.be.reverted; expect(await vault.deployedCount()).to.eq(0); await checkVaultAssetComposition(vault, [collateralToken, perp], [toFixedPtAmt("10"), toFixedPtAmt("0")]); }); it("should sync assets", async function () { - const tx = vault.recover(); + const tx = vault["recover()"](); await expect(tx).to.emit(vault, "AssetSynced").withArgs(collateralToken.address, toFixedPtAmt("10")); await expect(tx).to.emit(vault, "AssetSynced").withArgs(currentTranchesIn[1].address, toFixedPtAmt("0")); await expect(tx).to.emit(vault, "AssetSynced").withArgs(currentTranchesIn[2].address, toFixedPtAmt("0")); @@ -333,7 +335,7 @@ describe("RolloverVault", function () { describe("without reminder", function () { it("should recover", async function () { - await expect(vault.recover()).not.to.be.reverted; + await expect(vault["recover()"]()).not.to.be.reverted; expect(await vault.deployedCount()).to.eq(2); await checkVaultAssetComposition( vault, @@ -342,7 +344,7 @@ describe("RolloverVault", function () { ); }); it("should sync assets", async function () { - const tx = vault.recover(); + const tx = vault["recover()"](); await expect(tx).to.emit(vault, "AssetSynced").withArgs(collateralToken.address, toFixedPtAmt("2008")); await expect(tx).to.emit(vault, "AssetSynced").withArgs(currentTranchesIn[0].address, toFixedPtAmt("0")); await expect(tx).to.emit(vault, "AssetSynced").withArgs(currentTranchesIn[1].address, toFixedPtAmt("0")); @@ -380,7 +382,7 @@ describe("RolloverVault", function () { ); }); it("should recover", async function () { - await expect(vault.recover()).not.to.be.reverted; + await expect(vault["recover()"]()).not.to.be.reverted; expect(await vault.deployedCount()).to.eq(3); await checkVaultAssetComposition( vault, @@ -389,7 +391,270 @@ describe("RolloverVault", function () { ); }); it("should sync assets", async function () { - const tx = vault.recover(); + const tx = vault["recover()"](); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(collateralToken.address, toFixedPtAmt("2008")); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(currentTranchesIn[0].address, toFixedPtAmt("0")); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(currentTranchesIn[1].address, toFixedPtAmt("0")); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(currentTranchesIn[2].address, toFixedPtAmt("1")); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(newTranchesIn[1].address, toFixedPtAmt("3000")); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(newTranchesIn[2].address, toFixedPtAmt("5000")); + }); + }); + }); + }); + }); + + describe("#recover(address)", function () { + describe("when no asset is deployed", function () { + it("should revert", async function () { + await vault["recover()"](); + await expect(vault["recover(address)"](collateralToken.address)).to.be.revertedWith("UnexpectedAsset"); + expect(await vault.deployedCount()).to.eq(0); + }); + }); + + describe("when one asset deployed", function () { + let currentBondIn: Contract, currentTranchesIn: Contract[]; + beforeEach(async function () { + await advancePerpQueueToBondMaturity(perp, rolloverInBond); + currentBondIn = await bondAt(await perp.callStatic.getDepositBond()); + currentTranchesIn = await getTranches(currentBondIn); + + await pricingStrategy.computeTranchePrice + .whenCalledWith(currentTranchesIn[0].address) + .returns(toPriceFixedPtAmt("1")); + await discountStrategy.computeTrancheDiscount + .whenCalledWith(currentTranchesIn[0].address) + .returns(toDiscountFixedPtAmt("1")); + + await pricingStrategy.computeTranchePrice + .whenCalledWith(currentTranchesIn[1].address) + .returns(toPriceFixedPtAmt("1")); + await discountStrategy.computeTrancheDiscount + .whenCalledWith(currentTranchesIn[1].address) + .returns(toDiscountFixedPtAmt("1")); + + await collateralToken.transfer(vault.address, toFixedPtAmt("10")); + + await vault.deploy(); + await checkVaultAssetComposition( + vault, + [collateralToken, currentTranchesIn[2], perp], + [toFixedPtAmt("5"), toFixedPtAmt("5"), toFixedPtAmt("0")], + ); + expect(await vault.deployedCount()).to.eq(1); + expect(await vault.deployedAt(0)).to.eq(currentTranchesIn[2].address); + }); + describe("when address is not valid", function () { + it("should be reverted", async function () { + await expect(vault["recover(address)"](collateralToken.address)).to.be.revertedWith("UnexpectedAsset"); + }); + }); + describe("when belongs to a malicious tranche", function () { + it("should be reverted", async function () { + const maliciousBond = await createBondWithFactory(bondFactory, collateralToken, [1, 999], 100000000000); + await collateralToken.approve(maliciousBond.address, toFixedPtAmt("1")); + await maliciousBond.deposit(toFixedPtAmt("1")); + const maliciousTranches = await getTranches(maliciousBond); + await maliciousTranches[1].transfer( + vault.address, + maliciousTranches[1].balanceOf(await deployer.getAddress()), + ); + await expect(vault["recover(address)"](maliciousTranches[1].address)).to.be.revertedWith("UnexpectedAsset"); + }); + }); + describe("when its not mature", function () { + it("should be a no-op", async function () { + await expect(vault["recover(address)"](currentTranchesIn[2].address)).not.to.be.reverted; + await checkVaultAssetComposition( + vault, + [collateralToken, currentTranchesIn[2], perp], + [toFixedPtAmt("5"), toFixedPtAmt("5"), toFixedPtAmt("0")], + ); + expect(await vault.deployedCount()).to.eq(1); + expect(await vault.deployedAt(0)).to.eq(currentTranchesIn[2].address); + }); + }); + describe("when its mature", function () { + beforeEach(async function () { + await advancePerpQueueUpToBondMaturity(perp, currentBondIn); + }); + it("should recover", async function () { + await expect(vault["recover(address)"](currentTranchesIn[2].address)).not.to.be.reverted; + expect(await vault.deployedCount()).to.eq(0); + await checkVaultAssetComposition(vault, [collateralToken, perp], [toFixedPtAmt("10"), toFixedPtAmt("0")]); + }); + it("should sync assets", async function () { + const tx = vault["recover()"](); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(collateralToken.address, toFixedPtAmt("10")); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(currentTranchesIn[2].address, toFixedPtAmt("0")); + }); + }); + }); + + describe("when many assets are deployed", function () { + let currentBondIn: Contract, currentTranchesIn: Contract[], newBondIn: Contract, newTranchesIn: Contract[]; + beforeEach(async function () { + await advancePerpQueueToBondMaturity(perp, rolloverInBond); + currentBondIn = await bondAt(await perp.callStatic.getDepositBond()); + currentTranchesIn = await getTranches(currentBondIn); + + await pricingStrategy.computeTranchePrice + .whenCalledWith(currentTranchesIn[0].address) + .returns(toPriceFixedPtAmt("1")); + await discountStrategy.computeTrancheDiscount + .whenCalledWith(currentTranchesIn[0].address) + .returns(toDiscountFixedPtAmt("1")); + + await collateralToken.transfer(vault.address, toFixedPtAmt("10")); + await vault.deploy(); + + await checkVaultAssetComposition( + vault, + [collateralToken, currentTranchesIn[1], currentTranchesIn[2], perp], + [toFixedPtAmt("2"), toFixedPtAmt("3"), toFixedPtAmt("5"), toFixedPtAmt("0")], + ); + expect(await vault.deployedCount()).to.eq(2); + expect(await vault.deployedAt(0)).to.eq(currentTranchesIn[2].address); + expect(await vault.deployedAt(1)).to.eq(currentTranchesIn[1].address); + }); + + describe("when no redemption", function () { + it("should be a no-op", async function () { + await expect(vault["recover(address)"](currentTranchesIn[1].address)).not.to.be.reverted; + await checkVaultAssetComposition( + vault, + [collateralToken, currentTranchesIn[1], currentTranchesIn[2], perp], + [toFixedPtAmt("2"), toFixedPtAmt("3"), toFixedPtAmt("5"), toFixedPtAmt("0")], + ); + expect(await vault.deployedCount()).to.eq(2); + expect(await vault.deployedAt(0)).to.eq(currentTranchesIn[2].address); + expect(await vault.deployedAt(1)).to.eq(currentTranchesIn[1].address); + }); + }); + + describe("when mature redemption", function () { + beforeEach(async function () { + await advancePerpQueueToBondMaturity(perp, currentBondIn); + }); + it("should recover", async function () { + await expect(vault["recover(address)"](currentTranchesIn[1].address)).not.to.be.reverted; + await checkVaultAssetComposition( + vault, + [collateralToken, currentTranchesIn[2], perp], + [toFixedPtAmt("5"), toFixedPtAmt("5"), toFixedPtAmt("0")], + ); + expect(await vault.deployedCount()).to.eq(1); + expect(await vault.deployedAt(0)).to.eq(currentTranchesIn[2].address); + }); + it("should sync assets", async function () { + const tx = vault["recover(address)"](currentTranchesIn[1].address); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(collateralToken.address, toFixedPtAmt("5")); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(currentTranchesIn[1].address, toFixedPtAmt("0")); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(currentTranchesIn[2].address, toFixedPtAmt("5")); + }); + }); + + describe("when immature redemption", function () { + beforeEach(async function () { + await advancePerpQueueToRollover(perp, currentBondIn); + + newBondIn = await bondAt(await perp.callStatic.getDepositBond()); + newTranchesIn = await getTranches(newBondIn); + + await pricingStrategy.computeTranchePrice + .whenCalledWith(newTranchesIn[0].address) + .returns(toPriceFixedPtAmt("1")); + await discountStrategy.computeTrancheDiscount + .whenCalledWith(newTranchesIn[0].address) + .returns(toDiscountFixedPtAmt("1")); + + await collateralToken.transfer(vault.address, toFixedPtAmt("9998")); + await vault.deploy(); + + expect(await vault.deployedCount()).to.eq(5); + await checkVaultAssetComposition( + vault, + [ + collateralToken, + currentTranchesIn[0], + currentTranchesIn[1], + currentTranchesIn[2], + newTranchesIn[1], + newTranchesIn[2], + perp, + ], + [ + toFixedPtAmt("1998"), + toFixedPtAmt("2"), + toFixedPtAmt("3"), + toFixedPtAmt("5"), + toFixedPtAmt("3000"), + toFixedPtAmt("5000"), + toFixedPtAmt("0"), + ], + ); + }); + + describe("without reminder", function () { + it("should recover", async function () { + await expect(vault["recover(address)"](currentTranchesIn[1].address)).not.to.be.reverted; + expect(await vault.deployedCount()).to.eq(2); + await checkVaultAssetComposition( + vault, + [collateralToken, newTranchesIn[1], newTranchesIn[2], perp], + [toFixedPtAmt("2008"), toFixedPtAmt("3000"), toFixedPtAmt("5000"), toFixedPtAmt("0")], + ); + }); + it("should sync assets", async function () { + const tx = vault["recover()"](); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(collateralToken.address, toFixedPtAmt("2008")); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(currentTranchesIn[0].address, toFixedPtAmt("0")); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(currentTranchesIn[1].address, toFixedPtAmt("0")); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(currentTranchesIn[2].address, toFixedPtAmt("0")); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(newTranchesIn[1].address, toFixedPtAmt("3000")); + await expect(tx).to.emit(vault, "AssetSynced").withArgs(newTranchesIn[2].address, toFixedPtAmt("5000")); + }); + }); + + describe("with reminder", function () { + beforeEach(async function () { + await depositIntoBond(currentBondIn, toFixedPtAmt("1000"), deployer); + await currentTranchesIn[2].transfer(vault.address, toFixedPtAmt("1")); + expect(await vault.deployedCount()).to.eq(5); + await checkVaultAssetComposition( + vault, + [ + collateralToken, + currentTranchesIn[0], + currentTranchesIn[1], + currentTranchesIn[2], + newTranchesIn[1], + newTranchesIn[2], + perp, + ], + [ + toFixedPtAmt("1998"), + toFixedPtAmt("2"), + toFixedPtAmt("3"), + toFixedPtAmt("6"), + toFixedPtAmt("3000"), + toFixedPtAmt("5000"), + toFixedPtAmt("0"), + ], + ); + }); + it("should recover", async function () { + await expect(vault["recover(address)"](currentTranchesIn[0].address)).not.to.be.reverted; + expect(await vault.deployedCount()).to.eq(3); + await checkVaultAssetComposition( + vault, + [collateralToken, currentTranchesIn[2], newTranchesIn[1], newTranchesIn[2], perp], + [toFixedPtAmt("2008"), toFixedPtAmt("1"), toFixedPtAmt("3000"), toFixedPtAmt("5000"), toFixedPtAmt("0")], + ); + }); + it("should sync assets", async function () { + const tx = vault["recover()"](); await expect(tx).to.emit(vault, "AssetSynced").withArgs(collateralToken.address, toFixedPtAmt("2008")); await expect(tx).to.emit(vault, "AssetSynced").withArgs(currentTranchesIn[0].address, toFixedPtAmt("0")); await expect(tx).to.emit(vault, "AssetSynced").withArgs(currentTranchesIn[1].address, toFixedPtAmt("0")); From 5a2154db526e582a9fee95485dc8c6b7682fa627 Mon Sep 17 00:00:00 2001 From: aalavandhan1984 <6264334+aalavandhan@users.noreply.github.com> Date: Thu, 27 Apr 2023 18:59:10 -0400 Subject: [PATCH 2/4] updated exec code style --- .../contracts/vaults/RolloverVault.sol | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/spot-contracts/contracts/vaults/RolloverVault.sol b/spot-contracts/contracts/vaults/RolloverVault.sol index 566ad440..3a658ff6 100644 --- a/spot-contracts/contracts/vaults/RolloverVault.sol +++ b/spot-contracts/contracts/vaults/RolloverVault.sol @@ -439,7 +439,6 @@ contract RolloverVault is /// which wrap this function with the internal book-keeping necessary to keep track of the vault's assets. function _execTrancheRedemption(ITranche tranche) private returns (TrancheData memory) { IBondController bond = IBondController(tranche.bond()); - TrancheData memory td; // if bond has matured, redeem the tranche token if (bond.timeToMaturity() <= 0) { @@ -447,28 +446,26 @@ contract RolloverVault is bond.mature(); } - td = bond.getTrancheData(); uint256 trancheBalance = tranche.balanceOf(address(this)); - if (trancheBalance <= 0) { - return td; + if (trancheBalance > 0) { + bond.redeemMature(address(tranche), trancheBalance); } - bond.redeemMature(address(tranche), trancheBalance); + return bond.getTrancheData(); } // else redeem using proportional balances, redeems all tranches part of the bond else { uint256[] memory trancheAmts; + TrancheData memory td; (td, trancheAmts) = bond.computeRedeemableTrancheAmounts(address(this)); // NOTE: It is guaranteed that if one tranche amount is zero, all amounts are zeros. - if (trancheAmts[0] <= 0) { - return td; + if (trancheAmts[0] > 0) { + bond.redeem(trancheAmts); } - bond.redeem(trancheAmts); + return td; } - - return td; } /// @dev Syncs balance and adds the given asset into the deployed list if the vault has a balance. From a270f4863868528bb399c2f3e46838b0441471e9 Mon Sep 17 00:00:00 2001 From: aalavandhan1984 <6264334+aalavandhan@users.noreply.github.com> Date: Fri, 28 Apr 2023 08:31:33 -0400 Subject: [PATCH 3/4] added natspec comment --- spot-contracts/contracts/_interfaces/IVault.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/spot-contracts/contracts/_interfaces/IVault.sol b/spot-contracts/contracts/_interfaces/IVault.sol index b12289c9..f19c2e4d 100644 --- a/spot-contracts/contracts/_interfaces/IVault.sol +++ b/spot-contracts/contracts/_interfaces/IVault.sol @@ -50,6 +50,7 @@ interface IVault { function recover() external; /// @notice Recovers a given deployed asset. + /// @param token The ERC-20 token address of the deployed asset. function recover(IERC20Upgradeable token) external; /// @notice Deposits the underlying asset from {msg.sender} into the vault and mints notes. From 8a89c071a03a082da0374ea833f71b76238f225c Mon Sep 17 00:00:00 2001 From: aalavandhan1984 <6264334+aalavandhan@users.noreply.github.com> Date: Fri, 28 Apr 2023 08:45:32 -0400 Subject: [PATCH 4/4] removed one-time use method --- spot-contracts/contracts/vaults/RolloverVault.sol | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/spot-contracts/contracts/vaults/RolloverVault.sol b/spot-contracts/contracts/vaults/RolloverVault.sol index 3a658ff6..eacc8e04 100644 --- a/spot-contracts/contracts/vaults/RolloverVault.sol +++ b/spot-contracts/contracts/vaults/RolloverVault.sol @@ -217,7 +217,7 @@ contract RolloverVault is // calculating amounts and transferring assets out proportionally for (uint256 i = 0; i < assetCount; i++) { - redemptions[i].amount = _calculateAssetShare(redemptions[i].token, notes, totalNotes); + redemptions[i].amount = redemptions[i].token.balanceOf(address(this)).mulDiv(notes, totalNotes); redemptions[i].token.safeTransfer(_msgSender(), redemptions[i].amount); _syncAsset(redemptions[i].token); } @@ -504,16 +504,4 @@ contract RolloverVault is return balance; } - - //-------------------------------------------------------------------------- - // Private read methods - - /// @dev Computes the proportional share of the vault's asset token balance for a given amount of notes. - function _calculateAssetShare( - IERC20Upgradeable asset, - uint256 notes, - uint256 totalNotes - ) private view returns (uint256) { - return asset.balanceOf(address(this)).mulDiv(notes, totalNotes); - } }