Skip to content

Commit

Permalink
fee curve update
Browse files Browse the repository at this point in the history
  • Loading branch information
aalavandhan committed Nov 14, 2024
1 parent a8ae0ae commit aae3cd7
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 95 deletions.
69 changes: 30 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,19 @@ 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;
struct RolloverFeeParams {
/// @notice The maximum debasement rate for perp,
/// i.e) the maximum rate perp pays the vault for rollovers.
uint256 perpDebasementLim;
/// @notice The slope of the linear fee curve when (dr <= 1).
uint256 m1;
/// @notice The slope of the linear fee curve when (dr > 1).
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 +147,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.perpDebasementLim = 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 +202,10 @@ 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.
/// @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 +263,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 perpRate = MathUpgradeable.min(
perpRolloverFee.m1.mulDiv(ONE - dr, ONE),
perpRolloverFee.perpDebasementLim
);
return -1 * perpRate.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();
82 changes: 36 additions & 46 deletions spot-contracts/test/FeePolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ describe("FeePolicy", function () {
it("should revert", async function () {
await expect(
feePolicy.connect(otherUser).updatePerpRolloverFees({
lower: toPerc("-0.01"),
upper: toPerc("0.01"),
perpDebasementLim: toPerc("0.01"),
vaultRateMax: toPerc("0.01"),
growth: toPerc("3"),
}),
).to.be.revertedWith("Ownable: caller is not the owner");
Expand All @@ -177,48 +177,34 @@ describe("FeePolicy", function () {
it("should revert", async function () {
await expect(
feePolicy.connect(deployer).updatePerpRolloverFees({
lower: toPerc("-0.051"),
upper: toPerc("0.01"),
perpDebasementLim: toPerc("-0.05"),
vaultRateMax: 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");
).to.be.revertedWithCustomError(feePolicy, "InvalidPerc");
});

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

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"),
perpDebasementLim: toPerc("0.009"),
vaultRateMax: toPerc("0.01"),
growth: toPerc("7"),
});

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.perpDebasementLim).to.eq(toPerc("0.009"));
expect(p.vaultRateMax).to.eq(toPerc("0.01"));
expect(p.growth).to.eq(toPerc("7"));
});
});
});
Expand Down Expand Up @@ -339,9 +325,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"),
perpDebasementLim: 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 +468,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
53 changes: 46 additions & 7 deletions spot-vaults/tasks/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,20 @@ import { TaskArguments } from "hardhat/types";
import { sleep } from "./tools";

task("validate_upgrade")
.addPositionalParam("factory", "the name of the factory", undefined, types.string, false)
.addPositionalParam("address", "the address of the deployed proxy contract", undefined, types.string, false)
.addPositionalParam(
"factory",
"the name of the factory",
undefined,
types.string,
false,
)
.addPositionalParam(
"address",
"the address of the deployed proxy contract",
undefined,
types.string,
false,
)
.setAction(async function (args: TaskArguments, hre) {
const { factory, address } = args;
const Factory = await hre.ethers.getContractFactory(factory);
Expand All @@ -23,8 +35,20 @@ task("validate_upgrade")
});

task("prepare_upgrade")
.addPositionalParam("factory", "the name of the factory", undefined, types.string, false)
.addPositionalParam("address", "the address of the deployed proxy contract", undefined, types.string, false)
.addPositionalParam(
"factory",
"the name of the factory",
undefined,
types.string,
false,
)
.addPositionalParam(
"address",
"the address of the deployed proxy contract",
undefined,
types.string,
false,
)
.addParam("fromIdx", "the index of sender", 0, types.int)
.setAction(async function (args: TaskArguments, hre) {
const { factory, address } = args;
Expand Down Expand Up @@ -52,8 +76,20 @@ task("prepare_upgrade")
});

task("upgrade:testnet")
.addPositionalParam("factory", "the name of the factory", undefined, types.string, false)
.addPositionalParam("address", "the address of the deployed proxy contract", undefined, types.string, false)
.addPositionalParam(
"factory",
"the name of the factory",
undefined,
types.string,
false,
)
.addPositionalParam(
"address",
"the address of the deployed proxy contract",
undefined,
types.string,
false,
)
.addParam("fromIdx", "the index of sender", 0, types.int)
.setAction(async function (args: TaskArguments, hre) {
const signer = (await hre.ethers.getSigners())[args.fromIdx];
Expand All @@ -64,7 +100,10 @@ task("upgrade:testnet")
const Factory = await hre.ethers.getContractFactory(factory);

console.log("Proxy", address);
console.log("Current implementation", await getImplementationAddress(hre.ethers.provider, address));
console.log(
"Current implementation",
await getImplementationAddress(hre.ethers.provider, address),
);

const impl = await hre.upgrades.upgradeProxy(address, Factory, {
unsafeAllowRenames: true,
Expand Down

0 comments on commit aae3cd7

Please sign in to comment.