Buyback fees: Add staggered expiry (#478)

This commit is contained in:
Christian Kamm 2023-02-27 16:36:27 +01:00 committed by GitHub
parent 25d94b0e7b
commit e4d46c3c5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 278 additions and 37 deletions

View File

@ -33,19 +33,23 @@ pub fn account_buyback_fees_with_mngo(
let mut fees_bank = ctx.accounts.fees_bank.load_mut()?;
let bonus_factor = I80F48::from_num(group.buyback_fees_mngo_bonus_factor);
let now_ts = Clock::get()?.unix_timestamp.try_into().unwrap();
account
.fixed
.expire_buyback_fees(now_ts, group.buyback_fees_expiry_interval);
// quick return if nothing to buyback
let mut max_buyback = {
let dao_fees_token_position = dao_account.ensure_token_position(fees_bank.token_index)?.0;
let dao_fees_native = dao_fees_token_position.native(&fees_bank);
I80F48::from_num::<u64>(max_buyback.min(account.fixed.buyback_fees_accrued))
I80F48::from_num::<u64>(max_buyback.min(account.fixed.buyback_fees_accrued()))
.min(dao_fees_native)
};
if max_buyback <= I80F48::ZERO {
msg!(
"nothing to buyback, (buyback_fees_accrued {})",
account.fixed.buyback_fees_accrued
account.fixed.buyback_fees_accrued()
);
return Ok(());
}
@ -125,10 +129,9 @@ pub fn account_buyback_fees_with_mngo(
);
}
account.fixed.buyback_fees_accrued = account
account
.fixed
.buyback_fees_accrued
.saturating_sub(max_buyback.ceil().to_num::<u64>());
.reduce_buyback_fees_accrued(max_buyback.ceil().to_num::<u64>());
msg!(
"bought back {} native fees with {} native mngo",
max_buyback,

View File

@ -17,6 +17,7 @@ pub fn group_edit(
buyback_fees_bonus_factor_opt: Option<f32>,
buyback_fees_swap_mango_account_opt: Option<Pubkey>,
mngo_token_index_opt: Option<TokenIndex>,
buyback_fees_expiry_interval_opt: Option<u64>,
) -> Result<()> {
let mut group = ctx.accounts.group.load_mut()?;
@ -96,5 +97,14 @@ pub fn group_edit(
group.mngo_token_index = mngo_token_index;
}
if let Some(buyback_fees_expiry_interval) = buyback_fees_expiry_interval_opt {
msg!(
"Buyback fees expiry interval old {:?}, new {:?}",
group.buyback_fees_expiry_interval,
buyback_fees_expiry_interval
);
group.buyback_fees_expiry_interval = buyback_fees_expiry_interval;
}
Ok(())
}

View File

@ -81,8 +81,12 @@ pub fn perp_place_order(
};
let mut event_queue = ctx.accounts.event_queue.load_mut()?;
let group = ctx.accounts.group.load()?;
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
account
.fixed
.expire_buyback_fees(now_ts, group.buyback_fees_expiry_interval);
let pp = account.perp_position(perp_market_index)?;
let effective_pos = pp.effective_base_position_lots();

View File

@ -64,6 +64,7 @@ pub mod mango_v4 {
buyback_fees_bonus_factor_opt: Option<f32>,
buyback_fees_swap_mango_account_opt: Option<Pubkey>,
mngo_token_index_opt: Option<TokenIndex>,
buyback_fees_expiry_interval_opt: Option<u64>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::group_edit(
@ -78,6 +79,7 @@ pub mod mango_v4 {
buyback_fees_bonus_factor_opt,
buyback_fees_swap_mango_account_opt,
mngo_token_index_opt,
buyback_fees_expiry_interval_opt,
)?;
Ok(())
}

View File

@ -60,11 +60,19 @@ pub struct Group {
// - the user then claims quote for mngo at a bonus rate
pub buyback_fees_swap_mango_account: Pubkey,
pub reserved: [u8; 1832],
/// Number of seconds after which fees that could be used with the fees buyback feature expire.
///
/// The actual expiry is staggered such that the fees users accumulate are always
/// available for at least this interval - but may be available for up to twice this time.
///
/// When set to 0, there's no expiry of buyback fees.
pub buyback_fees_expiry_interval: u64,
pub reserved: [u8; 1824],
}
const_assert_eq!(
size_of::<Group>(),
32 + 4 + 32 * 2 + 4 + 32 * 2 + 4 + 4 + 20 * 32 + 32 + 8 + 16 + 32 + 1832
32 + 4 + 32 * 2 + 4 + 32 * 2 + 4 + 4 + 20 * 32 + 32 + 8 + 16 + 32 + 8 + 1824
);
const_assert_eq!(size_of::<Group>(), 2736);
const_assert_eq!(size_of::<Group>() % 8, 0);

View File

@ -87,9 +87,15 @@ pub struct MangoAccount {
pub frozen_until: u64,
pub buyback_fees_accrued: u64,
/// Fees usable with the "fees buyback" feature.
/// This tracks the ones that accrued in the current expiry interval.
pub buyback_fees_accrued_current: u64,
/// Fees buyback amount from the previous expiry interval.
pub buyback_fees_accrued_previous: u64,
/// End timestamp of the current expiry interval of the buyback fees amount.
pub buyback_fees_expiry_timestamp: u64,
pub reserved: [u8; 224],
pub reserved: [u8; 208],
// dynamic
pub header_version: u8,
@ -124,8 +130,10 @@ impl MangoAccount {
net_deposits: 0,
health_region_begin_init_health: 0,
frozen_until: 0,
buyback_fees_accrued: 0,
reserved: [0; 224],
buyback_fees_accrued_current: 0,
buyback_fees_accrued_previous: 0,
buyback_fees_expiry_timestamp: 0,
reserved: [0; 208],
header_version: DEFAULT_MANGO_ACCOUNT_VERSION,
padding3: Default::default(),
padding4: Default::default(),
@ -207,13 +215,12 @@ pub struct MangoAccountFixed {
pub perp_spot_transfers: i64,
pub health_region_begin_init_health: i64,
pub frozen_until: u64,
pub buyback_fees_accrued: u64,
pub reserved: [u8; 224],
pub buyback_fees_accrued_current: u64,
pub buyback_fees_accrued_previous: u64,
pub buyback_fees_expiry_timestamp: u64,
pub reserved: [u8; 208],
}
const_assert_eq!(
size_of::<MangoAccountFixed>(),
32 * 4 + 8 + 3 * 8 + 8 + 8 + 224
);
const_assert_eq!(size_of::<MangoAccountFixed>(), 32 * 4 + 8 + 7 * 8 + 208);
const_assert_eq!(size_of::<MangoAccountFixed>(), 400);
const_assert_eq!(size_of::<MangoAccountFixed>() % 8, 0);
@ -263,6 +270,43 @@ impl MangoAccountFixed {
false
}
}
/// Updates the buyback_fees_* fields for staggered expiry of available amounts.
pub fn expire_buyback_fees(&mut self, now_ts: u64, interval: u64) {
if interval == 0 || now_ts < self.buyback_fees_expiry_timestamp {
return;
} else if now_ts < self.buyback_fees_expiry_timestamp + interval {
self.buyback_fees_accrued_previous = self.buyback_fees_accrued_current;
} else {
self.buyback_fees_accrued_previous = 0;
}
self.buyback_fees_accrued_current = 0;
self.buyback_fees_expiry_timestamp = (now_ts / interval + 1) * interval;
}
/// The total buyback fees amount that the account can make use of.
pub fn buyback_fees_accrued(&self) -> u64 {
self.buyback_fees_accrued_current
.saturating_add(self.buyback_fees_accrued_previous)
}
/// Add new fees that are usable with the buyback fees feature.
pub fn accrue_buyback_fees(&mut self, amount: u64) {
self.buyback_fees_accrued_current =
self.buyback_fees_accrued_current.saturating_add(amount);
}
/// Reduce the available buyback fees amount because it was used up.
pub fn reduce_buyback_fees_accrued(&mut self, amount: u64) {
if amount > self.buyback_fees_accrued_previous {
self.buyback_fees_accrued_current = self
.buyback_fees_accrued_current
.saturating_sub(amount - self.buyback_fees_accrued_previous);
self.buyback_fees_accrued_previous = 0;
} else {
self.buyback_fees_accrued_previous -= amount;
}
}
}
impl Owner for MangoAccountFixed {
@ -903,7 +947,8 @@ impl<
let quote = I80F48::from(perp_market.quote_lot_size) * I80F48::from(quote_change);
let fees = quote.abs() * I80F48::from_num(fill.maker_fee);
if fees.is_positive() {
self.fixed_mut().buyback_fees_accrued += fees.floor().to_num::<u64>();
self.fixed_mut()
.accrue_buyback_fees(fees.floor().to_num::<u64>());
}
let pa = self.perp_position_mut(perp_market_index)?;
pa.settle_funding(perp_market);
@ -1232,6 +1277,9 @@ mod tests {
account.bump = 4;
account.net_deposits = 5;
account.health_region_begin_init_health = 7;
account.buyback_fees_accrued_current = 10;
account.buyback_fees_accrued_previous = 11;
account.buyback_fees_expiry_timestamp = 12;
account.tokens.resize(8, TokenPosition::default());
account.tokens[0].token_index = 8;
account.serum3.resize(8, Serum3Orders::default());
@ -1263,6 +1311,18 @@ mod tests {
account.health_region_begin_init_health,
account2.fixed.health_region_begin_init_health
);
assert_eq!(
account.buyback_fees_accrued_current,
account2.fixed.buyback_fees_accrued_current
);
assert_eq!(
account.buyback_fees_accrued_previous,
account2.fixed.buyback_fees_accrued_previous
);
assert_eq!(
account.buyback_fees_expiry_timestamp,
account2.fixed.buyback_fees_expiry_timestamp
);
assert_eq!(
account.tokens[0].token_index,
account2.token_position_by_raw_index(0).token_index
@ -1449,4 +1509,59 @@ mod tests {
assert!(account.perp_position(42).is_ok());
assert_eq!(account.active_perp_positions().count(), 2);
}
#[test]
fn test_buyback_fees() {
let mut account = make_test_account();
let fixed = account.fixed_mut();
assert_eq!(fixed.buyback_fees_accrued(), 0);
fixed.expire_buyback_fees(1000, 10);
assert_eq!(fixed.buyback_fees_accrued(), 0);
assert_eq!(fixed.buyback_fees_expiry_timestamp, 1010);
fixed.accrue_buyback_fees(10);
fixed.accrue_buyback_fees(5);
assert_eq!(fixed.buyback_fees_accrued(), 15);
fixed.reduce_buyback_fees_accrued(2);
assert_eq!(fixed.buyback_fees_accrued(), 13);
fixed.expire_buyback_fees(1009, 10);
assert_eq!(fixed.buyback_fees_expiry_timestamp, 1010);
assert_eq!(fixed.buyback_fees_accrued(), 13);
assert_eq!(fixed.buyback_fees_accrued_current, 13);
fixed.expire_buyback_fees(1010, 10);
assert_eq!(fixed.buyback_fees_expiry_timestamp, 1020);
assert_eq!(fixed.buyback_fees_accrued(), 13);
assert_eq!(fixed.buyback_fees_accrued_previous, 13);
assert_eq!(fixed.buyback_fees_accrued_current, 0);
fixed.accrue_buyback_fees(5);
assert_eq!(fixed.buyback_fees_accrued(), 18);
fixed.reduce_buyback_fees_accrued(15);
assert_eq!(fixed.buyback_fees_accrued(), 3);
assert_eq!(fixed.buyback_fees_accrued_previous, 0);
assert_eq!(fixed.buyback_fees_accrued_current, 3);
fixed.expire_buyback_fees(1021, 10);
fixed.accrue_buyback_fees(1);
assert_eq!(fixed.buyback_fees_expiry_timestamp, 1030);
assert_eq!(fixed.buyback_fees_accrued_previous, 3);
assert_eq!(fixed.buyback_fees_accrued_current, 1);
fixed.expire_buyback_fees(1051, 10);
assert_eq!(fixed.buyback_fees_expiry_timestamp, 1060);
assert_eq!(fixed.buyback_fees_accrued_previous, 0);
assert_eq!(fixed.buyback_fees_accrued_current, 0);
fixed.accrue_buyback_fees(7);
fixed.expire_buyback_fees(1060, 10);
fixed.accrue_buyback_fees(5);
assert_eq!(fixed.buyback_fees_expiry_timestamp, 1070);
assert_eq!(fixed.buyback_fees_accrued(), 12);
fixed.reduce_buyback_fees_accrued(100);
assert_eq!(fixed.buyback_fees_accrued(), 0);
}
}

View File

@ -363,7 +363,9 @@ fn apply_fees(
require_gte!(taker_fees, 0);
// The maker fees apply to the maker's account only when the fill event is consumed.
account.fixed.buyback_fees_accrued += taker_fees.floor().to_num::<u64>();
account
.fixed
.accrue_buyback_fees(taker_fees.floor().to_num::<u64>());
let perp_position = account.perp_position_mut(market.perp_market_index)?;
perp_position.record_trading_fee(taker_fees);
perp_position.taker_volume += taker_fees.to_num::<u64>();
@ -379,7 +381,9 @@ fn apply_fees(
/// Applies a fixed penalty fee to the account, and update the market's fees_accrued
fn apply_penalty(market: &mut PerpMarket, account: &mut MangoAccountRefMut) -> Result<()> {
let fee_penalty = I80F48::from_num(market.fee_penalty);
account.fixed.buyback_fees_accrued += fee_penalty.floor().to_num::<u64>();
account
.fixed
.accrue_buyback_fees(fee_penalty.floor().to_num::<u64>());
let perp_position = account.perp_position_mut(market.perp_market_index)?;
perp_position.record_trading_fee(fee_penalty);

View File

@ -154,7 +154,7 @@ async fn test_fees_buyback_with_mngo() -> Result<(), TransportError> {
.unwrap();
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
let before_fees_accrued = mango_account_1.buyback_fees_accrued;
let before_fees_accrued = mango_account_1.buyback_fees_accrued_current;
let fees_token_position_before =
mango_account_1.tokens[0].native(&solana.get_account::<Bank>(tokens[0].bank).await);
let mngo_token_position_before =
@ -174,7 +174,7 @@ async fn test_fees_buyback_with_mngo() -> Result<(), TransportError> {
let after_fees_accrued = solana
.get_account::<MangoAccount>(account_1)
.await
.buyback_fees_accrued;
.buyback_fees_accrued_current;
let fees_token_position_after =
mango_account_1.tokens[0].native(&solana.get_account::<Bank>(tokens[0].bank).await);
let mngo_token_position_after =

View File

@ -1515,6 +1515,7 @@ fn group_edit_instruction_default() -> mango_v4::instruction::GroupEdit {
buyback_fees_bonus_factor_opt: None,
buyback_fees_swap_mango_account_opt: None,
mngo_token_index_opt: None,
buyback_fees_expiry_interval_opt: None,
}
}

View File

@ -68,7 +68,6 @@ async function main() {
// get from jup api?
// how does the oracle look like?
// how was the price volatility yday?
}

View File

@ -32,7 +32,9 @@ export class MangoAccount {
perpSpotTransfers: BN;
healthRegionBeginInitHealth: BN;
frozenUntil: BN;
buybackFeesAccrued: BN;
buybackFeesAccruedCurrent: BN;
buybackFeesAccruedPrevious: BN;
buybackFeesExpiryTimestamp: BN;
headerVersion: number;
tokens: unknown;
serum3: unknown;
@ -53,7 +55,9 @@ export class MangoAccount {
obj.perpSpotTransfers,
obj.healthRegionBeginInitHealth,
obj.frozenUntil,
obj.buybackFeesAccrued,
obj.buybackFeesAccruedCurrent,
obj.buybackFeesAccruedPrevious,
obj.buybackFeesExpiryTimestamp,
obj.headerVersion,
obj.tokens as TokenPositionDto[],
obj.serum3 as Serum3PositionDto[],
@ -76,7 +80,9 @@ export class MangoAccount {
public perpSpotTransfers: BN,
public healthRegionBeginInitHealth: BN,
public frozenUntil: BN,
public buybackFeesAccrued: BN,
public buybackFeesAccruedCurrent: BN,
public buybackFeesAccruedPrevious: BN,
public buybackFeesExpiryTimestamp: BN,
public headerVersion: number,
tokens: TokenPositionDto[],
serum3: Serum3PositionDto[],

View File

@ -162,6 +162,7 @@ export class MangoClient {
feesMngoBonusRate?: number,
feesSwapMangoAccount?: PublicKey,
feesMngoTokenIndex?: TokenIndex,
feesExpiryInterval?: BN,
): Promise<TransactionSignature> {
const ix = await this.program.methods
.groupEdit(
@ -175,6 +176,7 @@ export class MangoClient {
feesMngoBonusRate ?? null,
feesSwapMangoAccount ?? null,
feesMngoTokenIndex ?? null,
feesExpiryInterval ?? null,
)
.accounts({
group: group.publicKey,

View File

@ -291,4 +291,3 @@ export function buildIxGate(p: IxGateParams): BN {
return ixGate;
}

View File

@ -168,6 +168,12 @@ export type MangoV4 = {
"type": {
"option": "u16"
}
},
{
"name": "buybackFeesExpiryIntervalOpt",
"type": {
"option": "u64"
}
}
]
},
@ -4088,12 +4094,24 @@ export type MangoV4 = {
"name": "buybackFeesSwapMangoAccount",
"type": "publicKey"
},
{
"name": "buybackFeesExpiryInterval",
"docs": [
"Number of seconds after which fees that could be used with the fees buyback feature expire.",
"",
"The actual expiry is staggered such that the fees users accumulate are always",
"available for at least this interval - but may be available for up to twice this time.",
"",
"When set to 0, there's no expiry of buyback fees."
],
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
1832
1824
]
}
}
@ -4187,7 +4205,25 @@ export type MangoV4 = {
"type": "u64"
},
{
"name": "buybackFeesAccrued",
"name": "buybackFeesAccruedCurrent",
"docs": [
"Fees usable with the \"fees buyback\" feature.",
"This tracks the ones that accrued in the current expiry interval."
],
"type": "u64"
},
{
"name": "buybackFeesAccruedPrevious",
"docs": [
"Fees buyback amount from the previous expiry interval."
],
"type": "u64"
},
{
"name": "buybackFeesExpiryTimestamp",
"docs": [
"End timestamp of the current expiry interval of the buyback fees amount."
],
"type": "u64"
},
{
@ -4195,7 +4231,7 @@ export type MangoV4 = {
"type": {
"array": [
"u8",
224
208
]
}
},
@ -5766,7 +5802,15 @@ export type MangoV4 = {
"type": "u64"
},
{
"name": "buybackFeesAccrued",
"name": "buybackFeesAccruedCurrent",
"type": "u64"
},
{
"name": "buybackFeesAccruedPrevious",
"type": "u64"
},
{
"name": "buybackFeesExpiryTimestamp",
"type": "u64"
},
{
@ -5774,7 +5818,7 @@ export type MangoV4 = {
"type": {
"array": [
"u8",
224
208
]
}
}
@ -8607,6 +8651,12 @@ export const IDL: MangoV4 = {
"type": {
"option": "u16"
}
},
{
"name": "buybackFeesExpiryIntervalOpt",
"type": {
"option": "u64"
}
}
]
},
@ -12527,12 +12577,24 @@ export const IDL: MangoV4 = {
"name": "buybackFeesSwapMangoAccount",
"type": "publicKey"
},
{
"name": "buybackFeesExpiryInterval",
"docs": [
"Number of seconds after which fees that could be used with the fees buyback feature expire.",
"",
"The actual expiry is staggered such that the fees users accumulate are always",
"available for at least this interval - but may be available for up to twice this time.",
"",
"When set to 0, there's no expiry of buyback fees."
],
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
1832
1824
]
}
}
@ -12626,7 +12688,25 @@ export const IDL: MangoV4 = {
"type": "u64"
},
{
"name": "buybackFeesAccrued",
"name": "buybackFeesAccruedCurrent",
"docs": [
"Fees usable with the \"fees buyback\" feature.",
"This tracks the ones that accrued in the current expiry interval."
],
"type": "u64"
},
{
"name": "buybackFeesAccruedPrevious",
"docs": [
"Fees buyback amount from the previous expiry interval."
],
"type": "u64"
},
{
"name": "buybackFeesExpiryTimestamp",
"docs": [
"End timestamp of the current expiry interval of the buyback fees amount."
],
"type": "u64"
},
{
@ -12634,7 +12714,7 @@ export const IDL: MangoV4 = {
"type": {
"array": [
"u8",
224
208
]
}
},
@ -14205,7 +14285,15 @@ export const IDL: MangoV4 = {
"type": "u64"
},
{
"name": "buybackFeesAccrued",
"name": "buybackFeesAccruedCurrent",
"type": "u64"
},
{
"name": "buybackFeesAccruedPrevious",
"type": "u64"
},
{
"name": "buybackFeesExpiryTimestamp",
"type": "u64"
},
{
@ -14213,7 +14301,7 @@ export const IDL: MangoV4 = {
"type": {
"array": [
"u8",
224
208
]
}
}