diff --git a/spot-contracts/contracts/PerpetualTranche.sol b/spot-contracts/contracts/PerpetualTranche.sol index 61aa78dc..32a81542 100644 --- a/spot-contracts/contracts/PerpetualTranche.sol +++ b/spot-contracts/contracts/PerpetualTranche.sol @@ -366,6 +366,8 @@ contract PerpetualTranche is ) public initializer { __ERC20_init(name, symbol); __Ownable_init(); + __Pausable_init(); + __ReentrancyGuard_init(); _decimals = IERC20MetadataUpgradeable(address(collateral_)).decimals(); // NOTE: `_reserveAt(0)` always points to the underling collateral token diff --git a/spot-contracts/contracts/_utils/BondHelpers.sol b/spot-contracts/contracts/_utils/BondHelpers.sol index 0e8656f8..c39d5119 100644 --- a/spot-contracts/contracts/_utils/BondHelpers.sol +++ b/spot-contracts/contracts/_utils/BondHelpers.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.18; import { SafeCastUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; +import { MathUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import { IBondController } from "../_interfaces/buttonwood/IBondController.sol"; @@ -66,6 +67,9 @@ library TrancheHelpers { * */ library BondHelpers { + using SafeCastUpgradeable for uint256; + using MathUpgradeable for uint256; + // Replicating value used here: // https://github.com/buttonwood-protocol/tranche/blob/main/contracts/BondController.sol uint256 private constant TRANCHE_RATIO_GRANULARITY = 1000; @@ -92,7 +96,7 @@ library BondHelpers { /// @return The tranche data. function getTrancheData(IBondController b) internal view returns (TrancheData memory) { TrancheData memory td; - td.trancheCount = SafeCastUpgradeable.toUint8(b.trancheCount()); + td.trancheCount = b.trancheCount().toUint8(); td.tranches = new ITranche[](td.trancheCount); td.trancheRatios = new uint256[](td.trancheCount); // Max tranches per bond < 2**8 - 1 @@ -191,27 +195,39 @@ library BondHelpers { return (td, collateralBalances, trancheSupplies); } - /// @notice Given a bond, retrieves the collateral redeemable for - /// each tranche held by the given address. + /// @notice For a given bond and user address, computes the maximum number of each of the bond's tranches + /// the user is able to redeem before the bond's maturity. These tranche amounts necessarily match the bond's tranche ratios. /// @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; + TrancheData memory td = getTrancheData(b); + uint256[] memory redeemableAmts = new uint256[](td.trancheCount); + + // Calculate how many underlying assets could be redeemed from each tranche balance, + // assuming other tranches are not an issue, and record the smallest amount. + uint256 minUnderlyingOut = type(uint256).max; + uint8 i; + for (i = 0; i < td.trancheCount; i++) { + uint256 d = td.tranches[i].balanceOf(u).mulDiv(TRANCHE_RATIO_GRANULARITY, td.trancheRatios[i]); + if (d < minUnderlyingOut) { + minUnderlyingOut = d; + } - (td, collateralBalances, trancheSupplies) = getTrancheCollateralizations(b); + // if one of the balances is zero, we return + if (minUnderlyingOut == 0) { + return (td, redeemableAmts); + } + } - 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].mulDiv(minUnderlyingOut, 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 5e3220b4..e02af7b5 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/vaults/RolloverVault.sol b/spot-contracts/contracts/vaults/RolloverVault.sol new file mode 100644 index 00000000..01f9f7b1 --- /dev/null +++ b/spot-contracts/contracts/vaults/RolloverVault.sol @@ -0,0 +1,502 @@ +// 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"; + +// TODO: create a IVault interface +// TODO: add mint cap +// TODO: limit size of vault assets + +/// @notice Expected asset to be a valid 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); + +/// @notice Expected vault assets to be deployed. +error NoDeployment(); + +/// @notice Storage array access out of bounds. +error OutOfBounds(); + +/* + * @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) and mint "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 through two external poke functions which off-chain keepers can execute. + * 1) `deploy`: When executed, the vault deposits the underlying asset into perp's current deposit bond + * to get tranche tokens in return, it then swaps these fresh tranche tokens for + * older tranche tokens (ones mature or approaching maturity) from perp. + * system through a rollover operation and earns an income in perp tokens. + * 2) `recover`: When executed, the vault redeems tranches for the underlying 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 assets. + * + * + */ +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 Emits the vault asset's token balance that's recorded after a 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 exchange rate between the underlying asset and notes. + uint256 private constant INITIAL_RATE = 10**6; + + /// @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); + + //-------------------------------------------------------------------------- + // ASSETS + // + // The vault's assets are represented by a master list of ERC-20 tokens + // => { [underlying] U _deployed U _earned } + // + // In the case of this vault, the "earned" assets are the perp tokens themselves. + // The reward (or yield) for performing rollovers is paid out in perp tokens. + + /// @notice The ERC20 token that can be deposited into this vault. + IERC20Upgradeable public underlying; + + /// @dev The set of the intermediate ERC-20 tokens when the underlying asset has been put to use. + /// In the case of this vault, they represent the tranche tokens held before maturity. + EnumerableSetUpgradeable.AddressSet private _deployed; + + //------------------------------------------------------------------------- + // Data + + /// @notice The perpetual token on which rollovers are performed. + IPerpetualTranche public perp; + + //-------------------------------------------------------------------------- + // 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 perpetual tranche rolled over. + function init( + string memory name, + string memory symbol, + IPerpetualTranche perp_ + ) public initializer { + __ERC20_init(name, symbol); + __Ownable_init(); + __Pausable_init(); + __ReentrancyGuard_init(); + + underlying = perp_.collateral(); + _syncAsset(underlying); + + assert(underlying != perp_); + perp = perp_; + } + + //-------------------------------------------------------------------------- + // 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 Transfers a non-vault token out of the contract, which may have been added accidentally. + /// @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. Reverts if there are no funds to deploy. + /// @dev Simply batches the `recover` and `deploy` functions. + function recoverAndRedeploy() external { + recover(); + deploy(); + } + + /// @notice Deploys deposited funds. Reverts if there are no funds to deploy. + /// @dev Its safer to call `recover` before `deploy` so the full available balance can be deployed. + function deploy() public nonReentrant whenNotPaused { + TrancheData memory td = _tranche(perp.getDepositBond()); + if (_rollover(perp, td) == 0) { + revert NoDeployment(); + } + } + + /// @notice Recovers deployed funds. + function recover() public nonReentrant whenNotPaused { + _redeemTranches(); + } + + /// @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 totalSupply_ = totalSupply(); + uint256 notes = (totalSupply_ > 0) ? amount.mulDiv(totalSupply_, getTVL()) : (amount * INITIAL_RATE); + + underlying.safeTransferFrom(_msgSender(), address(this), amount); + _syncAsset(underlying); + + _mint(_msgSender(), notes); + return notes; + } + + struct TokenAmount { + /// @notice The asset token redeemed. + IERC20Upgradeable token; + /// @notice The amount redeemed. + uint256 amount; + } + + /// @notice Burns notes and sends a proportional share of vault's assets back to {msg.sender}. + /// @param notes The amount of notes to be burnt. + /// @return The list of asset tokens and amounts redeemed. + function redeem(uint256 notes) external nonReentrant whenNotPaused returns (TokenAmount[] memory) { + uint256 totalNotes = totalSupply(); + uint256 deployedCount_ = _deployed.length(); + uint256 assetCount = 2 + deployedCount_; + + // aggregating vault assets to be redeemed + TokenAmount[] memory redemptions = new TokenAmount[](assetCount); + redemptions[0].token = underlying; + for (uint256 i = 0; i < deployedCount_; i++) { + redemptions[i + 1].token = IERC20Upgradeable(_deployed.at(i)); + } + redemptions[deployedCount_ + 1].token = IERC20Upgradeable(perp); + + // burn notes + _burn(_msgSender(), notes); + + // 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].token.safeTransfer(_msgSender(), redemptions[i].amount); + _syncAsset(redemptions[i].token); + } + + return redemptions; + } + + /// @return The total value of assets currently held by the vault, denominated in the underlying asset. + function getTVL() public returns (uint256) { + uint256 totalAssets = 0; + + // The underlying balance + totalAssets += underlying.balanceOf(address(this)); + + // The deployed asset value denominated in the underlying + for (uint256 i = 0; i < _deployed.length(); i++) { + ITranche tranche = ITranche(_deployed.at(i)); + uint256 trancheBalance = tranche.balanceOf(address(this)); + if (trancheBalance > 0) { + (uint256 collateralBalance, uint256 debt) = tranche.getTrancheCollateralization(); + totalAssets += trancheBalance.mulDiv(collateralBalance, debt); + } + } + + // The earned asset (perp token) value denominated in the underlying + uint256 perpBalance = perp.balanceOf(address(this)); + if (perpBalance > 0) { + // The "earned" asset is assumed to be the perp token. + totalAssets += perpBalance.mulDiv(IPerpetualTranche(address(perp)).getAvgPrice(), PERP_UNIT_PRICE); + } + + return totalAssets; + } + + //-------------------------------------------------------------------------- + // External & Public read methods + + /// @param token The address of the asset ERC-20 token held by the vault. + /// @return The vault's asset token balance. + function vaultAssetBalance(IERC20Upgradeable token) external view returns (uint256) { + return isVaultAsset(token) ? token.balanceOf(address(this)) : 0; + } + + /// @return Total count of deployed asset tokens held by the vault. + function deployedCount() external view returns (uint256) { + return _deployed.length(); + } + + /// @param i The index of a token. + /// @return The token address from the deployed asset token list by index. + function deployedAt(uint256 i) external view returns (IERC20Upgradeable) { + return IERC20Upgradeable(_deployed.at(i)); + } + + /// @return Total count of earned income tokens held by the vault. + function earnedCount() external pure returns (uint256) { + return 1; + } + + /// @param i The index of a token. + /// @return The token address from the earned income token list by index. + function earnedAt(uint256 i) external view returns (IERC20Upgradeable) { + if (i > 0) { + revert OutOfBounds(); + } + return IERC20Upgradeable(perp); + } + + /// @param token The address of a token to check. + /// @return If the given token is held by the vault. + function isVaultAsset(IERC20Upgradeable token) public view returns (bool) { + return (token == underlying) || _deployed.contains(address(token)) || (address(perp) == address(token)); + } + + //-------------------------------------------------------------------------- + // Private write methods + + /// @dev Deposits underlying balance into the provided bond and receives tranche tokens in return. + function _tranche(IBondController bond) private returns (TrancheData memory) { + // Get bond's tranche data + TrancheData memory td = bond.getTrancheData(); + + // Get underlying balance + uint256 balance = underlying.balanceOf(address(this)); + + // Ensure initial deposit remains unspent + if (balance == 0) { + return td; + } + + // balance is tranched + underlying.approve(address(bond), balance); + bond.deposit(balance); + + // sync holdings + for (uint8 i = 0; i < td.trancheCount; i++) { + _syncDeployedAsset(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. + /// @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, + // there after the list is NOT ordered by maturity. + IERC20Upgradeable[] memory rolloverTokens = perp_.getReserveTokensUpForRollover(); + + // Batch rollover + uint256 totalPerpRolledOver = 0; + uint8 vaultTokenIdx = 0; + uint256 perpTokenIdx = 0; + + // We pair tranche tokens held by the vault with tranche tokens held by perp, + // And execute the rollover and continue to the next token with a usable balance. + while (vaultTokenIdx < td.trancheCount && perpTokenIdx < rolloverTokens.length) { + // trancheIntoPerp refers to the tranche going into perp from the vault + ITranche trancheIntoPerp = td.tranches[vaultTokenIdx]; + + // tokenOutOfPerp is the reserve token coming out of perp into the vault + IERC20Upgradeable tokenOutOfPerp = rolloverTokens[perpTokenIdx]; + + // compute available token out + uint256 tokenOutAmtAvailable = address(tokenOutOfPerp) != address(0) + ? tokenOutOfPerp.balanceOf(perp_.reserve()) + : 0; + + // trancheIntoPerp tokens are NOT exhausted but tokenOutOfPerp is exhausted + if (tokenOutAmtAvailable == 0) { + // Rollover is a no-op, so skipping to next tokenOutOfPerp + perpTokenIdx++; + continue; + } + + // Compute available tranche in + uint256 trancheInAmtAvailable = trancheIntoPerp.balanceOf(address(this)); + + // trancheInAmtAvailable is exhausted + if (trancheInAmtAvailable == 0) { + // Rollover is a no-op, so skipping to next trancheIntoPerp + vaultTokenIdx++; + continue; + } + + // Preview rollover + IPerpetualTranche.RolloverPreview memory rd = perp_.computeRolloverAmt( + trancheIntoPerp, + tokenOutOfPerp, + trancheInAmtAvailable, + tokenOutAmtAvailable + ); + + // trancheIntoPerp isn't accepted by perp, likely because it's yield=0, refer perp docs for more info + if (rd.perpRolloverAmt == 0) { + // Rollover is a no-op, so skipping to next trancheIntoPerp + vaultTokenIdx++; + continue; + } + + // Perform rollover + trancheIntoPerp.approve(address(perp_), trancheInAmtAvailable); + perp_.rollover(trancheIntoPerp, tokenOutOfPerp, trancheInAmtAvailable); + + // sync holdings + _syncDeployedAsset(trancheIntoPerp); + if (tokenOutOfPerp != underlying) { + _syncDeployedAsset(tokenOutOfPerp); + } + _syncAsset(perp_); + _syncAsset(underlying); + + // keep track of total amount rolled over + totalPerpRolledOver += rd.perpRolloverAmt; + } + + return totalPerpRolledOver; + } + + /// @notice Redeems the deployed tranche tokens for the underlying asset. + function _redeemTranches() private { + for (uint256 i = 0; i < _deployed.length(); 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); + } + } + + // 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 = _deployed.length() - 1; i >= 0; i--) { + _syncDeployedAsset(IERC20Upgradeable(_deployed.at(i))); + } + _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); + + 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; + } + + //-------------------------------------------------------------------------- + // 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")); - }); - }); - }); - }); });