Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Individual recovery #146

Merged
merged 5 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
brandoniles marked this conversation as resolved.
Show resolved Hide resolved
/// @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) {
brandoniles marked this conversation as resolved.
Show resolved Hide resolved
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);
brandoniles marked this conversation as resolved.
Show resolved Hide resolved

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