From aae3cd7b2a277e7369d289df6509a24c83fc92ae Mon Sep 17 00:00:00 2001 From: aalavandhann <6264334+aalavandhan@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:26:20 -0500 Subject: [PATCH] fee curve update --- spot-contracts/contracts/FeePolicy.sol | 69 +++++++--------- .../contracts/_interfaces/ProtocolErrors.sol | 3 - spot-contracts/test/FeePolicy.ts | 82 ++++++++----------- spot-vaults/tasks/upgrade.ts | 53 ++++++++++-- 4 files changed, 112 insertions(+), 95 deletions(-) diff --git a/spot-contracts/contracts/FeePolicy.sol b/spot-contracts/contracts/FeePolicy.sol index 7376e105..1e11f502 100644 --- a/spot-contracts/contracts/FeePolicy.sol +++ b/spot-contracts/contracts/FeePolicy.sol @@ -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 * @@ -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. @@ -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 @@ -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; @@ -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; //----------------------------------------------------------------------------- @@ -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 @@ -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. @@ -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 diff --git a/spot-contracts/contracts/_interfaces/ProtocolErrors.sol b/spot-contracts/contracts/_interfaces/ProtocolErrors.sol index 9015f188..ce7f0238 100644 --- a/spot-contracts/contracts/_interfaces/ProtocolErrors.sol +++ b/spot-contracts/contracts/_interfaces/ProtocolErrors.sol @@ -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(); diff --git a/spot-contracts/test/FeePolicy.ts b/spot-contracts/test/FeePolicy.ts index b3eea137..c6c51206 100644 --- a/spot-contracts/test/FeePolicy.ts +++ b/spot-contracts/test/FeePolicy.ts @@ -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"); @@ -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")); }); }); }); @@ -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")); @@ -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")); }); }); }); diff --git a/spot-vaults/tasks/upgrade.ts b/spot-vaults/tasks/upgrade.ts index 613ba3c9..d47cdd94 100644 --- a/spot-vaults/tasks/upgrade.ts +++ b/spot-vaults/tasks/upgrade.ts @@ -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); @@ -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; @@ -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]; @@ -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,