Skip to content

Commit 55cbb39

Browse files
authored
Fee curve update (#232)
* fee curve update * code review fixes
1 parent ff96931 commit 55cbb39

File tree

3 files changed

+71
-104
lines changed

3 files changed

+71
-104
lines changed

spot-contracts/contracts/FeePolicy.sol

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@ pragma solidity ^0.8.20;
33

44
import { IFeePolicy } from "./_interfaces/IFeePolicy.sol";
55
import { SubscriptionParams } from "./_interfaces/CommonTypes.sol";
6-
import { InvalidPerc, InvalidTargetSRBounds, InvalidDRBounds, InvalidSigmoidAsymptotes } from "./_interfaces/ProtocolErrors.sol";
6+
import { InvalidPerc, InvalidTargetSRBounds, InvalidDRBounds } from "./_interfaces/ProtocolErrors.sol";
77

88
import { MathUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol";
99
import { SafeCastUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol";
1010
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
11-
import { Sigmoid } from "./_utils/Sigmoid.sol";
12-
1311
/**
1412
* @title FeePolicy
1513
*
@@ -39,8 +37,7 @@ import { Sigmoid } from "./_utils/Sigmoid.sol";
3937
*
4038
*
4139
* The rollover fees are signed and can flow in either direction based on the `deviationRatio`.
42-
* The fee is a percentage is computed through a sigmoid function.
43-
* The slope and asymptotes are set by the owner.
40+
* The fee function parameters are set by the owner.
4441
*
4542
* CRITICAL: The rollover fee percentage is NOT annualized, the fee percentage is applied per rollover.
4643
* 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 {
5451
// Libraries
5552
using MathUpgradeable for uint256;
5653
using SafeCastUpgradeable for uint256;
54+
using SafeCastUpgradeable for int256;
5755

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

70-
/// @notice Sigmoid asymptote bound.
71-
/// @dev Set to 0.05 or 5%, i.e) the rollover fee can be at most 5% on either direction.
72-
uint256 public constant SIGMOID_BOUND = ONE / 20;
73-
7468
/// @notice Target subscription ratio lower bound, 0.75 or 75%.
7569
uint256 public constant TARGET_SR_LOWER_BOUND = (ONE * 75) / 100;
7670

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

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

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

115120
//-----------------------------------------------------------------------------
116121

@@ -151,9 +156,9 @@ contract FeePolicy is IFeePolicy, OwnableUpgradeable {
151156
vaultPerpToUnderlyingSwapFeePerc = ONE;
152157

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

158163
targetSubscriptionRatio = (ONE * 133) / 100; // 1.33
159164
deviationRatioBoundLower = (ONE * 75) / 100; // 0.75
@@ -206,17 +211,11 @@ contract FeePolicy is IFeePolicy, OwnableUpgradeable {
206211
perpBurnFeePerc = perpBurnFeePerc_;
207212
}
208213

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

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

275274
/// @inheritdoc IFeePolicy
276275
function computePerpRolloverFeePerc(uint256 dr) external view override returns (int256) {
277-
return
278-
Sigmoid.compute(
279-
dr.toInt256(),
280-
perpRolloverFee.lower,
281-
perpRolloverFee.upper,
282-
perpRolloverFee.growth,
283-
ONE.toInt256()
276+
if (dr <= ONE) {
277+
uint256 negPerpRate = MathUpgradeable.min(
278+
perpRolloverFee.m1.mulDiv(ONE - dr, ONE),
279+
perpRolloverFee.maxPerpDebasementPerc
284280
);
281+
return -1 * negPerpRate.toInt256();
282+
} else {
283+
uint256 perpRate = perpRolloverFee.m2.mulDiv(dr - ONE, ONE);
284+
return perpRate.toInt256();
285+
}
285286
}
286287

287288
/// @inheritdoc IFeePolicy

spot-contracts/contracts/_interfaces/ProtocolErrors.sol

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,3 @@ error InvalidTargetSRBounds();
7575

7676
/// @notice Expected deviation ratio bounds to be valid.
7777
error InvalidDRBounds();
78-
79-
/// @notice Expected sigmoid asymptotes to be within defined bounds.
80-
error InvalidSigmoidAsymptotes();

spot-contracts/test/FeePolicy.ts

Lines changed: 31 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -165,60 +165,25 @@ describe("FeePolicy", function () {
165165
it("should revert", async function () {
166166
await expect(
167167
feePolicy.connect(otherUser).updatePerpRolloverFees({
168-
lower: toPerc("-0.01"),
169-
upper: toPerc("0.01"),
170-
growth: toPerc("3"),
168+
maxPerpDebasementPerc: toPerc("0.01"),
169+
m1: toPerc("0.01"),
170+
m2: toPerc("0.01"),
171171
}),
172172
).to.be.revertedWith("Ownable: caller is not the owner");
173173
});
174174
});
175175

176-
describe("when parameters are invalid", function () {
177-
it("should revert", async function () {
178-
await expect(
179-
feePolicy.connect(deployer).updatePerpRolloverFees({
180-
lower: toPerc("-0.051"),
181-
upper: toPerc("0.01"),
182-
growth: toPerc("3"),
183-
}),
184-
).to.be.revertedWithCustomError(feePolicy, "InvalidSigmoidAsymptotes");
185-
});
186-
it("should revert", async function () {
187-
await expect(
188-
feePolicy.connect(deployer).updatePerpRolloverFees({
189-
lower: toPerc("-0.01"),
190-
upper: toPerc("0.051"),
191-
growth: toPerc("3"),
192-
}),
193-
).to.be.revertedWithCustomError(feePolicy, "InvalidSigmoidAsymptotes");
194-
});
195-
196-
it("should revert", async function () {
197-
await expect(
198-
feePolicy.connect(deployer).updatePerpRolloverFees({
199-
lower: toPerc("0.02"),
200-
upper: toPerc("0.01"),
201-
growth: toPerc("3"),
202-
}),
203-
).to.be.revertedWithCustomError(feePolicy, "InvalidSigmoidAsymptotes");
204-
});
205-
});
206-
207176
describe("when triggered by owner", function () {
208177
it("should update parameters", async function () {
209-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1"))).to.eq(0);
210-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("10"))).to.eq(toPerc("0.00769230"));
211-
expect(await feePolicy.computePerpRolloverFeePerc("0")).to.eq(toPerc("-0.00245837"));
212-
213178
await feePolicy.connect(deployer).updatePerpRolloverFees({
214-
lower: toPerc("-0.009"),
215-
upper: toPerc("0.009"),
216-
growth: toPerc("3"),
179+
maxPerpDebasementPerc: toPerc("0.01"),
180+
m1: toPerc("0.02"),
181+
m2: toPerc("0.03"),
217182
});
218-
219-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1"))).to.eq(0);
220-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("10"))).to.eq(toPerc("0.009"));
221-
expect(await feePolicy.computePerpRolloverFeePerc("0")).to.eq(toPerc("-0.007"));
183+
const p = await feePolicy.perpRolloverFee();
184+
expect(p.maxPerpDebasementPerc).to.eq(toPerc("0.01"));
185+
expect(p.m1).to.eq(toPerc("0.02"));
186+
expect(p.m2).to.eq(toPerc("0.03"));
222187
});
223188
});
224189
});
@@ -339,9 +304,9 @@ describe("FeePolicy", function () {
339304
await feePolicy.updatePerpMintFees(toPerc("0.025"));
340305
await feePolicy.updatePerpBurnFees(toPerc("0.035"));
341306
await feePolicy.updatePerpRolloverFees({
342-
lower: toPerc("-0.00253"),
343-
upper: toPerc("0.00769"),
344-
growth: toPerc("5"),
307+
maxPerpDebasementPerc: toPerc("0.1"),
308+
m1: toPerc("0.3"),
309+
m2: toPerc("0.6"),
345310
});
346311
await feePolicy.updateVaultUnderlyingToPerpSwapFeePerc(toPerc("0.1"));
347312
await feePolicy.updateVaultPerpToUnderlyingSwapFeePerc(toPerc("0.15"));
@@ -482,21 +447,25 @@ describe("FeePolicy", function () {
482447

483448
describe("rollover fee", function () {
484449
it("should compute fees as expected", async function () {
485-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.01"))).to.eq(toPerc("-0.00242144"));
486-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.25"))).to.eq(toPerc("-0.00228606"));
487-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.5"))).to.eq(toPerc("-0.00196829"));
488-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.75"))).to.eq(toPerc("-0.00128809"));
489-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.9"))).to.eq(toPerc("-0.00060117"));
490-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.99"))).to.eq(toPerc("-0.00004101"));
450+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0"))).to.eq(toPerc("-0.1"));
451+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.01"))).to.eq(toPerc("-0.1"));
452+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.25"))).to.eq(toPerc("-0.1"));
453+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.5"))).to.eq(toPerc("-0.1"));
454+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.66"))).to.eq(toPerc("-0.1"));
455+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.7"))).to.eq(toPerc("-0.09"));
456+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.8"))).to.eq(toPerc("-0.06"));
457+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.9"))).to.eq(toPerc("-0.03"));
458+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("0.99"))).to.eq(toPerc("-0.003"));
491459
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1"))).to.eq("0");
492-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.01"))).to.eq(toPerc("0.00004146"));
493-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.05"))).to.eq(toPerc("0.00034407"));
494-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.1"))).to.eq(toPerc("0.00071519"));
495-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.25"))).to.eq(toPerc("0.00195646"));
496-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.5"))).to.eq(toPerc("0.00411794"));
497-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.75"))).to.eq(toPerc("0.00580663"));
498-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("2"))).to.eq(toPerc("0.00680345"));
499-
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("5"))).to.eq(toPerc("0.00768997"));
460+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.01"))).to.eq(toPerc("0.006"));
461+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.05"))).to.eq(toPerc("0.03"));
462+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.1"))).to.eq(toPerc("0.06"));
463+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.25"))).to.eq(toPerc("0.15"));
464+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.5"))).to.eq(toPerc("0.3"));
465+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("1.75"))).to.eq(toPerc("0.45"));
466+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("2"))).to.eq(toPerc("0.6"));
467+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("5"))).to.eq(toPerc("2.4"));
468+
expect(await feePolicy.computePerpRolloverFeePerc(toPerc("10"))).to.eq(toPerc("5.4"));
500469
});
501470
});
502471
});

0 commit comments

Comments
 (0)