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
25 changes: 25 additions & 0 deletions spot-contracts/contracts/FeePolicy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -427,4 +427,29 @@ contract FeePolicy is IFeePolicy, OwnableUpgradeable {
// Deduct protocol fee from value transfer
r.underlyingAmtIntoPerp -= (perpDebasement ? int256(-1) : int256(1)) * r.protocolFeeUnderlyingAmt.toInt256();
}

/// @inheritdoc IFeePolicy
function computeDREquilibriumSplit(
uint256 underlyingAmt,
uint256 seniorTR
) external view override returns (uint256 perpUnderlyingAmt, uint256 vaultUnderlyingAmt) {
uint256 juniorTR = (TRANCHE_RATIO_GRANULARITY - seniorTR);
perpUnderlyingAmt = underlyingAmt.mulDiv(seniorTR, seniorTR + juniorTR.mulDiv(targetSubscriptionRatio, ONE));
vaultUnderlyingAmt = underlyingAmt - perpUnderlyingAmt;
}

/// @inheritdoc IFeePolicy
function computeDRNeutralSplit(
uint256 perpAmtAvailable,
uint256 vaultNoteAmtAvailable,
uint256 perpSupply,
uint256 vaultNoteSupply
) external pure override returns (uint256 perpAmt, uint256 vaultNoteAmt) {
perpAmt = perpAmtAvailable;
vaultNoteAmt = vaultNoteSupply.mulDiv(perpAmt, perpSupply);
if (vaultNoteAmt > vaultNoteAmtAvailable) {
vaultNoteAmt = vaultNoteAmtAvailable;
perpAmt = perpAmtAvailable.mulDiv(vaultNoteAmt, vaultNoteSupply);
}
}
}
263 changes: 157 additions & 106 deletions spot-contracts/contracts/RolloverVault.sol

Large diffs are not rendered by default.

