Skip to content
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
9 changes: 9 additions & 0 deletions spot-contracts/contracts/_utils/BondHelpers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ library BondHelpers {
return td;
}

/// @notice Given a bond, returns the tranche at the specified index.
/// @param b The address of the bond contract.
/// @param i Index of the tranche.
/// @return t The tranche address.
function trancheAt(IBondController b, uint8 i) internal view returns (ITranche t) {
(t, ) = b.tranches(i);
return t;
}

/// @notice Helper function to estimate the amount of tranches minted when a given amount of collateral
/// is deposited into the bond.
/// @dev This function is used off-chain services (using callStatic) to preview tranches minted after
Expand Down
124 changes: 88 additions & 36 deletions spot-contracts/contracts/vaults/RolloverVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -195,34 +195,88 @@ contract RolloverVault is

// execute redemption on each deployed asset
for (uint256 i = 0; i < deployedCount_; i++) {
_execTrancheRedemption(ITranche(_deployed.at(i)));
ITranche tranche = ITranche(_deployed.at(i));
uint256 trancheBalance = tranche.balanceOf(address(this));

// if the vault has no tranche balance,
// we update our internal book-keeping and continue to the next one.
if (trancheBalance <= 0) {
continue;
}

// get the parent bond
IBondController bond = IBondController(tranche.bond());

// if bond has matured, redeem the tranche token
if (bond.secondsToMaturity() <= 0) {
// execute redemption
_execMatureTrancheRedemption(bond, tranche, trancheBalance);
}

// if not redeem using proportional balances
// redeems this tranche and it's siblings if the vault holds balances.
// NOTE: For gas optimization, we perform this operation only once
// ie) when we encounter the most-senior tranche.
else if (tranche == bond.trancheAt(0)) {
// execute redemption
_execImmatureTrancheRedemption(bond);
}
}

// sync holdings
// sync deployed tranches
// 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--) {
_syncAndRemoveDeployedAsset(IERC20Upgradeable(_deployed.at(i - 1)));
}

// sync underlying
_syncAsset(underlying);
}

/// @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 tranche) external override nonReentrant whenNotPaused {
if (!_deployed.contains(address(tranche))) {
revert UnexpectedAsset(tranche);
function recover(IERC20Upgradeable token) external override nonReentrant whenNotPaused {
if (!_deployed.contains(address(token))) {
revert UnexpectedAsset(token);
}

BondTranches memory td = _execTrancheRedemption(ITranche(address(tranche)));
ITranche tranche = ITranche(address(token));
uint256 trancheBalance = tranche.balanceOf(address(this));

// sync holdings
// Note: Immature redemption, may remove sibling tranches from the deployed list.
for (uint8 i = 0; i < td.tranches.length; i++) {
_syncAndRemoveDeployedAsset(td.tranches[i]);
// if the vault has no tranche balance,
// we update our internal book-keeping and return.
if (trancheBalance <= 0) {
_syncAndRemoveDeployedAsset(tranche);
return;
}

// get the parent bond
IBondController bond = IBondController(tranche.bond());

// if bond has matured, redeem the tranche token
if (bond.secondsToMaturity() <= 0) {
// execute redemption
_execMatureTrancheRedemption(bond, tranche, trancheBalance);

// sync deployed asset
_syncAndRemoveDeployedAsset(tranche);
}
// if not redeem using proportional balances
// redeems this tranche and it's siblings if the vault holds balances.
else {
// execute redemption
BondTranches memory td = _execImmatureTrancheRedemption(bond);

// sync deployed asset, ie current tranche and all its siblings.
for (uint8 j = 0; j < td.tranches.length; j++) {
_syncAndRemoveDeployedAsset(td.tranches[j]);
}
}

// sync underlying
_syncAsset(underlying);
}

Expand Down Expand Up @@ -468,40 +522,38 @@ contract RolloverVault is
return totalPerpRolledOver;
}

/// @dev Low level method that redeems the given deployed tranche tokens for the underlying asset.
/// @dev Low level method that redeems the given mature tranche for the underlying asset.
/// It interacts with the button-wood bond contract.
/// This function should NOT be called directly, use `recover()` or `recover(tranche)`
/// which wrap this function with the internal book-keeping necessary,
/// to keep track of the vault's assets.
function _execTrancheRedemption(ITranche tranche) private returns (BondTranches memory) {
IBondController bond = IBondController(tranche.bond());
uint256 trancheBalance = tranche.balanceOf(address(this));

if (trancheBalance <= 0) {
return bond.getTranches();
function _execMatureTrancheRedemption(
IBondController bond,
ITranche tranche,
uint256 amount
) private {
if (!bond.isMature()) {
bond.mature();
}
bond.redeemMature(address(tranche), amount);
}

// if bond has matured, redeem the tranche token
if (bond.secondsToMaturity() <= 0) {
if (!bond.isMature()) {
bond.mature();
}
/// @dev Low level method that redeems the given tranche for the underlying asset, before maturity.
/// If the vault holds sibling tranches with proportional balances, those will also get redeemed.
/// It interacts with the button-wood bond contract.
/// This function should NOT be called directly, use `recover()` or `recover(tranche)`
/// which wrap this function with the internal book-keeping necessary,
/// to keep track of the vault's assets.
function _execImmatureTrancheRedemption(IBondController bond) private returns (BondTranches memory td) {
uint256[] memory trancheAmts;
(td, trancheAmts) = bond.computeRedeemableTrancheAmounts(address(this));

bond.redeemMature(address(tranche), trancheBalance);
return bond.getTranches();
// NOTE: It is guaranteed that if one tranche amount is zero, all amounts are zeros.
if (trancheAmts[0] > 0) {
bond.redeem(trancheAmts);
}
// else redeem using proportional balances, redeems all tranches part of the bond
else {
uint256[] memory trancheAmts;
BondTranches 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;
}
return td;
}

/// @dev Syncs balance and adds the given asset into the deployed list if the vault has a balance.
Expand Down
1 change: 0 additions & 1 deletion spot-contracts/test/vaults/RolloverVault_recover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,6 @@ describe("RolloverVault", 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"));
});
});

Expand Down