Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

Commit 72b42a9

Browse files
authored
Implement optimal protocol fees (#32)
1 parent 62831eb commit 72b42a9

File tree

7 files changed

+188
-3
lines changed

7 files changed

+188
-3
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ test-ledger
99
**/yarn.lock
1010

1111
**/.next
12+
13+
.pnp.*
14+
.yarn/

app/src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ async function addCustody(
125125
closePosition: new BN(100),
126126
liquidation: new BN(100),
127127
protocolShare: new BN(10),
128+
feeMax: new BN(250),
129+
feeOptimal: new BN(10),
128130
};
129131
const borrowRate: BorrowRateParams = {
130132
baseRate: new BN(0),

programs/perpetuals/src/state/custody.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use {
1515
pub enum FeesMode {
1616
Fixed,
1717
Linear,
18+
Optimal,
1819
}
1920

2021
#[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)]
@@ -33,6 +34,9 @@ pub struct Fees {
3334
pub close_position: u64,
3435
pub liquidation: u64,
3536
pub protocol_share: u64,
37+
// configs for optimal fee mode
38+
pub fee_max: u64,
39+
pub fee_optimal: u64,
3640
}
3741

3842
#[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)]
@@ -218,6 +222,8 @@ impl Fees {
218222
&& self.close_position as u128 <= Perpetuals::BPS_POWER
219223
&& self.liquidation as u128 <= Perpetuals::BPS_POWER
220224
&& self.protocol_share as u128 <= Perpetuals::BPS_POWER
225+
&& self.fee_max as u128 <= Perpetuals::BPS_POWER
226+
&& self.fee_optimal as u128 <= Perpetuals::BPS_POWER
221227
}
222228
}
223229

programs/perpetuals/src/state/pool.rs

Lines changed: 168 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ impl Pool {
125125
locked_amount: u64,
126126
collateral_custody: &Custody,
127127
) -> Result<u64> {
128+
// The "optimal" algorithm is always used to compute the fee for entering a position.
128129
// entry_fee = custody.fees.open_position * utilization_fee * size
129130
// where utilization_fee = 1 + custody.fees.utilization_mult * (new_utilization - optimal_utilization) / (1 - optimal_utilization);
130131

@@ -957,9 +958,37 @@ impl Pool {
957958
require!(!custody.is_virtual, PerpetualsError::InstructionNotAllowed);
958959

959960
if custody.fees.mode == FeesMode::Fixed {
960-
return Self::get_fee_amount(base_fee, std::cmp::max(amount_add, amount_remove));
961+
Self::get_fee_amount(base_fee, std::cmp::max(amount_add, amount_remove))
962+
} else if custody.fees.mode == FeesMode::Linear {
963+
self.get_fee_linear(
964+
token_id,
965+
base_fee,
966+
amount_add,
967+
amount_remove,
968+
custody,
969+
token_price,
970+
)
971+
} else {
972+
self.get_fee_optimal(
973+
token_id,
974+
base_fee,
975+
amount_add,
976+
amount_remove,
977+
custody,
978+
token_price,
979+
)
961980
}
981+
}
962982

983+
fn get_fee_linear(
984+
&self,
985+
token_id: usize,
986+
base_fee: u64,
987+
amount_add: u64,
988+
amount_remove: u64,
989+
custody: &Custody,
990+
token_price: &OraclePrice,
991+
) -> Result<u64> {
963992
// if token ratio is improved:
964993
// fee = base_fee / ratio_fee
965994
// otherwise:
@@ -1035,6 +1064,63 @@ impl Pool {
10351064
std::cmp::max(amount_add, amount_remove),
10361065
)
10371066
}
1067+
1068+
fn get_fee_optimal(
1069+
&self,
1070+
token_id: usize,
1071+
base_fee: u64,
1072+
amount_add: u64,
1073+
amount_remove: u64,
1074+
custody: &Custody,
1075+
token_price: &OraclePrice,
1076+
) -> Result<u64> {
1077+
// Fee calculations must temporarily be in i64 because of negative slope.
1078+
let fee_max: i64 = custody.fees.fee_max as i64;
1079+
let fee_optimal: i64 = custody.fees.fee_optimal as i64;
1080+
1081+
let target_ratio: i64 = self.ratios[token_id].target as i64;
1082+
let min_ratio: i64 = self.ratios[token_id].min as i64;
1083+
let max_ratio: i64 = self.ratios[token_id].max as i64;
1084+
let post_lp_ratio: i64 =
1085+
self.get_new_ratio(amount_add, amount_remove, custody, token_price)? as i64;
1086+
1087+
let base_fee: i64 = base_fee as i64;
1088+
1089+
let slope_denominator: i64 = if post_lp_ratio > target_ratio {
1090+
math::checked_sub(max_ratio, target_ratio)?
1091+
} else {
1092+
math::checked_sub(target_ratio, min_ratio)?
1093+
};
1094+
1095+
let slope_numerator: i64 = if amount_add != 0 {
1096+
if post_lp_ratio > max_ratio {
1097+
return err!(PerpetualsError::TokenRatioOutOfRange);
1098+
}
1099+
fee_max - fee_optimal
1100+
} else {
1101+
if post_lp_ratio < min_ratio {
1102+
return err!(PerpetualsError::TokenRatioOutOfRange);
1103+
}
1104+
fee_optimal - fee_max
1105+
};
1106+
1107+
// Delay applying slope_denominator until the very end to avoid losing precision.
1108+
// b = fee_optimal - target_ratio * slope
1109+
// lp_fee = slope * post_lp_ratio + b
1110+
let b: i64 = math::checked_sub(
1111+
math::checked_mul(fee_optimal, slope_denominator)?,
1112+
math::checked_mul(target_ratio, slope_numerator)?,
1113+
)?;
1114+
let lp_fee: i64 = math::checked_div(
1115+
math::checked_add(math::checked_mul(slope_numerator, post_lp_ratio)?, b)?,
1116+
slope_denominator,
1117+
)?;
1118+
1119+
Self::get_fee_amount(
1120+
math::checked_as_u64(math::checked_add(lp_fee, base_fee)?)?,
1121+
std::cmp::max(amount_add, amount_remove),
1122+
)
1123+
}
10381124
}
10391125

10401126
#[cfg(test)]
@@ -1096,12 +1182,14 @@ mod test {
10961182
swap_out: 100,
10971183
stable_swap_in: 100,
10981184
stable_swap_out: 100,
1099-
add_liquidity: 200,
1100-
remove_liquidity: 300,
1185+
add_liquidity: 0,
1186+
remove_liquidity: 0,
11011187
open_position: 100,
11021188
close_position: 0,
11031189
liquidation: 50,
11041190
protocol_share: 25,
1191+
fee_max: 0,
1192+
fee_optimal: 0,
11051193
};
11061194

11071195
let custody = Custody {
@@ -1570,6 +1658,83 @@ mod test {
15701658
)
15711659
.unwrap()
15721660
);
1661+
1662+
// Test Optimal fees
1663+
custody.fees.mode = FeesMode::Optimal;
1664+
custody.fees.fee_max = 250; // 0.025
1665+
custody.fees.fee_optimal = 10; // 0.001
1666+
assert_eq!(
1667+
18_000_000, /* 0.1% fee because we approach target ratio. */
1668+
pool.get_fee(
1669+
0,
1670+
custody.fees.add_liquidity,
1671+
scale(18, custody.decimals),
1672+
0,
1673+
&custody,
1674+
&token_price,
1675+
)
1676+
.unwrap()
1677+
);
1678+
assert_eq!(
1679+
4_014_000_000, /* 2.23% fee because we exceed target ratio, nearing max ratio. */
1680+
pool.get_fee(
1681+
0,
1682+
custody.fees.add_liquidity,
1683+
scale(180, custody.decimals),
1684+
0,
1685+
&custody,
1686+
&token_price,
1687+
)
1688+
.unwrap()
1689+
);
1690+
assert_eq!(
1691+
13_100_000, /* 1.31% fee for removing a little liquidity. */
1692+
pool.get_fee(
1693+
0,
1694+
custody.fees.remove_liquidity,
1695+
0,
1696+
scale(1, custody.decimals),
1697+
&custody,
1698+
&token_price,
1699+
)
1700+
.unwrap()
1701+
);
1702+
assert_eq!(
1703+
231_000_000, /* 2.31% fee because almost all liquidity is removed. */
1704+
pool.get_fee(
1705+
0,
1706+
custody.fees.remove_liquidity,
1707+
0,
1708+
scale(10, custody.decimals),
1709+
&custody,
1710+
&token_price,
1711+
)
1712+
.unwrap()
1713+
);
1714+
// Removing too much liquidity takes the token ratio out of range.
1715+
assert_eq!(
1716+
err!(PerpetualsError::TokenRatioOutOfRange),
1717+
pool.get_fee(
1718+
0,
1719+
custody.fees.remove_liquidity,
1720+
0,
1721+
scale(15, custody.decimals),
1722+
&custody,
1723+
&token_price,
1724+
)
1725+
);
1726+
// Adding too much liquidity takes the token ratio out of range.
1727+
assert_eq!(
1728+
err!(PerpetualsError::TokenRatioOutOfRange),
1729+
pool.get_fee(
1730+
0,
1731+
custody.fees.add_liquidity,
1732+
scale(1800, custody.decimals),
1733+
0,
1734+
&custody,
1735+
&token_price,
1736+
)
1737+
);
15731738
}
15741739

15751740
#[test]

programs/perpetuals/tests/anchor/basic.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ describe("perpetuals", () => {
174174
closePosition: new BN(100),
175175
liquidation: new BN(100),
176176
protocolShare: new BN(10),
177+
feeMax: new BN(250),
178+
feeOptimal: new BN(10),
177179
};
178180
borrowRate = {
179181
baseRate: new BN(0),
@@ -266,6 +268,8 @@ describe("perpetuals", () => {
266268
closePosition: "100",
267269
liquidation: "100",
268270
protocolShare: "10",
271+
feeMax: "250",
272+
feeOptimal: "10",
269273
},
270274
borrowRate: {
271275
baseRate: "0",

programs/perpetuals/tests/native/utils/fixtures.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ pub fn fees_linear_regular() -> Fees {
4949
close_position: 100,
5050
liquidation: 50,
5151
protocol_share: 25,
52+
fee_max: 0,
53+
fee_optimal: 0,
5254
}
5355
}
5456

ui/src/lib/types.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,14 @@ export interface Fees {
9393
closePosition: BN;
9494
liquidation: BN;
9595
protocolShare: BN;
96+
fee_max: BN;
97+
fee_optimal: BN;
9698
}
9799

98100
export enum FeesMode {
99101
Fixed,
100102
Linear,
103+
Optimal
101104
}
102105

103106
export interface OracleParams {

0 commit comments

Comments
 (0)