14 changes: 4 additions & 10 deletions spot-contracts/contracts/RouterV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/
import { SafeCastUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol";
import { BondTranches, BondTranchesHelpers } from "./_utils/BondTranchesHelpers.sol";
import { BondHelpers } from "./_utils/BondHelpers.sol";
import { ERC20Helpers } from "./_utils/ERC20Helpers.sol";

/**
* @title RouterV2
Expand All @@ -30,6 +31,7 @@ contract RouterV2 {
using SafeERC20Upgradeable for IERC20Upgradeable;
using SafeERC20Upgradeable for ITranche;
using SafeERC20Upgradeable for IPerpetualTranche;
using ERC20Helpers for IERC20Upgradeable;

/// @notice Calculates the amount of tranche tokens minted after depositing into the deposit bond.
/// @dev Used by off-chain services to preview a tranche operation.
Expand Down Expand Up @@ -64,14 +66,14 @@ contract RouterV2 {
collateralToken.safeTransferFrom(msg.sender, address(this), collateralAmount);

// approves collateral to be tranched
_checkAndApproveMax(collateralToken, address(bond), collateralAmount);
collateralToken.checkAndApproveMax(address(bond), collateralAmount);

// tranches collateral
bond.deposit(collateralAmount);

// uses senior tranches to mint perps
uint256 trancheAmt = bt.tranches[0].balanceOf(address(this));
_checkAndApproveMax(bt.tranches[0], address(perp), trancheAmt);
IERC20Upgradeable(bt.tranches[0]).checkAndApproveMax(address(perp), trancheAmt);
perp.deposit(bt.tranches[0], trancheAmt);

// transfers remaining junior tranches back
Expand All @@ -86,12 +88,4 @@ contract RouterV2 {
// transfers perp tokens back
perp.safeTransfer(msg.sender, perp.balanceOf(address(this)));
}

/// @dev Checks if the spender has sufficient allowance. If not, approves the maximum possible amount.
function _checkAndApproveMax(IERC20Upgradeable token, address spender, uint256 amount) private {
uint256 allowance = token.allowance(address(this), spender);
if (allowance < amount) {
token.safeApprove(spender, type(uint256).max);
}
}
}
26 changes: 26 additions & 0 deletions spot-contracts/contracts/_interfaces/IFeePolicy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,30 @@ interface IFeePolicy {
/// @return r Rebalance data, magnitude and direction of value flow between perp and the rollover vault
/// expressed in the underlying token amount and the protocol's cut.
function computeRebalanceData(SubscriptionParams memory s) external view returns (RebalanceData memory r);

/// @notice Computes the dr-equilibrium split of underlying tokens into perp and the vault.
/// @dev The this basically the `targetSr` adjusted bond ratio.
/// @param underlyingAmt The amount of underlying tokens to split.
/// @param seniorTR The tranche ratio of seniors accepted by perp.
/// @return underlyingAmtIntoPerp The amount of underlying tokens to go into perp.
/// @return underlyingAmtIntoVault The amount of underlying tokens to go into the vault.
function computeDREquilibriumSplit(
uint256 underlyingAmt,
uint256 seniorTR
) external view returns (uint256 underlyingAmtIntoPerp, uint256 underlyingAmtIntoVault);

/// @notice Computes the dr-neutral split of perp tokens and vault notes.
/// @dev The "system ratio" or the ratio of assets in the system as it stands.
/// @param perpAmtAvailable The available amount of perp tokens.
/// @param vaultNoteAmtAvailable The available amount of vault notes.
/// @param perpSupply The total supply of perp tokens.
/// @param vaultNoteSupply The total supply of vault notes.
/// @return perpAmt The amount of perp tokens, with the same share of total supply as the vault notes.
/// @return vaultNoteAmt The amount of vault notes, with the same share of total supply as the perp tokens.
function computeDRNeutralSplit(
uint256 perpAmtAvailable,
uint256 vaultNoteAmtAvailable,
uint256 perpSupply,
uint256 vaultNoteSupply
) external view returns (uint256, uint256);
}
19 changes: 18 additions & 1 deletion spot-contracts/contracts/_interfaces/IRolloverVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,30 @@
pragma solidity ^0.8.0;

import { IVault } from "./IVault.sol";
import { SubscriptionParams } from "./CommonTypes.sol";
import { SubscriptionParams, TokenAmount } from "./CommonTypes.sol";

interface IRolloverVault is IVault {
/// @notice Gradually transfers value between the perp and vault, to bring the system back into balance.
/// @dev The rebalance function can be executed at-most once a day.
function rebalance() external;

/// @notice Batch operation to mint both perp and rollover vault tokens.
/// @param underlyingAmtIn The amount of underlying tokens to be tranched.
/// @return perpAmt The amount of perp tokens minted.
/// @return vaultNoteAmt The amount of vault notes minted.
function mint2(uint256 underlyingAmtIn) external returns (uint256 perpAmt, uint256 vaultNoteAmt);

/// @notice Batch operation to redeem both perp and rollover vault tokens for the underlying collateral and tranches.
/// @param perpAmtAvailable The amount of perp tokens available to redeem.
/// @param vaultNoteAmtAvailable The amount of vault notes available to redeem.
/// @return perpAmtBurnt The amount of perp tokens redeemed.
/// @return vaultNoteAmtBurnt The amount of vault notes redeemed.
/// @return returnedTokens The list of asset tokens and amounts returned.
function redeem2(
uint256 perpAmtAvailable,
uint256 vaultNoteAmtAvailable
) external returns (uint256 perpAmtBurnt, uint256 vaultNoteAmtBurnt, TokenAmount[] memory returnedTokens);

/// @notice Allows users to swap their underlying tokens for perps held by the vault.
/// @param underlyingAmtIn The amount of underlying tokens swapped in.
/// @return The amount of perp tokens swapped out.
Expand Down
11 changes: 0 additions & 11 deletions spot-contracts/contracts/_interfaces/IVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,4 @@ interface IVault is IERC20Upgradeable {
/// @param token The address of a token to check.
/// @return If the given token is held by the vault.
function isVaultAsset(IERC20Upgradeable token) external view returns (bool);

/// @notice Computes the amount of notes minted when given amount of underlying asset tokens
/// are deposited into the system.
/// @param amount The amount tokens to be deposited into the vault.
/// @return The amount of notes to be minted.
function computeMintAmt(uint256 amount) external returns (uint256);

/// @notice Computes the amount of asset tokens redeemed when burning given number of vault notes.
/// @param notes The amount of notes to be burnt.
/// @return The list of asset tokens and amounts redeemed.
function computeRedemptionAmts(uint256 notes) external returns (TokenAmount[] memory);
}
15 changes: 15 additions & 0 deletions spot-contracts/contracts/_utils/BondHelpers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { UnacceptableDeposit, UnacceptableTrancheLength } from "../_interfaces/P
import { SafeCastUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol";
import { MathUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol";
import { BondTranches } from "./BondTranchesHelpers.sol";
import { ERC20Helpers } from "./ERC20Helpers.sol";

/**
* @title BondHelpers
Expand All @@ -20,6 +21,7 @@ import { BondTranches } from "./BondTranchesHelpers.sol";
library BondHelpers {
using SafeCastUpgradeable for uint256;
using MathUpgradeable for uint256;
using ERC20Helpers for IERC20Upgradeable;

// Replicating value used here:
// https://github.com/buttonwood-protocol/tranche/blob/main/contracts/BondController.sol
Expand Down Expand Up @@ -97,4 +99,17 @@ library BondHelpers {

return tranchesOut;
}

/// @notice Helper function which approves underlying tokens and mints tranche tokens by depositing into the provided bond contract.
/// @return The array of tranche tokens minted.
function approveAndDeposit(
IBondController b,
IERC20Upgradeable underlying_,
uint256 underlyingAmt
) internal returns (ITranche[2] memory) {
BondTranches memory bt = getTranches(b);
underlying_.checkAndApproveMax(address(b), underlyingAmt);
b.deposit(underlyingAmt);
return bt.tranches;
}
}
23 changes: 23 additions & 0 deletions spot-contracts/contracts/_utils/ERC20Helpers.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.20;

import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";

/**
* @title ERC20Helpers
*
* @notice Library with helper functions for ERC20 Tokens.
*
*/
library ERC20Helpers {
using SafeERC20Upgradeable for IERC20Upgradeable;

/// @notice Checks if the spender has sufficient allowance. If not, approves the maximum possible amount.
function checkAndApproveMax(IERC20Upgradeable token, address spender, uint256 amount) internal {
uint256 allowance = token.allowance(address(this), spender);
if (allowance < amount) {
token.safeApprove(spender, type(uint256).max);
}
}
}
72 changes: 72 additions & 0 deletions spot-contracts/contracts/_utils/TrancheManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.20;

import { IERC20Upgradeable, IBondController, ITranche } from "../_interfaces/IPerpetualTranche.sol";

import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import { MathUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol";
import { BondTranches, BondTranchesHelpers } from "./BondTranchesHelpers.sol";
import { TrancheHelpers } from "./TrancheHelpers.sol";
import { BondHelpers } from "./BondHelpers.sol";
import { ERC20Helpers } from "./ERC20Helpers.sol";

/**
* @title TrancheManager
*
* @notice Linked external library with helper functions for tranche management.
*
* @dev Proxies which use external libraries are by default NOT upgrade safe.
* We guarantee that this linked external library will never trigger selfdestruct,
* and this one is.
*
*/
library TrancheManager {
// data handling
using BondHelpers for IBondController;
using TrancheHelpers for ITranche;
using BondTranchesHelpers for BondTranches;

// ERC20 operations
using SafeERC20Upgradeable for IERC20Upgradeable;
using ERC20Helpers for IERC20Upgradeable;

// math
using MathUpgradeable for uint256;

//--------------------------------------------------------------------------
// Helper methods

/// @notice Low level method that redeems the given mature tranche for the underlying asset.
/// It interacts with the button-wood bond contract.
function execMatureTrancheRedemption(IBondController bond, ITranche tranche, uint256 amount) external {
if (!bond.isMature()) {
bond.mature();
}
bond.redeemMature(address(tranche), amount);
}

/// @notice Low level method that redeems the given tranche for the underlying asset, before maturity.
/// If the contract holds sibling tranches with proportional balances, those will also get redeemed.
/// It interacts with the button-wood bond contract.
function execImmatureTrancheRedemption(IBondController bond, BondTranches memory bt) external {
uint256[] memory trancheAmts = bt.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);
}
}

/// @notice Computes the value of the given amount of tranche tokens, based on it's current CDR.
/// Value is denominated in the underlying collateral.
function computeTrancheValue(
address tranche,
address collateralToken,
uint256 trancheAmt
) external view returns (uint256) {
(uint256 trancheClaim, uint256 trancheSupply) = ITranche(tranche).getTrancheCollateralization(
IERC20Upgradeable(collateralToken)
);
return trancheClaim.mulDiv(trancheAmt, trancheSupply, MathUpgradeable.Rounding.Up);
}
}
2 changes: 1 addition & 1 deletion spot-contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default {
settings: {
optimizer: {
enabled: true,
runs: 250,
runs: 200,
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion spot-contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"ethers": "^6.6.0",
"ethers-v5": "npm:ethers@^5.7.0",
"ganache-cli": "latest",
"hardhat": "^2.22.19",
"hardhat": "^2.23.0",
"hardhat-gas-reporter": "latest",
"lodash": "^4.17.21",
"prettier": "^2.7.1",
Expand Down
68 changes: 68 additions & 0 deletions spot-contracts/test/FeePolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,4 +652,72 @@ describe("FeePolicy", function () {
});
});
});

