|
| 1 | +// SPDX-License-Identifier: BUSL-1.1 |
| 2 | +pragma solidity ^0.8.19; |
| 3 | + |
| 4 | +import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; |
| 5 | +import { IERC20MetadataUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; |
| 6 | +import { ITranche } from "./_interfaces/buttonwood/ITranche.sol"; |
| 7 | +import { IBondController } from "./_interfaces/buttonwood/IBondController.sol"; |
| 8 | +import { IPerpetualTranche } from "./_interfaces/IPerpetualTranche.sol"; |
| 9 | +import { TokenAmount } from "./_interfaces/ReturnData.sol"; |
| 10 | +import { IRolloverVault } from "./_interfaces/IRolloverVault.sol"; |
| 11 | +import { IFeePolicy } from "./_interfaces/IFeePolicy.sol"; |
| 12 | + |
| 13 | +import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; |
| 14 | +import { MathUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; |
| 15 | +import { BondTranches, BondTranchesHelpers } from "./_utils/BondTranchesHelpers.sol"; |
| 16 | +import { BondHelpers } from "./_utils/BondHelpers.sol"; |
| 17 | + |
| 18 | +/** |
| 19 | + * @title Rebalancer |
| 20 | + * |
| 21 | + * @notice A router contract to interact with both perp and rollover vault tokens in unison. |
| 22 | + * |
| 23 | + * Perp tokens act as low volatility derivative of the underlying collateral. |
| 24 | + * The vault notes act as a higher volatility derivative of the underlying collateral. |
| 25 | + * |
| 26 | + * When a user holds both tokens in the right ratio (aka magic ratio) |
| 27 | + * his exposure is neutral (same as holding the underlying collateral). |
| 28 | + * |
| 29 | + * When the user mints/burns both perps and vault notes together in the magic ratio, |
| 30 | + * the system charges no fees. This operation does not alter the system's `deviationRatio`. |
| 31 | + * |
| 32 | + * Additionally, when the user burn perps and mints vault notes the system charges no fees. |
| 33 | + * This operation strictly increases the `deviationRatio` and leave the system in a healthier state. |
| 34 | + * |
| 35 | + * |
| 36 | + */ |
| 37 | +contract Rebalancer { |
| 38 | + // math |
| 39 | + using MathUpgradeable for uint256; |
| 40 | + |
| 41 | + // data handling |
| 42 | + using BondHelpers for IBondController; |
| 43 | + using BondTranchesHelpers for BondTranches; |
| 44 | + |
| 45 | + // ERC20 operations |
| 46 | + using SafeERC20Upgradeable for IERC20Upgradeable; |
| 47 | + using SafeERC20Upgradeable for ITranche; |
| 48 | + using SafeERC20Upgradeable for IPerpetualTranche; |
| 49 | + using SafeERC20Upgradeable for IRolloverVault; |
| 50 | + |
| 51 | + // Constants & Immutables |
| 52 | + |
| 53 | + /// @dev Using the same granularity as the underlying buttonwood tranche contracts. |
| 54 | + /// https://github.com/buttonwood-protocol/tranche/blob/main/contracts/BondController.sol |
| 55 | + uint256 private constant TRANCHE_RATIO_GRANULARITY = 1000; |
| 56 | + |
| 57 | + /// @dev Immature redemption may result in some dust tranches when balances are not perfectly divisible by the tranche ratio. |
| 58 | + /// Based on current the implementation of `computeRedeemableTrancheAmounts`, |
| 59 | + /// the dust balances which remain after immature redemption will be at most {TRANCHE_RATIO_GRANULARITY} or 1000. |
| 60 | + /// We exclude dust tranche balances from recurrent immature redemption. |
| 61 | + uint256 public constant TRANCHE_DUST_AMT = 1000; |
| 62 | + |
| 63 | + uint8 public constant FEE_POLICY_DECIMALS = 8; |
| 64 | + uint256 public constant FEE_ONE_PERC = (10**FEE_POLICY_DECIMALS); |
| 65 | + |
| 66 | + uint256 public constant ONE = FEE_ONE_PERC; // 1.0 or 100% |
| 67 | + |
| 68 | + /// @notice TODO |
| 69 | + function deposit2( |
| 70 | + IPerpetualTranche perp, |
| 71 | + uint256 underlyingAmtIn |
| 72 | + ) public returns (uint256, uint256) { |
| 73 | + IRolloverVault vault = perp.vault(); |
| 74 | + IERC20Upgradeable underlying = perp.underlying(); |
| 75 | + |
| 76 | + // Transfer underlying tokens from user |
| 77 | + underlying.safeTransferFrom(msg.sender, address(this), underlyingAmtIn); |
| 78 | + |
| 79 | + // approve underlying to be spent |
| 80 | + _checkAndApproveMax(underlying, address(vault), underlyingAmtIn); |
| 81 | + |
| 82 | + // Figure out the accepted perp/vault collateral split |
| 83 | + (, uint256 vaultPerc) = _computeNeutralSplit(perp, perp.getDepositTrancheRatio()); |
| 84 | + |
| 85 | + // mint notes based on the vault ratio |
| 86 | + uint256 underlyingAmtIntoVault = underlyingAmtIn.mulDiv(vaultPerc, ONE, MathUpgradeable.Rounding.Up); |
| 87 | + vault.deposit(underlyingAmtIntoVault); |
| 88 | + |
| 89 | + // use the remaining collateral create perps |
| 90 | + vault.swapUnderlyingForPerps(underlyingAmtIn - underlyingAmtIntoVault); |
| 91 | + |
| 92 | + // Transfer perps and notes back to user |
| 93 | + return (_transferAll(perp, msg.sender), _transferAll(vault, msg.sender)); |
| 94 | + } |
| 95 | + |
| 96 | + /// @notice TODO |
| 97 | + function redeem2( |
| 98 | + IPerpetualTranche perp, |
| 99 | + uint256 perpAmtAvailable, |
| 100 | + uint256 noteAmtAvailable |
| 101 | + ) |
| 102 | + public |
| 103 | + returns ( |
| 104 | + uint256, |
| 105 | + uint256, |
| 106 | + TokenAmount[] memory |
| 107 | + ) |
| 108 | + { |
| 109 | + IRolloverVault vault = perp.vault(); |
| 110 | + |
| 111 | + // Compute the user's available balances |
| 112 | + perpAmtAvailable = MathUpgradeable.min(perpAmtAvailable, perp.balanceOf(msg.sender)); |
| 113 | + noteAmtAvailable = MathUpgradeable.min(noteAmtAvailable, vault.balanceOf(msg.sender)); |
| 114 | + |
| 115 | + // Compute redemption amounts |
| 116 | + (uint256 perpAmtBurnt, uint256 noteAmtBurnt) = _computeNeutralBurnAmts( |
| 117 | + perp, |
| 118 | + vault, |
| 119 | + perpAmtAvailable, |
| 120 | + noteAmtAvailable |
| 121 | + ); |
| 122 | + |
| 123 | + // Transfer perps and vault notes from the user |
| 124 | + perp.safeTransferFrom(msg.sender, address(this), perpAmtBurnt); |
| 125 | + vault.safeTransferFrom(msg.sender, address(this), noteAmtBurnt); |
| 126 | + |
| 127 | + // Redeem both perps and vault notes for tranches and underlying |
| 128 | + TokenAmount[] memory perpTokensRedeemed = perp.redeem(perpAmtBurnt); |
| 129 | + TokenAmount[] memory vaultTokensRedeemed = vault.redeem(noteAmtBurnt); |
| 130 | + |
| 131 | + // Meld perp tranches with vault tranches if possible if not transfer out |
| 132 | + TokenAmount[] memory tokensRedeemed = new TokenAmount[]( |
| 133 | + perpTokensRedeemed.length + vaultTokensRedeemed.length - 1 |
| 134 | + ); |
| 135 | + uint8 k = 0; |
| 136 | + for (uint8 i = 1; i < perpTokensRedeemed.length; i++) { |
| 137 | + IERC20Upgradeable tokenRedeemed = perpTokensRedeemed[i].token; |
| 138 | + _redeemImmatureTranche(ITranche(address(tokenRedeemed))); |
| 139 | + tokensRedeemed[k++] = TokenAmount({ |
| 140 | + token: tokenRedeemed, |
| 141 | + amount: _transferAll(tokenRedeemed, msg.sender) |
| 142 | + }); |
| 143 | + } |
| 144 | + for (uint8 i = 1; i < vaultTokensRedeemed.length; i++) { |
| 145 | + IERC20Upgradeable tokenRedeemed = vaultTokensRedeemed[i].token; |
| 146 | + tokensRedeemed[k++] = TokenAmount({ |
| 147 | + token: tokenRedeemed, |
| 148 | + amount: _transferAll(tokenRedeemed, msg.sender) |
| 149 | + }); |
| 150 | + } |
| 151 | + |
| 152 | + // transfer out remaining underlying |
| 153 | + IERC20Upgradeable underlying = perpTokensRedeemed[0].token; |
| 154 | + tokensRedeemed[k] = TokenAmount({ token: underlying, amount: _transferAll(underlying, msg.sender) }); |
| 155 | + |
| 156 | + return (perpAmtBurnt, noteAmtBurnt, tokensRedeemed); |
| 157 | + } |
| 158 | + |
| 159 | + /// @notice TODO |
| 160 | + function redeemPerpsAndMintVaultNotes( |
| 161 | + IPerpetualTranche perp, |
| 162 | + uint256 perpAmtIn |
| 163 | + ) external returns (uint256) { |
| 164 | + IRolloverVault vault = perp.vault(); |
| 165 | + IERC20Upgradeable underlying = perp.underlying(); |
| 166 | + |
| 167 | + // Transfer perps from user |
| 168 | + perp.safeTransferFrom(msg.sender, address(this), perpAmtIn); |
| 169 | + |
| 170 | + // Swap perps for underlying |
| 171 | + _checkAndApproveMax(perp, address(vault), perpAmtIn); |
| 172 | + uint256 underlyingAmt = vault.swapPerpsForUnderlying(perpAmtIn); |
| 173 | + |
| 174 | + // Deposit underlying into vault |
| 175 | + _checkAndApproveMax(underlying, address(vault), underlyingAmt); |
| 176 | + vault.deposit(underlyingAmt); |
| 177 | + |
| 178 | + // Transfer minted notes back to user |
| 179 | + return _transferAll(vault, msg.sender); |
| 180 | + } |
| 181 | + |
| 182 | + /// @dev Transfers the entire balance of a given token to the provided recipient. |
| 183 | + function _transferAll(IERC20Upgradeable token, address to) private returns (uint256) { |
| 184 | + uint256 bal = token.balanceOf(address(this)); |
| 185 | + if (bal > 0) { |
| 186 | + token.safeTransfer(to, bal); |
| 187 | + } |
| 188 | + return bal; |
| 189 | + } |
| 190 | + |
| 191 | + /// @dev Checks if the spender has sufficient allowance. If not, approves the maximum possible amount. |
| 192 | + function _checkAndApproveMax( |
| 193 | + IERC20Upgradeable token, |
| 194 | + address spender, |
| 195 | + uint256 amount |
| 196 | + ) private { |
| 197 | + uint256 allowance = token.allowance(address(this), spender); |
| 198 | + if (allowance < amount) { |
| 199 | + token.safeApprove(spender, 0); |
| 200 | + token.safeApprove(spender, type(uint256).max); |
| 201 | + } |
| 202 | + } |
| 203 | + |
| 204 | + /// @dev Redeems tranche tokens held by this contract, for underlying. |
| 205 | + function _redeemImmatureTranche(ITranche tranche) private { |
| 206 | + uint256 trancheBalance = tranche.balanceOf(address(this)); |
| 207 | + if (trancheBalance < TRANCHE_DUST_AMT) { |
| 208 | + return; |
| 209 | + } |
| 210 | + |
| 211 | + IBondController bond = IBondController(tranche.bond()); |
| 212 | + uint256[] memory trancheAmts = (bond.getTranches()).computeRedeemableTrancheAmounts(address(this)); |
| 213 | + if (trancheAmts[0] > 0) { |
| 214 | + bond.redeem(trancheAmts); |
| 215 | + } |
| 216 | + } |
| 217 | + |
| 218 | + struct SystemState { |
| 219 | + uint256 perpTVL; |
| 220 | + uint256 perpSupply; |
| 221 | + uint256 vaultTVL; |
| 222 | + uint256 noteSupply; |
| 223 | + } |
| 224 | + |
| 225 | + /// @dev Calculates the amount of perps and vault notes to burn based on the magic ratio. |
| 226 | + function _computeNeutralBurnAmts( |
| 227 | + IPerpetualTranche perp, |
| 228 | + IRolloverVault vault, |
| 229 | + uint256 perpAmtAvailable, |
| 230 | + uint256 noteAmtAvailable |
| 231 | + ) private returns (uint256, uint256) { |
| 232 | + // Compute current system state |
| 233 | + SystemState memory s = SystemState({ |
| 234 | + perpTVL: perp.getTVL(), |
| 235 | + perpSupply: perp.totalSupply(), |
| 236 | + vaultTVL: vault.getTVL(), |
| 237 | + noteSupply: vault.totalSupply() |
| 238 | + }); |
| 239 | + |
| 240 | + // Figure out the accepted perp/vault collateral split |
| 241 | + (uint256 perpPerc, uint256 vaultPerc) = _computeNeutralSplit(perp, perp.getDepositTrancheRatio()); |
| 242 | + |
| 243 | + // We calculate `noteAmtIn` based on `perpAmtAvailable` |
| 244 | + uint256 perpAmtToBurn = perpAmtAvailable; |
| 245 | + uint256 noteAmtToBurn = perpAmtToBurn |
| 246 | + .mulDiv(s.perpTVL, s.perpSupply, MathUpgradeable.Rounding.Up) |
| 247 | + .mulDiv(s.noteSupply, s.vaultTVL, MathUpgradeable.Rounding.Up) |
| 248 | + .mulDiv(vaultPerc, perpPerc, MathUpgradeable.Rounding.Up); |
| 249 | + |
| 250 | + // if more notes are required than available, we recalculate `perpAmtToBurn` based on `noteAmtAvailable` |
| 251 | + if (noteAmtToBurn > noteAmtAvailable) { |
| 252 | + noteAmtToBurn = noteAmtAvailable; |
| 253 | + perpAmtToBurn = noteAmtToBurn |
| 254 | + .mulDiv(s.vaultTVL, s.noteSupply, MathUpgradeable.Rounding.Up) |
| 255 | + .mulDiv(s.perpSupply, s.perpTVL, MathUpgradeable.Rounding.Up) |
| 256 | + .mulDiv(perpPerc, vaultPerc, MathUpgradeable.Rounding.Up); |
| 257 | + } |
| 258 | + |
| 259 | + return (perpAmtToBurn, noteAmtToBurn); |
| 260 | + } |
| 261 | + |
| 262 | + /// @dev Calculates the magic ratio split. |
| 263 | + function _computeNeutralSplit(IPerpetualTranche perp, uint256 perpRatio) public view returns (uint256, uint256) { |
| 264 | + uint256 vaultRatio = (TRANCHE_RATIO_GRANULARITY - perpRatio).mulDiv( |
| 265 | + perp.feePolicy().targetSubscriptionRatio(), |
| 266 | + FEE_ONE_PERC, |
| 267 | + MathUpgradeable.Rounding.Up |
| 268 | + ); |
| 269 | + uint256 vaultPerc = ONE.mulDiv(vaultRatio, (perpRatio + vaultRatio), MathUpgradeable.Rounding.Up); |
| 270 | + uint256 perpPerc = ONE - vaultPerc; |
| 271 | + return (perpPerc, vaultPerc); |
| 272 | + } |
| 273 | +} |
0 commit comments