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
39 changes: 25 additions & 14 deletions spot-contracts/contracts/FeePolicy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity ^0.8.20;

import { IFeePolicy } from "./_interfaces/IFeePolicy.sol";
import { SubscriptionParams, Range, Line, RebalanceData } from "./_interfaces/CommonTypes.sol";
import { InvalidPerc, InvalidTargetSRBounds, InvalidDRBounds } from "./_interfaces/ProtocolErrors.sol";
import { InvalidPerc, InvalidTargetSRBounds, InvalidDRRange } from "./_interfaces/ProtocolErrors.sol";

import { LineHelpers } from "./_utils/LineHelpers.sol";
import { MathUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol";
Expand Down Expand Up @@ -71,11 +71,6 @@ contract FeePolicy is IFeePolicy, OwnableUpgradeable {
/// @notice Target subscription ratio higher bound, 2.0 or 200%.
uint256 public constant TARGET_SR_UPPER_BOUND = 2 * ONE;

// TODO: Make this configurable
/// @notice TVL percentage range within which we skip rebalance, 0.01%.
/// @dev Intended to limit rebalancing small amounts.
uint256 public constant EQUILIBRIUM_REBALANCE_PERC = ONE / 10000;

//-----------------------------------------------------------------------------
/// @notice The target subscription ratio i.e) the normalization factor.
/// @dev The ratio under which the system is considered "under-subscribed".
Expand All @@ -87,9 +82,12 @@ contract FeePolicy is IFeePolicy, OwnableUpgradeable {
Range public drHardBound;

/// @notice The deviation ratio bounds outside which flash swaps are still functional but,
/// the swap fees transition from a constant fee to a linear function. disabled.
/// the swap fees transition from a constant fee to a linear function.
Range public drSoftBound;

/// @notice The deviation ratio bounds inside which rebalancing is disabled.
Range public rebalEqDr;

//-----------------------------------------------------------------------------
// Fee parameters

Expand Down Expand Up @@ -146,6 +144,10 @@ contract FeePolicy is IFeePolicy, OwnableUpgradeable {
lower: (ONE * 90) / 100, // 0.9
upper: (5 * ONE) / 4 // 1.25
});
rebalEqDr = Range({
lower: ONE, // 1.0
upper: ONE // 1.0
});

// initializing fees
perpMintFeePerc = 0;
Expand Down Expand Up @@ -184,12 +186,21 @@ contract FeePolicy is IFeePolicy, OwnableUpgradeable {
drSoftBound_.lower <= drSoftBound_.upper &&
drSoftBound_.upper <= drHardBound_.upper);
if (!validBounds) {
revert InvalidDRBounds();
revert InvalidDRRange();
}
drHardBound = drHardBound_;
drSoftBound = drSoftBound_;
}

/// @notice Updates rebalance equilibrium DR range within which rebalancing is disabled.
/// @param rebalEqDr_ The lower and upper equilibrium deviation ratio range as fixed point number with {DECIMALS} places.
function updateRebalanceEquilibriumDR(Range memory rebalEqDr_) external onlyOwner {
if (rebalEqDr_.upper < rebalEqDr_.lower || rebalEqDr_.lower > ONE || rebalEqDr_.upper < ONE) {
revert InvalidDRRange();
}
rebalEqDr = rebalEqDr_;
}

/// @notice Updates the perp mint fee parameters.
/// @param perpMintFeePerc_ The new perp mint fee ceiling percentage
/// as a fixed point number with {DECIMALS} places.
Expand Down Expand Up @@ -376,6 +387,12 @@ contract FeePolicy is IFeePolicy, OwnableUpgradeable {

/// @inheritdoc IFeePolicy
function computeRebalanceData(SubscriptionParams memory s) external view override returns (RebalanceData memory r) {
// We skip rebalancing if dr is close to 1.0
uint256 dr = computeDeviationRatio(s);
if (dr >= rebalEqDr.lower && dr <= rebalEqDr.upper) {
return r;
}

uint256 juniorTR = (TRANCHE_RATIO_GRANULARITY - s.seniorTR);
uint256 drNormalizedSeniorTR = ONE.mulDiv(
(s.seniorTR * ONE),
Expand All @@ -400,12 +417,6 @@ contract FeePolicy is IFeePolicy, OwnableUpgradeable {
);
}

// We skip "dust" value transfer
uint256 minPerpAbsValueDelta = s.perpTVL.mulDiv(EQUILIBRIUM_REBALANCE_PERC, ONE);
if (r.underlyingAmtIntoPerp.abs() <= minPerpAbsValueDelta) {
r.underlyingAmtIntoPerp = 0;
}

// Compute protocol fee
r.protocolFeeUnderlyingAmt = r.underlyingAmtIntoPerp.abs().mulDiv(
(perpDebasement ? debasementProtocolSharePerc : enrichmentProtocolSharePerc),
Expand Down
37 changes: 22 additions & 15 deletions spot-contracts/contracts/RolloverVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ pragma solidity ^0.8.20;
import { IERC20Upgradeable, IPerpetualTranche, IBondController, ITranche, IFeePolicy } from "./_interfaces/IPerpetualTranche.sol";
import { IVault } from "./_interfaces/IVault.sol";
import { IRolloverVault } from "./_interfaces/IRolloverVault.sol";
import { IERC20Burnable } from "./_interfaces/IERC20Burnable.sol";
import { TokenAmount, RolloverData, SubscriptionParams, RebalanceData } from "./_interfaces/CommonTypes.sol";
import { UnauthorizedCall, UnauthorizedTransferOut, UnexpectedDecimals, UnexpectedAsset, OutOfBounds, UnacceptableSwap, InsufficientDeployment, DeployedCountOverLimit, InsufficientLiquidity, LastRebalanceTooRecent } from "./_interfaces/ProtocolErrors.sol";
import { UnauthorizedCall, UnauthorizedTransferOut, UnexpectedDecimals, UnexpectedAsset, OutOfBounds, UnacceptableSwap, InsufficientDeployment, DeployedCountOverLimit, InsufficientLiquidity, LastRebalanceTooRecent, UnacceptableParams } from "./_interfaces/ProtocolErrors.sol";

import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
Expand Down Expand Up @@ -69,6 +70,7 @@ contract RolloverVault is

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

// math
using MathUpgradeable for uint256;
Expand Down Expand Up @@ -107,9 +109,6 @@ contract RolloverVault is
/// during recovery (through recurrent immature redemption).
uint256 public constant TRANCHE_DUST_AMT = 10000000;

/// @notice Number of seconds in one day.
uint256 public constant DAY_SEC = (3600 * 24);

//--------------------------------------------------------------------------
// ASSETS
//
Expand Down Expand Up @@ -171,6 +170,9 @@ contract RolloverVault is
/// @notice Recorded timestamp of the last successful rebalance.
uint256 public lastRebalanceTimestampSec;

/// @notice Number of seconds after which the subsequent rebalance can be triggered.
uint256 public rebalanceFreqSec;

//--------------------------------------------------------------------------
// Modifiers

Expand Down Expand Up @@ -225,6 +227,7 @@ contract RolloverVault is
reservedUnderlyingBal = 0;
reservedSubscriptionPerc = 0;
lastRebalanceTimestampSec = block.timestamp;
rebalanceFreqSec = 86400; // 1 day

// sync underlying
_syncAsset(underlying);
Expand Down Expand Up @@ -259,6 +262,12 @@ contract RolloverVault is
keeper = keeper_;
}

/// @notice Updates the rebalance frequency.
/// @param rebalanceFreqSec_ The new rebalance frequency in seconds.
function updateRebalanceFrequency(uint256 rebalanceFreqSec_) external onlyOwner {
rebalanceFreqSec = rebalanceFreqSec_;
}

//--------------------------------------------------------------------------
// Keeper only methods

Expand All @@ -276,7 +285,7 @@ contract RolloverVault is

/// @notice Pauses the rebalance operation.
function pauseRebalance() external onlyKeeper {
lastRebalanceTimestampSec = type(uint256).max - DAY_SEC;
lastRebalanceTimestampSec = type(uint256).max - rebalanceFreqSec;
}

/// @notice Unpauses the rebalance operation.
Expand Down Expand Up @@ -306,10 +315,8 @@ contract RolloverVault is
// External & Public write methods

/// @inheritdoc IRolloverVault
/// @dev This method can execute at-most once every 24 hours. The rebalance frequency is hard-coded and can't be changed.
function rebalance() external override nonReentrant whenNotPaused {
// TODO: make rebalance frequency configurable.
if (block.timestamp <= lastRebalanceTimestampSec + DAY_SEC) {
if (block.timestamp <= lastRebalanceTimestampSec + rebalanceFreqSec) {
revert LastRebalanceTooRecent();
}
_rebalance(perp, underlying);
Expand Down Expand Up @@ -764,18 +771,18 @@ contract RolloverVault is
SubscriptionParams memory s = _querySubscriptionState(perp_);
RebalanceData memory r = feePolicy.computeRebalanceData(s);
if (r.underlyingAmtIntoPerp <= 0) {
// We transfer value from perp to the vault, by minting the vault perp tokens.
// We transfer value from perp to the vault, by minting the vault perp tokens (without any deposits).
// NOTE: We first mint the vault perp tokens, and then pay the protocol fee.
perp_.rebalanceToVault(r.underlyingAmtIntoPerp.abs() + r.protocolFeeUnderlyingAmt);

// We immediately deconstruct perp tokens minted to the vault.
_meldPerps(perp_);
} else {
// TODO: Look into sending As instead of AMPL on rebalance from vault to perp.
// We transfer value from the vault to perp, by transferring underlying collateral tokens to perp.
underlying_.safeTransfer(address(perp), r.underlyingAmtIntoPerp.toUint256());

perp_.updateState(); // Trigger perp book-keeping
// We transfer value from the vault to perp, by minting the perp tokens (after making required deposit)
// and then simply burning the newly minted perp tokens.
uint256 perpAmtToTransfer = r.underlyingAmtIntoPerp.toUint256().mulDiv(perp_.totalSupply(), s.perpTVL);
_trancheAndMintPerps(perp_, underlying_, s.perpTVL, s.seniorTR, perpAmtToTransfer);
IERC20Burnable(address(perp_)).burn(perpAmtToTransfer);
}

// Pay protocol fees
Expand Down Expand Up @@ -850,7 +857,7 @@ contract RolloverVault is
}
}

/// @dev Tranches the vault's underlying to mint perps..
/// @dev Tranches the vault's underlying to mint perps.
/// Performs some book-keeping to keep track of the vault's assets.
function _trancheAndMintPerps(
IPerpetualTranche perp_,
Expand Down
4 changes: 2 additions & 2 deletions spot-contracts/contracts/_interfaces/ProtocolErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,5 @@ error InvalidPerc();
/// @notice Expected target subscription ratio to be within defined bounds.
error InvalidTargetSRBounds();

/// @notice Expected deviation ratio bounds to be valid.
error InvalidDRBounds();
/// @notice Expected deviation ratio range to be valid.
error InvalidDRRange();
64 changes: 59 additions & 5 deletions spot-contracts/test/FeePolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,16 @@ describe("FeePolicy", function () {
it("should revert", async function () {
await expect(
feePolicy.connect(deployer).updateDRBounds([toPerc("0.9"), toPerc("2")], [toPerc("0.75"), toPerc("1.5")]),
).to.be.revertedWithCustomError(feePolicy, "InvalidDRBounds");
).to.be.revertedWithCustomError(feePolicy, "InvalidDRRange");
await expect(
feePolicy.connect(deployer).updateDRBounds([toPerc("0.5"), toPerc("2")], [toPerc("2"), toPerc("1.5")]),
).to.be.revertedWithCustomError(feePolicy, "InvalidDRBounds");
).to.be.revertedWithCustomError(feePolicy, "InvalidDRRange");
await expect(
feePolicy.connect(deployer).updateDRBounds([toPerc("0.5"), toPerc("2")], [toPerc("0.75"), toPerc("0.6")]),
).to.be.revertedWithCustomError(feePolicy, "InvalidDRBounds");
).to.be.revertedWithCustomError(feePolicy, "InvalidDRRange");
await expect(
feePolicy.connect(deployer).updateDRBounds([toPerc("0.5"), toPerc("2")], [toPerc("0.75"), toPerc("2.5")]),
).to.be.revertedWithCustomError(feePolicy, "InvalidDRBounds");
).to.be.revertedWithCustomError(feePolicy, "InvalidDRRange");
});
});