describe("#computeDREquilibriumSplit", async function () {
it("should compute correct perp and vault underlying amounts", async function () {
const r = await feePolicy.computeDREquilibriumSplit(toFixedPtAmt("100"), 333);
expect(r[0]).to.eq(toFixedPtAmt("24.981245311327831957"));
expect(r[1]).to.eq(toFixedPtAmt("75.018754688672168043"));
});

it("should compute correct perp and vault underlying amounts", async function () {
await feePolicy.updateTargetSubscriptionRatio(toPercFixedPtAmt("1.0"));
const r = await feePolicy.computeDREquilibriumSplit(toFixedPtAmt("100"), 500);
expect(r[0]).to.eq(toFixedPtAmt("50"));
expect(r[1]).to.eq(toFixedPtAmt("50"));
});

it("should compute correct perp and vault underlying amounts", async function () {
await feePolicy.updateTargetSubscriptionRatio(toPercFixedPtAmt("2.0"));
const r = await feePolicy.computeDREquilibriumSplit(toFixedPtAmt("100"), 500);
expect(r[0]).to.eq(toFixedPtAmt("33.333333333333333333"));
expect(r[1]).to.eq(toFixedPtAmt("66.666666666666666667"));
});
});

describe("#computeDRNeutralSplit", async function () {
it("should compute proportional split", async function () {
const r = await feePolicy.computeDRNeutralSplit(
toFixedPtAmt("1000"),
toFixedPtAmt("100"),
toFixedPtAmt("1000"),
toFixedPtAmt("1000"),
);
expect(r[0]).to.equal(toFixedPtAmt("100"));
expect(r[1]).to.equal(toFixedPtAmt("100"));
});

it("should compute proportional split", async function () {
const r = await feePolicy.computeDRNeutralSplit(
toFixedPtAmt("1000"),
toFixedPtAmt("100"),
toFixedPtAmt("1000"),
toFixedPtAmt("100"),
);
expect(r[0]).to.equal(toFixedPtAmt("1000"));
expect(r[1]).to.equal(toFixedPtAmt("100"));
});

it("should compute proportional split", async function () {
const r = await feePolicy.computeDRNeutralSplit(
toFixedPtAmt("1000"),
toFixedPtAmt("100"),
toFixedPtAmt("100000"),
toFixedPtAmt("100"),
);
expect(r[0]).to.equal(toFixedPtAmt("1000"));
expect(r[1]).to.equal(toFixedPtAmt("1"));
});

it("should compute proportional split", async function () {
const r = await feePolicy.computeDRNeutralSplit(
toFixedPtAmt("1000"),
toFixedPtAmt("100"),
toFixedPtAmt("1000"),
toFixedPtAmt("10000"),
);
expect(r[0]).to.equal(toFixedPtAmt("10"));
expect(r[1]).to.equal(toFixedPtAmt("100"));
});
});
});
9 changes: 8 additions & 1 deletion spot-contracts/test/RouterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,14 @@ describe("RouterV2", function () {
await perp.updateTolerableTrancheMaturity(600, 3600);
await advancePerpQueue(perp, 3600);

vault = new DMock(await ethers.getContractFactory("RolloverVault"));
const TrancheManager = await ethers.getContractFactory("TrancheManager");
const trancheManager = await TrancheManager.deploy();
const RolloverVault = await ethers.getContractFactory("RolloverVault", {
libraries: {
TrancheManager: trancheManager.target,
},
});
vault = new DMock(RolloverVault);
await vault.deploy();
await vault.mockMethod("getTVL()", [0]);
await perp.updateVault(vault.target);
Expand Down
Loading