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

import { IFeePolicy } from "./_interfaces/IFeePolicy.sol";
import { SubscriptionParams } from "./_interfaces/CommonTypes.sol";
import { InvalidPerc, InvalidTargetSRBounds, InvalidDRBounds, InvalidSigmoidAsymptotes } from "./_interfaces/ProtocolErrors.sol";
import { InvalidPerc, InvalidTargetSRBounds, InvalidDRBounds } from "./_interfaces/ProtocolErrors.sol";

import { MathUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol";
import { SafeCastUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol";
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { Sigmoid } from "./_utils/Sigmoid.sol";

/**
* @title FeePolicy
*
Expand Down Expand Up @@ -39,8 +37,7 @@ import { Sigmoid } from "./_utils/Sigmoid.sol";
*
*
* The rollover fees are signed and can flow in either direction based on the `deviationRatio`.
* The fee is a percentage is computed through a sigmoid function.
* The slope and asymptotes are set by the owner.
* The fee function parameters are set by the owner.
*
* CRITICAL: The rollover fee percentage is NOT annualized, the fee percentage is applied per rollover.
* The number of rollovers per year changes based on the duration of perp's minting bond.
Expand All @@ -54,6 +51,7 @@ contract FeePolicy is IFeePolicy, OwnableUpgradeable {
// Libraries
using MathUpgradeable for uint256;
using SafeCastUpgradeable for uint256;
using SafeCastUpgradeable for int256;

// Replicating value used here:
// https://github.com/buttonwood-protocol/tranche/blob/main/contracts/BondController.sol
Expand All @@ -67,10 +65,6 @@ contract FeePolicy is IFeePolicy, OwnableUpgradeable {
/// @notice Fixed point representation of 1.0 or 100%.
uint256 public constant ONE = (1 * 10 ** DECIMALS);

/// @notice Sigmoid asymptote bound.
/// @dev Set to 0.05 or 5%, i.e) the rollover fee can be at most 5% on either direction.
uint256 public constant SIGMOID_BOUND = ONE / 20;

/// @notice Target subscription ratio lower bound, 0.75 or 75%.
uint256 public constant TARGET_SR_LOWER_BOUND = (ONE * 75) / 100;

Expand Down Expand Up @@ -100,17 +94,28 @@ contract FeePolicy is IFeePolicy, OwnableUpgradeable {
/// @notice The percentage fee charged on burning perp tokens.
uint256 public perpBurnFeePerc;

struct RolloverFeeSigmoidParams {
/// @notice Lower asymptote
int256 lower;
/// @notice Upper asymptote
int256 upper;
/// @notice sigmoid slope
int256 growth;
/// @dev NOTE: We updated the type of the parameters from int256 to uint256, which is an upgrade safe operation.
struct RolloverFeeParams {
/// @notice The maximum debasement rate for perp,
/// i.e) the maximum rate perp pays the vault for rollovers.
/// @dev This is represented as fixed point number with {DECIMALS} places.
/// For example, setting this to (0.1 / 13), would mean that the yearly perp debasement rate is capped at ~10%.
uint256 maxPerpDebasementPerc;
/// @notice The slope of the linear fee curve when (dr <= 1).
/// @dev This is represented as fixed point number with {DECIMALS} places.
/// Setting it to (1.0 / 13), would mean that it would take 1 year for dr to increase to 1.0.
/// (assuming no other changes to the system)
uint256 m1;
/// @notice The slope of the linear fee curve when (dr > 1).
/// @dev This is represented as fixed point number with {DECIMALS} places.
/// Setting it to (1.0 / 13), would mean that it would take 1 year for dr to decrease to 1.0.
/// (assuming no other changes to the system)
uint256 m2;
}

/// @notice Parameters which control the asymptotes and the slope of the perp token's rollover fee.
RolloverFeeSigmoidParams public perpRolloverFee;
/// @notice Parameters which control the perp rollover fee,
/// i.e) the funding rate for holding perps.
RolloverFeeParams public perpRolloverFee;

//-----------------------------------------------------------------------------

Expand Down Expand Up @@ -151,9 +156,9 @@ contract FeePolicy is IFeePolicy, OwnableUpgradeable {
vaultPerpToUnderlyingSwapFeePerc = ONE;

// NOTE: With the current bond length of 28 days, rollover rate is annualized by dividing by: 365/28 ~= 13
perpRolloverFee.lower = -int256(ONE) / (30 * 13); // -0.033/13 = -0.00253 (3.3% annualized)
perpRolloverFee.upper = int256(ONE) / (10 * 13); // 0.1/13 = 0.00769 (10% annualized)
perpRolloverFee.growth = 5 * int256(ONE); // 5.0
perpRolloverFee.maxPerpDebasementPerc = ONE / (10 * 13); // 0.1/13 = 0.0077 (10% annualized)
perpRolloverFee.m1 = ONE / (3 * 13); // 0.025
perpRolloverFee.m2 = ONE / (3 * 13); // 0.025

targetSubscriptionRatio = (ONE * 133) / 100; // 1.33
deviationRatioBoundLower = (ONE * 75) / 100; // 0.75
Expand Down Expand Up @@ -206,17 +211,11 @@ contract FeePolicy is IFeePolicy, OwnableUpgradeable {
perpBurnFeePerc = perpBurnFeePerc_;
}

/// @notice Update the parameters determining the slope and asymptotes of the sigmoid fee curve.
/// @param p Lower, Upper and Growth sigmoid paramters are fixed point numbers with {DECIMALS} places.
function updatePerpRolloverFees(RolloverFeeSigmoidParams calldata p) external onlyOwner {
// If the bond duration is 28 days and 13 rollovers happen per year,
// perp can be inflated or enriched up to ~65% annually.
if (p.lower < -int256(SIGMOID_BOUND) || p.upper > int256(SIGMOID_BOUND) || p.lower > p.upper) {
revert InvalidSigmoidAsymptotes();
}
perpRolloverFee.lower = p.lower;
perpRolloverFee.upper = p.upper;
perpRolloverFee.growth = p.growth;
/// @notice Update the parameters determining the rollover fee curve.
/// @dev Back into the per-rollover percentage based on the bond duration, and thus number of rollovers per year.
/// @param p Paramters are fixed point numbers with {DECIMALS} places.
function updatePerpRolloverFees(RolloverFeeParams calldata p) external onlyOwner {
perpRolloverFee = p;
}

/// @notice Updates the vault mint fee parameters.
Expand Down Expand Up @@ -274,14 +273,16 @@ contract FeePolicy is IFeePolicy, OwnableUpgradeable {

/// @inheritdoc IFeePolicy
function computePerpRolloverFeePerc(uint256 dr) external view override returns (int256) {
return
Sigmoid.compute(
dr.toInt256(),
perpRolloverFee.lower,
perpRolloverFee.upper,
perpRolloverFee.growth,
ONE.toInt256()
if (dr <= ONE) {
uint256 negPerpRate = MathUpgradeable.min(
perpRolloverFee.m1.mulDiv(ONE - dr, ONE),
perpRolloverFee.maxPerpDebasementPerc
);
return -1 * negPerpRate.toInt256();
} else {
uint256 perpRate = perpRolloverFee.m2.mulDiv(dr - ONE, ONE);
return perpRate.toInt256();
}
}

/// @inheritdoc IFeePolicy
Expand Down
3 changes: 0 additions & 3 deletions spot-contracts/contracts/_interfaces/ProtocolErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,3 @@ error InvalidTargetSRBounds();

/// @notice Expected deviation ratio bounds to be valid.
error InvalidDRBounds();

/// @notice Expected sigmoid asymptotes to be within defined bounds.
error InvalidSigmoidAsymptotes();
93 changes: 31 additions & 62 deletions spot-contracts/test/FeePolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,60 +165,25 @@ describe("FeePolicy", function () {
it("should revert", async function () {
await expect(
feePolicy.connect(otherUser).updatePerpRolloverFees({
lower: toPerc("-0.01"),
upper: toPerc("0.01"),
growth: toPerc("3"),
maxPerpDebasementPerc: toPerc("0.01"),
m1: toPerc("0.01"),
m2: toPerc("0.01"),
}),
).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).updatePerpRolloverFees({
lower: toPerc("-0.051"),
upper: toPerc("0.01"),
growth: toPerc("3"),
}),
).to.be.revertedWithCustomError(feePolicy, "InvalidSigmoidAsymptotes");
});
it("should revert", async function () {
await expect(
feePolicy.connect(deployer).updatePerpRolloverFees({
lower: toPerc("-0.01"),
upper: toPerc("0.051"),
growth: toPerc("3"),
}),
).to.be.revertedWithCustomError(feePolicy, "InvalidSigmoidAsymptotes");
});

it("should revert", async function () {
await expect(
feePolicy.connect(deployer).updatePerpRolloverFees({
lower: toPerc("0.02"),
upper: toPerc("0.01"),
growth: toPerc("3"),
}),
).to.be.revertedWithCustomError(feePolicy, "InvalidSigmoidAsymptotes");
});
});

describe("when triggered by owner", function () {
it("should update parameters", async function () {
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1"))).to.eq(0);
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("10"))).to.eq(toPerc("0.00769230"));
expect(await feePolicy.computePerpRolloverFeePerc("0")).to.eq(toPerc("-0.00245837"));

await feePolicy.connect(deployer).updatePerpRolloverFees({
lower: toPerc("-0.009"),
upper: toPerc("0.009"),
growth: toPerc("3"),
maxPerpDebasementPerc: toPerc("0.01"),
m1: toPerc("0.02"),
m2: toPerc("0.03"),
});

expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1"))).to.eq(0);
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("10"))).to.eq(toPerc("0.009"));
expect(await feePolicy.computePerpRolloverFeePerc("0")).to.eq(toPerc("-0.007"));
const p = await feePolicy.perpRolloverFee();
expect(p.maxPerpDebasementPerc).to.eq(toPerc("0.01"));
expect(p.m1).to.eq(toPerc("0.02"));
expect(p.m2).to.eq(toPerc("0.03"));
});
});
});
Expand Down Expand Up @@ -339,9 +304,9 @@ describe("FeePolicy", function () {
await feePolicy.updatePerpMintFees(toPerc("0.025"));
await feePolicy.updatePerpBurnFees(toPerc("0.035"));
await feePolicy.updatePerpRolloverFees({
lower: toPerc("-0.00253"),
upper: toPerc("0.00769"),
growth: toPerc("5"),
maxPerpDebasementPerc: toPerc("0.1"),
m1: toPerc("0.3"),
m2: toPerc("0.6"),
});
await feePolicy.updateVaultUnderlyingToPerpSwapFeePerc(toPerc("0.1"));
await feePolicy.updateVaultPerpToUnderlyingSwapFeePerc(toPerc("0.15"));
Expand Down Expand Up @@ -482,21 +447,25 @@ describe("FeePolicy", function () {

describe("rollover fee", function () {
it("should compute fees as expected", async function () {
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.01"))).to.eq(toPerc("-0.00242144"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.25"))).to.eq(toPerc("-0.00228606"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.5"))).to.eq(toPerc("-0.00196829"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.75"))).to.eq(toPerc("-0.00128809"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.9"))).to.eq(toPerc("-0.00060117"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.99"))).to.eq(toPerc("-0.00004101"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0"))).to.eq(toPerc("-0.1"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.01"))).to.eq(toPerc("-0.1"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.25"))).to.eq(toPerc("-0.1"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.5"))).to.eq(toPerc("-0.1"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.66"))).to.eq(toPerc("-0.1"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.7"))).to.eq(toPerc("-0.09"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.8"))).to.eq(toPerc("-0.06"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.9"))).to.eq(toPerc("-0.03"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.99"))).to.eq(toPerc("-0.003"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1"))).to.eq("0");
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.01"))).to.eq(toPerc("0.00004146"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.05"))).to.eq(toPerc("0.00034407"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.1"))).to.eq(toPerc("0.00071519"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.25"))).to.eq(toPerc("0.00195646"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.5"))).to.eq(toPerc("0.00411794"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.75"))).to.eq(toPerc("0.00580663"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("2"))).to.eq(toPerc("0.00680345"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("5"))).to.eq(toPerc("0.00768997"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.01"))).to.eq(toPerc("0.006"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.05"))).to.eq(toPerc("0.03"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.1"))).to.eq(toPerc("0.06"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.25"))).to.eq(toPerc("0.15"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.5"))).to.eq(toPerc("0.3"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.75"))).to.eq(toPerc("0.45"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("2"))).to.eq(toPerc("0.6"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("5"))).to.eq(toPerc("2.4"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("10"))).to.eq(toPerc("5.4"));
});
});
});
Expand Down