Expand All @@ -130,6 +130,39 @@ describe("FeePolicy", function () {
});
});

describe("#updateRebalanceEquilibriumDR", function () {
describe("when triggered by non-owner", function () {
it("should revert", async function () {
await expect(
feePolicy.connect(otherUser).updateRebalanceEquilibriumDR([toPerc("1"), toPerc("1")]),
).to.be.revertedWith("Ownable: caller is not the owner");
});
});

describe("when parameters are invalid", function () {
it("should revert", async function () {
await expect(
feePolicy.connect(deployer).updateRebalanceEquilibriumDR([toPerc("2"), toPerc("0.9")]),
).to.be.revertedWithCustomError(feePolicy, "InvalidDRRange");
await expect(
feePolicy.connect(deployer).updateRebalanceEquilibriumDR([toPerc("1.1"), toPerc("2")]),
).to.be.revertedWithCustomError(feePolicy, "InvalidDRRange");
await expect(
feePolicy.connect(deployer).updateRebalanceEquilibriumDR([toPerc("0.95"), toPerc("0.99")]),
).to.be.revertedWithCustomError(feePolicy, "InvalidDRRange");
});
});

describe("when triggered by owner", function () {
it("should update dr hard bounds", async function () {
await feePolicy.connect(deployer).updateRebalanceEquilibriumDR([toPerc("0.5"), toPerc("2")]);
const s = await feePolicy.rebalEqDr();
expect(s[0]).to.eq(toPerc("0.5"));
expect(s[1]).to.eq(toPerc("2"));
});
});
});

