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 5, 2024
1 parent a8ae0ae commit 86ca566
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 95 deletions.
76 changes: 45 additions & 31 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 } 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";
Expand Down Expand Up @@ -54,6 +54,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 +68,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 +97,18 @@ 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
struct RolloverFeeParams {
/// @notice The maximum rate perp pays the vault for rollovers.
int256 perpRateMax;
/// @notice The maximum rate vault pays the perp for rollovers.
int256 vaultRateMax;
/// @notice Sigmoid slope
int256 growth;
}

/// @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 +149,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.perpRateMax = int256(ONE) / (10 * 13); // 0.1/13 = 0.0077 (10% annualized)
perpRolloverFee.vaultRateMax = int256(ONE) / (20 * 13); // 0.05/13 = 0.00385 (5% annualized)
perpRolloverFee.growth = 25 * int256(ONE); // 25.0

targetSubscriptionRatio = (ONE * 133) / 100; // 1.33
deviationRatioBoundLower = (ONE * 75) / 100; // 0.75
Expand Down Expand Up @@ -208,15 +206,13 @@ contract FeePolicy is IFeePolicy, OwnableUpgradeable {

/// @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 {
function updatePerpRolloverFees(RolloverFeeParams 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();
if (p.perpRateMax < 0 || p.vaultRateMax < 0) {
revert InvalidPerc();
}
perpRolloverFee.lower = p.lower;
perpRolloverFee.upper = p.upper;
perpRolloverFee.growth = p.growth;
perpRolloverFee = p;
}

/// @notice Updates the vault mint fee parameters.
Expand Down Expand Up @@ -273,15 +269,33 @@ 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()
);
function computePerpRolloverFeePerc(uint256 dr, uint256 seniorTR) external view override returns (int256) {
if (dr <= ONE) {
return
Sigmoid.compute(
dr.toInt256(),
-perpRolloverFee.perpRateMax,
perpRolloverFee.perpRateMax,
perpRolloverFee.growth,
ONE.toInt256()
);
} else {
uint256 vaultRate = Sigmoid
.compute(
dr.toInt256(),
-perpRolloverFee.vaultRateMax,
perpRolloverFee.vaultRateMax,
perpRolloverFee.growth,
ONE.toInt256()
)
.toUint256();
return
vaultRate
.mulDiv(dr, ONE)
.mulDiv(targetSubscriptionRatio, ONE)
.mulDiv(TRANCHE_RATIO_GRANULARITY - seniorTR, seniorTR)
.toInt256();
}
}

/// @inheritdoc IFeePolicy
Expand Down
10 changes: 4 additions & 6 deletions spot-contracts/contracts/PerpetualTranche.sol
Original file line number Diff line number Diff line change
Expand Up @@ -792,14 +792,12 @@ contract PerpetualTranche is
// The rollover fees are settled by, adjusting the exchange rate
// between `trancheInAmt` and `tokenOutAmt`.
//
uint256 seniorTR = _depositBond.getSeniorTrancheRatio();
int256 feePerc = feePolicy.computePerpRolloverFeePerc(
feePolicy.computeDeviationRatio(
SubscriptionParams({
perpTVL: _reserveValue(),
vaultTVL: vault.getTVL(),
seniorTR: _depositBond.getSeniorTrancheRatio()
})
)
SubscriptionParams({ perpTVL: _reserveValue(), vaultTVL: vault.getTVL(), seniorTR: seniorTR })
),
seniorTR
);
//-----------------------------------------------------------------------------

Expand Down
3 changes: 2 additions & 1 deletion spot-contracts/contracts/_interfaces/IFeePolicy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface IFeePolicy {
function computePerpBurnFeePerc() external view returns (uint256);

/// @param dr The current system deviation ratio.
/// @param seniorTR The tranche ratio of senior tranches accepted to mint perps.
/// @return The applied exchange rate adjustment between tranches into perp and
/// tokens out of perp during a rollover,
/// as a fixed-point number with {DECIMALS} decimal places.
Expand All @@ -22,7 +23,7 @@ interface IFeePolicy {
/// example) 100 tranchesIn for 99 tranchesOut; i.e) perp enrichment
/// - A fee of -1%, implies the exchange rate is adjusted in favor of tranchesOut.
/// example) 99 tranchesIn for 100 tranchesOut
function computePerpRolloverFeePerc(uint256 dr) external view returns (int256);
function computePerpRolloverFeePerc(uint256 dr, uint256 seniorTR) external view returns (int256);

/// @return The percentage of the mint vault note amount to be charged as fees,
/// as a fixed-point number with {DECIMALS} decimal places.
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();
87 changes: 40 additions & 47 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"),
perpRateMax: 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"),
perpRateMax: 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"),
perpRateMax: 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"),
perpRateMax: 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.perpRateMax).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"),
perpRateMax: toPerc("0.1"),
vaultRateMax: toPerc("0.05"),
growth: toPerc("25"),
});
await feePolicy.updateVaultUnderlyingToPerpSwapFeePerc(toPerc("0.1"));
await feePolicy.updateVaultPerpToUnderlyingSwapFeePerc(toPerc("0.15"));
Expand Down Expand Up @@ -482,21 +468,28 @@ 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("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("0.01"), 333)).to.eq(toPerc("-0.1"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.25"), 333)).to.eq(toPerc("-0.09999955"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.5"), 333)).to.eq(toPerc("-0.09996548"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.75"), 333)).to.eq(toPerc("-0.09740628"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.9"), 333)).to.eq(toPerc("-0.06995578"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.99"), 333)).to.eq(toPerc("-0.00864273"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1"), 333)).to.eq("0");
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.01"), 333)).to.eq(toPerc("0.01162717"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.05"), 333)).to.eq(toPerc("0.05706359"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.1"), 333)).to.eq(toPerc("0.10249891"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.25"), 333)).to.eq(toPerc("0.16218105"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.5"), 333)).to.eq(toPerc("0.19973050"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.75"), 333)).to.eq(toPerc("0.23309837"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("2"), 333)).to.eq(toPerc("0.26639933"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("5"), 333)).to.eq(toPerc("0.66599849"));

expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.9"), 500)).to.eq(toPerc("-0.06995578"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.99"), 500)).to.eq(toPerc("-0.00864273"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1"), 500)).to.eq("0");
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.01"), 500)).to.eq(toPerc("0.00580487"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.05"), 500)).to.eq(toPerc("0.02848902"));
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.1"), 500)).to.eq(toPerc("0.05117262"));
});
});
});
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 86ca566

Please sign in to comment.