Skip to content

Commit f333dd2

Browse files
committed
rebalancer
1 parent 2efdca3 commit f333dd2

File tree

1 file changed

+273
-0
lines changed

1 file changed

+273
-0
lines changed
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
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

Comments
 (0)