describe("#updatePerpMintFees", function () {
describe("when triggered by non-owner", function () {
it("should revert", async function () {
Expand Down Expand Up @@ -511,7 +544,28 @@ describe("FeePolicy", function () {
beforeEach(async function () {
await feePolicy.updateTargetSubscriptionRatio(toPerc("1.25"));
await feePolicy.updateProtocolSharePerc(toPerc("0.05"), toPerc("0.1"));
await feePolicy.updateRebalanceRates(toPerc("0.02"), toPerc("0.01"));
await feePolicy.updateMaxRebalancePerc(toPerc("0.02"), toPerc("0.01"));
await feePolicy.updateRebalanceEquilibriumDR([toPerc("0.9999"), toPerc("1.0001")])
});

describe("when deviation is within eq range", function () {
it("should compute rebalance data", async function () {
await feePolicy.updateRebalanceEquilibriumDR([toPerc("0.5"), toPerc("2")])
const r1 = await feePolicy.computeRebalanceData({
perpTVL: toAmt("120"),
vaultTVL: toAmt("500"),
seniorTR: 200,
});
expect(r1[0]).to.eq(0n);
expect(r1[1]).to.eq(0n);
const r2 = await feePolicy.computeRebalanceData({
perpTVL: toAmt("80"),
vaultTVL: toAmt("500"),
seniorTR: 200,
});
expect(r2[0]).to.eq(0n);
expect(r2[1]).to.eq(0n);
});
});

describe("when deviation = 1.0", function () {
Expand Down
25 changes: 25 additions & 0 deletions spot-contracts/test/rollover-vault/RolloverVault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,4 +441,29 @@ describe("RolloverVault", function () {
});
});
});

describe("#updateRebalanceFrequency", function () {
let tx: Transaction;
beforeEach(async function () {
await vault.connect(deployer).transferOwnership(await otherUser.getAddress());
});

describe("when triggered by non-owner", function () {
it("should revert", async function () {
await expect(vault.connect(deployer).updateRebalanceFrequency(3600)).to.be.revertedWith(
"Ownable: caller is not the owner",
);
});
});

describe("when triggered by owner", function () {
beforeEach(async function () {
tx = await vault.connect(otherUser).updateRebalanceFrequency(3600);
await tx;
});
it("should update the rebalance freq", async function () {
expect(await vault.rebalanceFreqSec()).to.eq(3600);
});
});
});
});
Loading