Skip to content

Commit

Permalink
Individual recovery (#146)
Browse files Browse the repository at this point in the history
* Public method to recover individual assets"

* updated exec code style

* added natspec comment

* removed one-time use method
  • Loading branch information
aalavandhan authored Apr 28, 2023
1 parent ebc9a46 commit 084a78f
Show file tree
Hide file tree
Showing 6 changed files with 463 additions and 109 deletions.
4 changes: 4 additions & 0 deletions spot-contracts/contracts/_interfaces/IVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ interface IVault {
/// @notice Recovers deployed funds.
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.
/// @param amount The amount tokens to be deposited into the vault.
/// @return The amount of notes.
Expand Down
147 changes: 92 additions & 55 deletions spot-contracts/contracts/vaults/RolloverVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -207,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);
}
Expand Down Expand Up @@ -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();
Expand All @@ -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;
}

Expand All @@ -301,14 +312,15 @@ contract RolloverVault is

// sync holdings
for (uint8 i = 0; i < td.trancheCount; i++) {
_syncDeployedAsset(td.tranches[i]);
_syncAndAddDeployedAsset(td.tranches[i]);
}
_syncAsset(underlying);

return td;
}

/// @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,
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -385,61 +397,80 @@ 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
// NOTE: We traverse the deployed set in the reverse order
// 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 Syncs balance and keeps the deployed assets list up to date.
/// @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());

// if bond has matured, redeem the tranche token
if (bond.timeToMaturity() <= 0) {
if (!bond.isMature()) {
bond.mature();
}

uint256 trancheBalance = tranche.balanceOf(address(this));
if (trancheBalance > 0) {
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) {
bond.redeem(trancheAmts);
}

return td;
}
}

/// @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));

Expand All @@ -451,23 +482,29 @@ contract RolloverVault is
}
}

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

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);
/// @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;
}
}
7 changes: 5 additions & 2 deletions spot-contracts/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,14 @@ export const advancePerpQueue = async (perp: Contract, time: number): Promise<Tr
return perp.updateState();
};

export const advancePerpQueueToBondMaturity = async (perp: Contract, bond: Contract): Promise<Transaction> => {
export const advancePerpQueueUpToBondMaturity = async (perp: Contract, bond: Contract): Promise<Transaction> => {
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<Transaction> => {
await advancePerpQueueUpToBondMaturity(perp, bond);
await TimeHelpers.increaseTime(1);
return perp.updateState();
};
Expand Down
67 changes: 45 additions & 22 deletions spot-contracts/test/strategies/CDRPricingStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,51 +56,74 @@ 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");
});
});
});
});
});
});

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(
Expand Down
Loading

0 comments on commit 084a78f

Please sign in to comment.