From 4a1c8de4d17ca08ab8a214def700d2de845ec805 Mon Sep 17 00:00:00 2001 From: nithin Date: Thu, 29 Dec 2022 09:06:53 +0530 Subject: [PATCH] intermediate --- .../contracts/_utils/BondHelpers.sol | 32 +- .../contracts/test/BondHelpersTester.sol | 16 +- .../test/mocks/MockPerpetualTranche.sol | 5 + .../contracts/vaults/RolloverVault.sol | 528 ++++++++++++++++++ spot-contracts/test/_utils/BondHelpers.ts | 148 +---- 5 files changed, 561 insertions(+), 168 deletions(-) create mode 100644 spot-contracts/contracts/vaults/RolloverVault.sol diff --git a/spot-contracts/contracts/_utils/BondHelpers.sol b/spot-contracts/contracts/_utils/BondHelpers.sol index dde9aca3..bc3ffad6 100644 --- a/spot-contracts/contracts/_utils/BondHelpers.sol +++ b/spot-contracts/contracts/_utils/BondHelpers.sol @@ -191,27 +191,33 @@ library BondHelpers { return (td, collateralBalances, trancheSupplies); } - /// @notice Given a bond, retrieves the collateral redeemable for - /// each tranche held by the given address. + /// @notice Given a bond and a user address computes the tranche amounts proportional to the tranche ratios, + // that can be redeemed for the collateral before maturity. /// @param b The address of the bond contract. /// @param u The address to check balance for. - /// @return The tranche data and an array of collateral balances. - function getTrancheCollateralBalances(IBondController b, address u) + /// @return The tranche data and an array of tranche token balances. + function computeRedeemableTrancheAmounts(IBondController b, address u) internal view returns (TrancheData memory, uint256[] memory) { - TrancheData memory td; - uint256[] memory collateralBalances; - uint256[] memory trancheSupplies; - - (td, collateralBalances, trancheSupplies) = getTrancheCollateralizations(b); + TrancheData memory td = getTrancheData(b); + uint256[] memory redeemableAmts = new uint256[](td.trancheCount); + + // We calculate the minimum value of {trancheBal/trancheRatio} across tranches + uint256 min = type(uint256).max; + uint8 i; + for (i = 0; i < td.trancheCount && min != 0; i++) { + uint256 d = (td.tranches[i].balanceOf(u) * TRANCHE_RATIO_GRANULARITY) / td.trancheRatios[i]; + if (d < min) { + min = d; + } + } - uint256[] memory balances = new uint256[](td.trancheCount); - for (uint8 i = 0; i < td.trancheCount; i++) { - balances[i] = (td.tranches[i].balanceOf(u) * collateralBalances[i]) / trancheSupplies[i]; + for (i = 0; i < td.trancheCount; i++) { + redeemableAmts[i] = (td.trancheRatios[i] * min) / TRANCHE_RATIO_GRANULARITY; } - return (td, balances); + return (td, redeemableAmts); } } diff --git a/spot-contracts/contracts/test/BondHelpersTester.sol b/spot-contracts/contracts/test/BondHelpersTester.sol index 103ef55f..0d8a54f4 100644 --- a/spot-contracts/contracts/test/BondHelpersTester.sol +++ b/spot-contracts/contracts/test/BondHelpersTester.sol @@ -33,14 +33,6 @@ contract BondHelpersTester { return b.previewDeposit(collateralAmount); } - function getTrancheCollateralBalances(IBondController b, address u) - public - view - returns (TrancheData memory, uint256[] memory) - { - return b.getTrancheCollateralBalances(u); - } - function getTrancheCollateralizations(IBondController b) public view @@ -57,4 +49,12 @@ contract BondHelpersTester { TrancheData memory td = b.getTrancheData(); return td.getTrancheIndex(t); } + + function computeRedeemableTrancheAmounts(IBondController b, address u) + public + view + returns (TrancheData memory td, uint256[] memory) + { + return b.computeRedeemableTrancheAmounts(u); + } } diff --git a/spot-contracts/contracts/test/mocks/MockPerpetualTranche.sol b/spot-contracts/contracts/test/mocks/MockPerpetualTranche.sol index 33256cf6..3e3d68c6 100644 --- a/spot-contracts/contracts/test/mocks/MockPerpetualTranche.sol +++ b/spot-contracts/contracts/test/mocks/MockPerpetualTranche.sol @@ -7,6 +7,7 @@ contract MockPerpetualTranche is MockERC20 { uint256 private _reserveTrancheBalance; uint256 public matureTrancheBalance; address public collateral; + address public feeToken; function protocolFeeCollector() public view returns (address) { return address(this); @@ -29,4 +30,8 @@ contract MockPerpetualTranche is MockERC20 { function setCollateral(address c) external { collateral = c; } + + function setFeeToken(address c) external { + feeToken = c; + } } diff --git a/spot-contracts/contracts/vaults/RolloverVault.sol b/spot-contracts/contracts/vaults/RolloverVault.sol new file mode 100644 index 00000000..98aee2e8 --- /dev/null +++ b/spot-contracts/contracts/vaults/RolloverVault.sol @@ -0,0 +1,528 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import { MathUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; + +import { ERC20BurnableUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; +import { EnumerableSetUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol"; +import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; + +import { TrancheData, TrancheHelpers, BondHelpers } from "../_utils/BondHelpers.sol"; +import { IERC20Upgradeable, IPerpetualTranche, IBondIssuer, IBondController, ITranche } from "../_interfaces/IPerpetualTranche.sol"; + +// import "hardhat/console.sol"; + +/// @notice Expected assets to be deployed. +error NoDeployment(); + +/// @notice Expected asset to be a vault asset. +/// @param token Address of the token. +error UnexpectedAsset(IERC20Upgradeable token); + +/// @notice Expected transfer out asset to not be a vault asset. +/// @param token Address of the token transferred. +error UnauthorizedTransferOut(IERC20Upgradeable token); + +/* + * @title RolloverVault + * + * @notice A vault which generates yield (from fees) by performing rollovers on PerpetualTranche (or perp). + * + * Users deposit a "underlying" asset (like AMPL or any other rebasing collateral) for "notes". + * The vault "deploys" underlying asset in a rules-based fashion to "earn" income. + * It "recovers" deployed assets once the investment matures. + * + * The vault operates though two external poke which off-chain keepers can execute. + * 1) deploy: When executed, the vault "tranches" the underlying asset, + * swaps these fresh tranches for near mature (or mature tranches) from the perp + * system through a rollover operation and earns an income in perp tokens. + * 2) recover: When executed, the vault redeems tranches for the deposit asset. + * NOTE: it performs both mature and immature redemption. Read more: https://bit.ly/3tuN6OC + * + * At any time the vault will hold multiple ERC20 tokens, together referred to as the vault's "assets". + * They can be a combination of the underlying asset, the earned asset and multiple tranches (deployed assets). + * + * On redemption users burn their "notes" to receive a proportional slice of all the vault's holding tokens. + * + * + */ +contract RolloverVault is + ERC20BurnableUpgradeable, + OwnableUpgradeable, + PausableUpgradeable, + ReentrancyGuardUpgradeable +{ + // data handling + using EnumerableSetUpgradeable for EnumerableSetUpgradeable.AddressSet; + using BondHelpers for IBondController; + using TrancheHelpers for ITranche; + + // ERC20 operations + using SafeERC20Upgradeable for IERC20Upgradeable; + + // math + using MathUpgradeable for uint256; + + //------------------------------------------------------------------------- + // Events + + /// @notice Event emitted the vault's current asset token balance is recorded after change. + /// @param token Address of token. + /// @param balance The recorded ERC-20 balance of the token. + event AssetSynced(IERC20Upgradeable token, uint256 balance); + + //------------------------------------------------------------------------- + // Constants + uint8 public constant PERC_DECIMALS = 6; + uint256 public constant UNIT_PERC = 10**PERC_DECIMALS; + uint256 public constant HUNDRED_PERC = 100 * UNIT_PERC; + + /// @dev Initial micro-deposit into the vault so that the totalSupply is never zero. + uint256 public constant INITIAL_DEPOSIT = 10**9; + + /// @dev Values should line up as is in the perp contract. + uint8 private constant PERP_PRICE_DECIMALS = 8; + uint256 private constant PERP_UNIT_PRICE = (10**PERP_PRICE_DECIMALS); + + //------------------------------------------------------------------------- + // Data + + /// @notice Address of the perp contract to be rolled over. + IPerpetualTranche public perp; + + //-------------------------------------------------------------------------- + // ASSETS + // + // The vault's asset holdings are represented by list of ERC-20 tokens + // => { [underlying] U [earned] U _deployed } + + /// @notice The ERC20 token that can be deposited into this vault. + IERC20Upgradeable public underlying; + + /// @notice The ERC-20 token earned as income for vault operations. + IERC20Upgradeable public earned; + + /// @dev A record of the intermediate ERC-20 tokens when the underlying asset has been put to use. + /// In the cause of this vault, they represent the tranche tokens held till maturity. + EnumerableSetUpgradeable.AddressSet private _deployed; + + //-------------------------------------------------------------------------- + // Construction & Initialization + + /// @notice Contract state initialization. + /// @param name ERC-20 Name of the vault token. + /// @param symbol ERC-20 Symbol of the vault token. + /// @param perp_ ERC-20 address of the perp contract. + /// @param initialRate Initial exchange rate between notes and the deposit asset tokens. + function init( + string memory name, + string memory symbol, + IPerpetualTranche perp_, + uint256 initialRate + ) public initializer { + __ERC20_init(name, symbol); + __Ownable_init(); + + perp = perp_; + + underlying = perp.collateral(); + earned = IERC20Upgradeable(address(perp)); + + // initial micro-deposit and mint + underlying.safeTransferFrom(_msgSender(), address(this), INITIAL_DEPOSIT); + _mint(address(this), initialRate * INITIAL_DEPOSIT); + } + + //-------------------------------------------------------------------------- + // ADMIN only methods + + /// @notice Pauses deposits, withdrawals and vault operations. + /// @dev NOTE: ERC-20 functions, like transfers will always remain operational. + function pause() public onlyOwner { + _pause(); + } + + /// @notice Unpauses deposits, withdrawals and vault operations. + /// @dev NOTE: ERC-20 functions, like transfers will always remain operational. + function unpause() public onlyOwner { + _unpause(); + } + + /// @notice Allows the owner to transfer non vault assets out of the vault if required. + /// @param token The token address. + /// @param to The destination address. + /// @param amount The amount of tokens to be transferred. + function transferERC20( + IERC20Upgradeable token, + address to, + uint256 amount + ) external onlyOwner { + if (isVaultAsset(token)) { + revert UnauthorizedTransferOut(token); + } + token.safeTransfer(to, amount); + } + + //-------------------------------------------------------------------------- + // External & Public write methods + + /// @notice Recovers deployed funds and redeploys them. + /// @dev Simply batches the `recover` and `deploy` functions. + function recoverAndRedeploy() external whenNotPaused { + recover(); + deploy(); + } + + /// @notice Deploys deposited funds. + function deploy() public whenNotPaused { + IBondController depositBond = trancheUnderlying(); + TrancheData memory td = depositBond.getTrancheData(); + + // Tranches up for rollover + // NOTE: The first element of the list is the mature tranche, + // there after the list is NOT ordered by maturity. + IERC20Upgradeable[] memory rolloverTokens = perp.getReserveTokensUpForRollover(); + + // Batch rollover + uint8 i = 0; + uint256 j = 0; + uint256 totalRolloverAmt = 0; + while (i < td.trancheCount && j < rolloverTokens.length) { + // For each tranche token, and each token up for rollover + // Execute rollover and move on to the next pair + (bool trancheInExhausted, uint256 rolloverAmt) = executeRollover(td.tranches[i], rolloverTokens[j]); + if (trancheInExhausted) { + i++; + } else { + j++; + } + totalRolloverAmt += rolloverAmt; + } + + if (totalRolloverAmt == 0) { + revert NoDeployment(); + } + } + + /// @notice Recovers deployed funds. + function recover() public whenNotPaused { + // Batch redemption + // NOTE: we skip can _deployed[0], i.e the deposit asset. + for (uint256 i = 1; i < _deployed.length(); i++) { + redeemTranche(ITranche(_deployed.at(i))); + } + } + + /// @notice Deposits the provided asset from {msg.sender} into the vault and mints notes. + /// @param token The address of the asset to deposit. + /// @param amount The amount tokens to be deposited into the vault. + /// @return The amount of notes. + function deposit(IERC20Upgradeable token, uint256 amount) external nonReentrant whenNotPaused returns (uint256) { + // NOTE: The vault only accepts the underlying asset tokens. + if (token != underlying) { + revert UnexpectedAsset(token); + } + + uint256 notes = amount.mulDiv(totalSupply(), getTVL()); + + underlying.safeTransferFrom(_msgSender(), address(this), amount); + _syncAsset(underlying); + + _mint(_msgSender(), notes); + return notes; + } + + /// @notice Burns notes and sends a proportional share vault's assets back to {msg.sender}. + /// @param notes The amount of notes to be burnt. + /// @return The list of asset tokens and amounts. + function redeem(uint256 notes) + external + nonReentrant + whenNotPaused + returns (IERC20Upgradeable[] memory, uint256[] memory) + { + _burn(_msgSender(), notes); + + uint256 totalNotes = totalSupply(); + uint256 nAssets = 2 + _deployed.length(); + IERC20Upgradeable[] memory tokens = new IERC20Upgradeable[](nAssets); + uint256[] memory amounts = new uint256[](nAssets); + + // underlying asset share + tokens[0] = underlying; + amounts[0] = _calculateAssetShare(underlying, notes, totalNotes); + tokens[0].safeTransfer(_msgSender(), amounts[0]); + _syncAsset(tokens[0]); + + // earned asset share + tokens[1] = earned; + amounts[1] = _calculateAssetShare(earned, notes, totalNotes); + tokens[1].safeTransfer(_msgSender(), amounts[1]); + _syncAsset(tokens[1]); + + // deployed assets share + for (uint256 i = 2; i < nAssets; i++) { + tokens[i] = IERC20Upgradeable(_deployed.at(i - 2)); + amounts[i] = _calculateAssetShare(tokens[i], notes, totalNotes); + tokens[i].safeTransfer(_msgSender(), amounts[i]); + _syncAsset(tokens[i]); + } + + return (tokens, amounts); + } + + /// @notice Deposits underlying balance into the perp's active deposit bond. + /// @return The active deposit bond. + function trancheUnderlying() public nonReentrant whenNotPaused returns (IBondController) { + // current bond + IBondController depositBond = perp.getDepositBond(); + + // Get underlying balance + uint256 balance = underlying.balanceOf(address(this)); + + // Ensure initial deposit remains unspent + balance = (balance > INITIAL_DEPOSIT) ? balance - INITIAL_DEPOSIT : 0; + if (balance == 0) { + return depositBond; + } + + // usable balance is tranched + _checkAndApproveMax(underlying, address(depositBond), balance); + depositBond.deposit(balance); + + // sync holdings + TrancheData memory td = depositBond.getTrancheData(); + for (uint8 i = 0; i < td.trancheCount; i++) { + _syncDeployedAsset(td.tranches[i]); + } + _syncAsset(underlying); + + return depositBond; + } + + /// @notice Rolls over a given trancheIn for a given tokenOut from perp. + /// @return If trancheIn tokens are exhausted by the rollover and the perp denominated rollover amount. + function executeRollover(ITranche trancheIn, IERC20Upgradeable tokenOut) + public + nonReentrant + whenNotPaused + returns (bool, uint256) + { + // Compute available tranche in + uint256 trancheInAmtAvailable = trancheIn.balanceOf(address(this)); + + // trancheIn tokens are exhausted + if (trancheInAmtAvailable == 0) { + // Rollover is a no-op, so skipping and returning + return (true, 0); + } + + // compute available token out + uint256 tokenOutAmtAvailable = address(tokenOut) != address(0) ? tokenOut.balanceOf(perp.reserve()) : 0; + + // trancheIn tokens are NOT exhausted but tokenOut is exhausted + if (tokenOutAmtAvailable == 0) { + // Rollover is a no-op, so skipping and returning + return (false, 0); + } + + // Preview rollover + IPerpetualTranche.RolloverPreview memory rd = perp.computeRolloverAmt( + trancheIn, + tokenOut, + trancheInAmtAvailable, + tokenOutAmtAvailable + ); + + // trancheIn isn't accepted by perp, likely because yield=0 + if (rd.perpRolloverAmt == 0) { + // Though `trancheInAmtAvailable` it is unusable so marking as exhausted + // Rollover is a no-op, so skipping and returning + return (true, 0); + } + + // TODO: handle case when fees need to paid by the vault for rolling over + // _checkAndApproveMax(earned, address(perp), type(uint256).max); + + // Perform rollover + _checkAndApproveMax(trancheIn, address(perp), rd.trancheInAmt); + perp.rollover(trancheIn, tokenOut, rd.trancheInAmt); + + // sync holdings + trancheInAmtAvailable = _syncDeployedAsset(trancheIn); + if (tokenOut != underlying) { + // when rolling over mature tranches, we skip + _syncDeployedAsset(tokenOut); + } + _syncAsset(earned); + _syncAsset(underlying); + + // return if trancheIn is exhausted after rollover and the rollover amount + return (trancheInAmtAvailable == 0, rd.perpRolloverAmt); + } + + /// @notice Redeems the deployed tranche tokens for the underlying asset. + /// @param tranche The address of the deployed tranche token to redeem. + function redeemTranche(ITranche tranche) public nonReentrant whenNotPaused { + // CRITICAL: prevents someone from transferring malicious tranche tokens funds into the vault + // and make the `_deployed` list large and run out of gas. + if (!_deployed.contains(address(tranche))) { + revert UnexpectedAsset(tranche); + } + + IBondController bond = IBondController(tranche.bond()); + + // if bond has mature, redeem the tranche token + if (bond.timeToMaturity() == 0) { + if (!bond.isMature()) { + bond.mature(); + } + bond.redeemMature(address(tranche), tranche.balanceOf(address(this))); + _syncDeployedAsset(tranche); + } + // 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) { + return; + } + + bond.redeem(trancheAmts); + for (uint8 i = 0; i < td.trancheCount; i++) { + _syncDeployedAsset(td.tranches[i]); + } + } + + // setup + _syncAsset(underlying); + } + + /// @notice Total value of assets currently held by the vault denominated in the underlying asset. + /// @dev TODO: This is EXPENSIVE!, Figure out when this will run out of gas. + /// `getTrancheCollateralization` gets called twice, once when iterating through the spot reserve + /// and again when iterating through the tranche vaults. + /// Each call `getTrancheCollateralization` call first accesses the parent bond, + /// then queries all tranches and balances. + /// On the bright side, even if this call runs out of gas it will never stop redemptions. + function getTVL() public returns (uint256) { + uint256 totalAssets = 0; + + // The underlying balance + totalAssets += underlying.balanceOf(address(this)); + + // The earned asset value denominated in the underlying + uint256 earnedBal = earned.balanceOf(address(this)); + if (earnedBal > 0) { + // The "earned" asset is assumed to be the perp token. + totalAssets += earnedBal.mulDiv(IPerpetualTranche(address(earned)).getAvgPrice(), PERP_UNIT_PRICE); + } + + // The deployed asset value denominated in the underlying + for (uint256 i = 0; i < _deployed.length(); i++) { + ITranche tranche = ITranche(_deployed.at(i)); + uint256 trancheBal = tranche.balanceOf(address(this)); + if (trancheBal > 0) { + (uint256 collateralBalance, uint256 debt) = tranche.getTrancheCollateralization(); + totalAssets += trancheBal.mulDiv(collateralBalance, debt); + } + } + + return totalAssets; + } + + //-------------------------------------------------------------------------- + // External & Public read methods + + /// @notice Total count of asset tokens held by the vault. + /// @return The asset count. + function assetCount() external view returns (uint256) { + return 2 + _deployed.length(); + } + + /// @notice The token address from the asset token list by index. + /// @param i The index of a token. + function assetAt(uint256 i) external view returns (IERC20Upgradeable) { + if (i == 0) { + return underlying; + } else if (i == 1) { + return earned; + } else { + return IERC20Upgradeable(_deployed.at(i - 2)); + } + } + + /// @notice Fetches the vault's asset token balance. + /// @param token The address of the asset ERC-20 token held by the vault. + /// @return The token balance. + function assetBalance(IERC20Upgradeable token) external view returns (uint256) { + return isVaultAsset(token) ? token.balanceOf(address(this)) : 0; + } + + /// @notice Checks if the given token is held by the vault. + /// @param token The address of a token to check. + function isVaultAsset(IERC20Upgradeable token) public view returns (bool) { + return token == underlying || token == earned || _deployed.contains(address(token)); + } + + //-------------------------------------------------------------------------- + // Private write methods + + /// @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; + } + + /// @dev Syncs balance and keeps the deployed assets list up to date. + /// @return The Vault's token balance. + function _syncDeployedAsset(IERC20Upgradeable token) private returns (uint256) { + uint256 balance = _syncAsset(token); + bool isHeld = _deployed.contains(address(token)); + + if (balance > 0 && !isHeld) { + // Inserts new token into the deployed assets list. + _deployed.add(address(token)); + } + + if (balance == 0 && isHeld) { + // Removes token into the deployed assets list. + _deployed.remove(address(token)); + } + + return balance; + } + + /// @dev Approves the spender to spend an infinite tokens from the vault's balance. + // NOTE: Only audited & immutable spender contracts should have infinite approvals. + function _checkAndApproveMax( + IERC20Upgradeable token, + address spender, + uint256 amount + ) private { + if (token.allowance(address(this), spender) < amount) { + token.approve(spender, type(uint256).max); + } + } + + //-------------------------------------------------------------------------- + // 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); + } +} diff --git a/spot-contracts/test/_utils/BondHelpers.ts b/spot-contracts/test/_utils/BondHelpers.ts index 90d0d557..988dfbcd 100644 --- a/spot-contracts/test/_utils/BondHelpers.ts +++ b/spot-contracts/test/_utils/BondHelpers.ts @@ -11,7 +11,6 @@ import { rebase, depositIntoBond, getTrancheBalances, - getTranches, } from "../helpers"; let bondFactory: Contract, @@ -20,18 +19,13 @@ let bondFactory: Contract, bondHelpers: Contract, accounts: Signer[], deployer: Signer, - deployerAddress: string, - user: Signer, - userAddress: string; + deployerAddress: string; async function setupContracts() { accounts = await ethers.getSigners(); deployer = accounts[0]; deployerAddress = await deployer.getAddress(); - user = accounts[1]; - userAddress = await user.getAddress(); - bondFactory = await setupBondFactory(); ({ collateralToken, rebaseOracle } = await setupCollateralToken("Bitcoin", "BTC")); @@ -425,144 +419,4 @@ describe("BondHelpers", function () { }); }); }); - - describe("#getTrancheCollateralBalances", function () { - let bond: Contract, bondLength: number; - beforeEach(async function () { - bondLength = 86400; - bond = await createBondWithFactory(bondFactory, collateralToken, [200, 300, 500], bondLength); - await depositIntoBond(bond, toFixedPtAmt("1000"), deployer); - const tranches = await getTranches(bond); - await tranches[0].transfer(userAddress, toFixedPtAmt("50")); - await tranches[1].transfer(userAddress, toFixedPtAmt("50")); - await tranches[2].transfer(userAddress, toFixedPtAmt("50")); - }); - - describe("when bond not mature", function () { - describe("when no change in supply", function () { - it("should calculate the balances", async function () { - const b = await bondHelpers.getTrancheCollateralBalances(bond.address, deployerAddress); - expect(b[1][0]).to.eq(toFixedPtAmt("150")); - expect(b[1][1]).to.eq(toFixedPtAmt("250")); - expect(b[1][2]).to.eq(toFixedPtAmt("450")); - - const c = await bondHelpers.getTrancheCollateralBalances(bond.address, userAddress); - expect(c[1][0]).to.eq(toFixedPtAmt("50")); - expect(c[1][1]).to.eq(toFixedPtAmt("50")); - expect(c[1][2]).to.eq(toFixedPtAmt("50")); - }); - }); - - describe("when supply increases above z threshold", function () { - it("should calculate the balances", async function () { - await rebase(collateralToken, rebaseOracle, 0.1); - const b = await bondHelpers.getTrancheCollateralBalances(bond.address, deployerAddress); - expect(b[1][0]).to.eq(toFixedPtAmt("150")); - expect(b[1][1]).to.eq(toFixedPtAmt("250")); - expect(b[1][2]).to.eq(toFixedPtAmt("540")); - - const c = await bondHelpers.getTrancheCollateralBalances(bond.address, userAddress); - expect(c[1][0]).to.eq(toFixedPtAmt("50")); - expect(c[1][1]).to.eq(toFixedPtAmt("50")); - expect(c[1][2]).to.eq(toFixedPtAmt("60")); - }); - }); - - describe("when supply decreases below z threshold", function () { - it("should calculate the balances", async function () { - await rebase(collateralToken, rebaseOracle, -0.1); - const b = await bondHelpers.getTrancheCollateralBalances(bond.address, deployerAddress); - expect(b[1][0]).to.eq(toFixedPtAmt("150")); - expect(b[1][1]).to.eq(toFixedPtAmt("250")); - expect(b[1][2]).to.eq(toFixedPtAmt("360")); - - const c = await bondHelpers.getTrancheCollateralBalances(bond.address, userAddress); - expect(c[1][0]).to.eq(toFixedPtAmt("50")); - expect(c[1][1]).to.eq(toFixedPtAmt("50")); - expect(c[1][2]).to.eq(toFixedPtAmt("40")); - }); - }); - - describe("when supply decreases below b threshold", function () { - it("should calculate the balances", async function () { - await rebase(collateralToken, rebaseOracle, -0.6); - const b = await bondHelpers.getTrancheCollateralBalances(bond.address, deployerAddress); - expect(b[1][0]).to.eq(toFixedPtAmt("150")); - expect(b[1][1]).to.eq(toFixedPtAmt("166.666666666666666666")); - expect(b[1][2]).to.eq(toFixedPtAmt("0")); - - const c = await bondHelpers.getTrancheCollateralBalances(bond.address, userAddress); - expect(c[1][0]).to.eq(toFixedPtAmt("50")); - expect(c[1][1]).to.eq(toFixedPtAmt("33.333333333333333333")); - expect(c[1][2]).to.eq(toFixedPtAmt("0")); - }); - }); - - describe("when supply decreases below a threshold", function () { - it("should calculate the balances", async function () { - await rebase(collateralToken, rebaseOracle, -0.85); - const b = await bondHelpers.getTrancheCollateralBalances(bond.address, deployerAddress); - expect(b[1][0]).to.eq(toFixedPtAmt("112.5")); - expect(b[1][1]).to.eq(toFixedPtAmt("0")); - expect(b[1][2]).to.eq(toFixedPtAmt("0")); - - const c = await bondHelpers.getTrancheCollateralBalances(bond.address, userAddress); - expect(c[1][0]).to.eq(toFixedPtAmt("37.5")); - expect(c[1][1]).to.eq(toFixedPtAmt("0")); - expect(c[1][2]).to.eq(toFixedPtAmt("0")); - }); - }); - }); - - describe("when bond is mature", function () { - beforeEach(async function () { - await TimeHelpers.increaseTime(bondLength); - await bond.mature(); // NOTE: Any rebase after maturity goes directly to the tranches - }); - - describe("when no change in supply", function () { - it("should calculate the balances", async function () { - const b = await bondHelpers.getTrancheCollateralBalances(bond.address, deployerAddress); - expect(b[1][0]).to.eq(toFixedPtAmt("150")); - expect(b[1][1]).to.eq(toFixedPtAmt("250")); - expect(b[1][2]).to.eq(toFixedPtAmt("450")); - - const c = await bondHelpers.getTrancheCollateralBalances(bond.address, userAddress); - expect(c[1][0]).to.eq(toFixedPtAmt("50")); - expect(c[1][1]).to.eq(toFixedPtAmt("50")); - expect(c[1][2]).to.eq(toFixedPtAmt("50")); - }); - }); - - describe("when supply increases", function () { - it("should calculate the balances", async function () { - await rebase(collateralToken, rebaseOracle, 0.1); - const b = await bondHelpers.getTrancheCollateralBalances(bond.address, deployerAddress); - expect(b[1][0]).to.eq(toFixedPtAmt("165")); - expect(b[1][1]).to.eq(toFixedPtAmt("275")); - expect(b[1][2]).to.eq(toFixedPtAmt("495")); - - const c = await bondHelpers.getTrancheCollateralBalances(bond.address, userAddress); - expect(c[1][0]).to.eq(toFixedPtAmt("55")); - expect(c[1][1]).to.eq(toFixedPtAmt("55")); - expect(c[1][2]).to.eq(toFixedPtAmt("55")); - }); - }); - - describe("when supply decreases", function () { - it("should calculate the balances", async function () { - await rebase(collateralToken, rebaseOracle, -0.1); - const b = await bondHelpers.getTrancheCollateralBalances(bond.address, deployerAddress); - expect(b[1][0]).to.eq(toFixedPtAmt("135")); - expect(b[1][1]).to.eq(toFixedPtAmt("225")); - expect(b[1][2]).to.eq(toFixedPtAmt("405")); - - const c = await bondHelpers.getTrancheCollateralBalances(bond.address, userAddress); - expect(c[1][0]).to.eq(toFixedPtAmt("45")); - expect(c[1][1]).to.eq(toFixedPtAmt("45")); - expect(c[1][2]).to.eq(toFixedPtAmt("45")); - }); - }); - }); - }); });