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);
- }
}