FlashLoan: fee fixes (#693)

- Rename the new "swap fee" to "deposit fee" and let it apply to all
  deposits, not just for Swap-type flash loans.
- But don't apply it to withdrawals (effectively giving rebates!)

Result of audit feedback

(cherry picked from commit 1d6ce550f3)
This commit is contained in:
Christian Kamm 2023-08-29 10:03:07 +02:00
parent f63163d737
commit 2d392c8fff
16 changed files with 73 additions and 64 deletions

View File

@ -35,10 +35,13 @@ Update this for each program release and mainnet deployment.
Liquidators may be interested in performing actions in the same transaction
as a flash loan swap.
- Flash loan swaps: The DAO can now charge a fee (#660)
- Flash loan: The DAO can now charge a deposit fee (#660, #693)
The DAO can now configure a fee on flash loan based swaps. Previously flash
loans that did not use more than the user's deposits were free.
The DAO can now configure a fee on deposits that happen in flash loans. This
could be used to apply a fee to flash loan swaps.
Previously flash loans that did not increase the user's token balance and did
not borrow tokens were free.
- Stop loss: Respect net borrow limits and change low-health completion (#677)
- Stop loss: Store helpful UI fields (#654, #667)

View File

@ -594,7 +594,7 @@
"type": "f32"
},
{
"name": "flashLoanSwapFeeRate",
"name": "flashLoanDepositFeeRate",
"type": "f32"
}
]
@ -926,7 +926,7 @@
}
},
{
"name": "flashLoanSwapFeeRateOpt",
"name": "flashLoanDepositFeeRateOpt",
"type": {
"option": "f32"
}
@ -6670,7 +6670,7 @@
"type": "f32"
},
{
"name": "flashLoanSwapFeeRate",
"name": "flashLoanDepositFeeRate",
"type": "f32"
},
{
@ -8256,7 +8256,7 @@
"type": "i128"
},
{
"name": "swapFee",
"name": "depositFee",
"type": "i128"
}
]

View File

@ -235,8 +235,8 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
// Verify that each mentioned vault has a bank in the health accounts
let mut vaults_with_banks = vec![false; vaults.len()];
// Biggest flash_loan_swap_fee_rate over all involved banks
let mut max_swap_fee_rate = 0.0f32;
// Biggest flash_loan_deposit_fee_rate over all involved banks
let mut max_deposit_fee_rate = 0.0f32;
// Loop over the banks, finding matching vaults
// TODO: must be moved into health.rs, because it assumes something about the health accounts structure
@ -295,7 +295,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
change += repay;
}
max_swap_fee_rate = max_swap_fee_rate.max(bank.flash_loan_swap_fee_rate);
max_deposit_fee_rate = max_deposit_fee_rate.max(bank.flash_loan_deposit_fee_rate);
changes.push(TokenVaultChange {
token_index: bank.token_index,
@ -364,14 +364,14 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
let loan_origination_fee = loan * bank.loan_origination_fee_rate;
bank.collected_fees_native += loan_origination_fee;
let swap_fee = if flash_loan_type == FlashLoanType::Swap {
change.amount * I80F48::from_num(max_swap_fee_rate)
let deposit_fee = if change.amount > 0 {
change.amount * I80F48::from_num(max_deposit_fee_rate)
} else {
I80F48::ZERO
};
bank.collected_fees_native += swap_fee;
bank.collected_fees_native += deposit_fee;
let change_amount = change.amount - loan_origination_fee - swap_fee;
let change_amount = change.amount - loan_origination_fee - deposit_fee;
let native_after_change = native + change_amount;
if bank.are_deposits_reduce_only() {
require!(
@ -415,7 +415,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
price: oracle_price.to_bits(),
swap_fee: swap_fee.to_bits(),
deposit_fee: deposit_fee.to_bits(),
});
emit!(TokenBalanceLog {

View File

@ -41,7 +41,7 @@ pub fn token_edit(
force_close_opt: Option<bool>,
token_conditional_swap_taker_fee_rate_opt: Option<f32>,
token_conditional_swap_maker_fee_rate_opt: Option<f32>,
flash_loan_swap_fee_rate_opt: Option<f32>,
flash_loan_deposit_fee_rate_opt: Option<f32>,
) -> Result<()> {
let group = ctx.accounts.group.load()?;
@ -308,35 +308,35 @@ pub fn token_edit(
require_group_admin = true;
};
if let Some(fee_fraction) = token_conditional_swap_taker_fee_rate_opt {
if let Some(fee_rate) = token_conditional_swap_taker_fee_rate_opt {
msg!(
"Token conditional swap taker fee fraction old {:?}, new {:?}",
bank.token_conditional_swap_taker_fee_rate,
fee_fraction
fee_rate
);
require_gte!(fee_fraction, 0.0); // values <0 are not currently supported
bank.token_conditional_swap_taker_fee_rate = fee_fraction;
require_gte!(fee_rate, 0.0); // values <0 are not currently supported
bank.token_conditional_swap_taker_fee_rate = fee_rate;
require_group_admin = true;
}
if let Some(fees_fraction) = token_conditional_swap_maker_fee_rate_opt {
if let Some(fee_rate) = token_conditional_swap_maker_fee_rate_opt {
msg!(
"Token conditional swap maker fee fraction old {:?}, new {:?}",
bank.token_conditional_swap_maker_fee_rate,
fees_fraction
fee_rate
);
require_gte!(fees_fraction, 0.0); // values <0 are not currently supported
bank.token_conditional_swap_maker_fee_rate = fees_fraction;
require_gte!(fee_rate, 0.0); // values <0 are not currently supported
bank.token_conditional_swap_maker_fee_rate = fee_rate;
require_group_admin = true;
}
if let Some(fees_fraction) = flash_loan_swap_fee_rate_opt {
if let Some(fee_rate) = flash_loan_deposit_fee_rate_opt {
msg!(
"Flash loan swap fee fraction old {:?}, new {:?}",
bank.flash_loan_swap_fee_rate,
fees_fraction
bank.flash_loan_deposit_fee_rate,
fee_rate
);
require_gte!(fees_fraction, 0.0); // values <0 are not currently supported
bank.flash_loan_swap_fee_rate = fees_fraction;
require_gte!(fee_rate, 0.0); // values <0 are not currently supported
bank.flash_loan_deposit_fee_rate = fee_rate;
require_group_admin = true;
}
}

View File

@ -37,7 +37,7 @@ pub fn token_register(
reduce_only: u8,
token_conditional_swap_taker_fee_rate: f32,
token_conditional_swap_maker_fee_rate: f32,
flash_loan_swap_fee_rate: f32,
flash_loan_deposit_fee_rate: f32,
) -> Result<()> {
// Require token 0 to be in the insurance token
if token_index == INSURANCE_TOKEN_INDEX {
@ -107,7 +107,7 @@ pub fn token_register(
fees_withdrawn: 0,
token_conditional_swap_taker_fee_rate,
token_conditional_swap_maker_fee_rate,
flash_loan_swap_fee_rate,
flash_loan_deposit_fee_rate,
reserved: [0; 2092],
};

View File

@ -79,7 +79,7 @@ pub fn token_register_trustless(
fees_withdrawn: 0,
token_conditional_swap_taker_fee_rate: 0.0005,
token_conditional_swap_maker_fee_rate: 0.0005,
flash_loan_swap_fee_rate: 0.0005,
flash_loan_deposit_fee_rate: 0.0005,
reserved: [0; 2092],
};
require_gt!(bank.max_rate, MINIMUM_MAX_RATE);

View File

@ -145,7 +145,7 @@ pub mod mango_v4 {
reduce_only: u8,
token_conditional_swap_taker_fee_rate: f32,
token_conditional_swap_maker_fee_rate: f32,
flash_loan_swap_fee_rate: f32,
flash_loan_deposit_fee_rate: f32,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::token_register(
@ -172,7 +172,7 @@ pub mod mango_v4 {
reduce_only,
token_conditional_swap_taker_fee_rate,
token_conditional_swap_maker_fee_rate,
flash_loan_swap_fee_rate,
flash_loan_deposit_fee_rate,
)?;
Ok(())
}
@ -216,7 +216,7 @@ pub mod mango_v4 {
force_close_opt: Option<bool>,
token_conditional_swap_taker_fee_rate_opt: Option<f32>,
token_conditional_swap_maker_fee_rate_opt: Option<f32>,
flash_loan_swap_fee_rate_opt: Option<f32>,
flash_loan_deposit_fee_rate_opt: Option<f32>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::token_edit(
@ -247,7 +247,7 @@ pub mod mango_v4 {
force_close_opt,
token_conditional_swap_taker_fee_rate_opt,
token_conditional_swap_maker_fee_rate_opt,
flash_loan_swap_fee_rate_opt,
flash_loan_deposit_fee_rate_opt,
)?;
Ok(())
}

View File

@ -67,7 +67,7 @@ pub struct FlashLoanTokenDetailV2 {
pub deposit_index: i128,
pub borrow_index: i128,
pub price: i128,
pub swap_fee: i128,
pub deposit_fee: i128,
}
#[event]

View File

@ -146,7 +146,7 @@ pub struct Bank {
pub token_conditional_swap_taker_fee_rate: f32,
pub token_conditional_swap_maker_fee_rate: f32,
pub flash_loan_swap_fee_rate: f32,
pub flash_loan_deposit_fee_rate: f32,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 2092],
@ -259,7 +259,7 @@ impl Bank {
fees_withdrawn: 0,
token_conditional_swap_taker_fee_rate: 0.0,
token_conditional_swap_maker_fee_rate: 0.0,
flash_loan_swap_fee_rate: 0.0,
flash_loan_deposit_fee_rate: 0.0,
reserved: [0; 2092],
}
}
@ -285,7 +285,7 @@ impl Bank {
require_gte!(2, self.reduce_only);
require_gte!(self.token_conditional_swap_taker_fee_rate, 0.0);
require_gte!(self.token_conditional_swap_maker_fee_rate, 0.0);
require_gte!(self.flash_loan_swap_fee_rate, 0.0);
require_gte!(self.flash_loan_deposit_fee_rate, 0.0);
Ok(())
}

View File

@ -242,7 +242,7 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
}
#[tokio::test]
async fn test_flash_loan_swap_fee() -> Result<(), BanksClientError> {
async fn test_flash_loan_deposit_fee() -> Result<(), BanksClientError> {
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(100_000);
let context = test_builder.start_default().await;
@ -271,7 +271,7 @@ async fn test_flash_loan_swap_fee() -> Result<(), BanksClientError> {
.create(solana)
.await;
let swap_fee_fraction = 0.042f64;
let deposit_fee_rate = 0.042f64;
send_tx(
solana,
TokenEdit {
@ -279,7 +279,7 @@ async fn test_flash_loan_swap_fee() -> Result<(), BanksClientError> {
admin,
mint: tokens[1].mint.pubkey,
options: mango_v4::instruction::TokenEdit {
flash_loan_swap_fee_rate_opt: Some(swap_fee_fraction as f32),
flash_loan_deposit_fee_rate_opt: Some(deposit_fee_rate as f32),
..token_edit_instruction_default()
},
},
@ -412,10 +412,16 @@ async fn test_flash_loan_swap_fee() -> Result<(), BanksClientError> {
provided_amount + deposit_amount
);
let mango_withdraw_amount = account_position_f64(solana, account, tokens[0].bank).await;
assert!(balance_f64eq(
mango_withdraw_amount,
(initial_deposit - withdraw_amount) as f64
));
let mango_deposit_amount = account_position_f64(solana, account, tokens[1].bank).await;
assert!(balance_f64eq(
mango_deposit_amount,
deposit_amount as f64 * (1.0 - swap_fee_fraction)
deposit_amount as f64 * (1.0 - deposit_fee_rate)
));
Ok(())

View File

@ -875,7 +875,7 @@ impl ClientInstruction for TokenRegisterInstruction {
reduce_only: 0,
token_conditional_swap_taker_fee_rate: 0.0,
token_conditional_swap_maker_fee_rate: 0.0,
flash_loan_swap_fee_rate: 0.0,
flash_loan_deposit_fee_rate: 0.0,
};
let bank = Pubkey::find_program_address(
@ -1119,7 +1119,7 @@ pub fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit {
force_close_opt: None,
token_conditional_swap_taker_fee_rate_opt: None,
token_conditional_swap_maker_fee_rate_opt: None,
flash_loan_swap_fee_rate_opt: None,
flash_loan_deposit_fee_rate_opt: None,
}
}

View File

@ -151,7 +151,7 @@ async function tokenEdit(): Promise<void> {
params.forceClose,
params.tokenConditionalSwapTakerFeeRate,
params.tokenConditionalSwapMakerFeeRate,
params.flashLoanSwapFeeRate,
params.flashLoanDepositFeeRate,
)
.accounts({
group: group.publicKey,

View File

@ -124,7 +124,7 @@ export class Bank implements BankForHealth {
feesWithdrawn: BN;
tokenConditionalSwapTakerFeeRate: number;
tokenConditionalSwapMakerFeeRate: number;
flashLoanSwapFeeRate: number;
flashLoanDepositFeeRate: number;
},
): Bank {
return new Bank(
@ -175,7 +175,7 @@ export class Bank implements BankForHealth {
obj.feesWithdrawn,
obj.tokenConditionalSwapTakerFeeRate,
obj.tokenConditionalSwapMakerFeeRate,
obj.flashLoanSwapFeeRate,
obj.flashLoanDepositFeeRate,
);
}
@ -227,7 +227,7 @@ export class Bank implements BankForHealth {
public feesWithdrawn: BN,
public tokenConditionalSwapTakerFeeRate: number,
public tokenConditionalSwapMakerFeeRate: number,
public flashLoanSwapFeeRate: number,
public flashLoanDepositFeeRate: number,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
this.oracleConfig = {

View File

@ -404,7 +404,7 @@ export class MangoClient {
params.reduceOnly,
params.tokenConditionalSwapTakerFeeRate,
params.tokenConditionalSwapMakerFeeRate,
params.flashLoanSwapFeeRate,
params.flashLoanDepositFeeRate,
)
.accounts({
group: group.publicKey,
@ -479,7 +479,7 @@ export class MangoClient {
params.forceClose,
params.tokenConditionalSwapTakerFeeRate,
params.tokenConditionalSwapMakerFeeRate,
params.flashLoanSwapFeeRate,
params.flashLoanDepositFeeRate,
)
.accounts({
group: group.publicKey,

View File

@ -24,7 +24,7 @@ export interface TokenRegisterParams {
reduceOnly: number;
tokenConditionalSwapTakerFeeRate: number;
tokenConditionalSwapMakerFeeRate: number;
flashLoanSwapFeeRate: number;
flashLoanDepositFeeRate: number;
}
export const DefaultTokenRegisterParams: TokenRegisterParams = {
@ -59,7 +59,7 @@ export const DefaultTokenRegisterParams: TokenRegisterParams = {
reduceOnly: 0,
tokenConditionalSwapTakerFeeRate: 0.0005,
tokenConditionalSwapMakerFeeRate: 0.0005,
flashLoanSwapFeeRate: 0.0005,
flashLoanDepositFeeRate: 0.0005,
};
export interface TokenEditParams {
@ -89,7 +89,7 @@ export interface TokenEditParams {
forceClose: boolean | null;
tokenConditionalSwapTakerFeeRate: number | null;
tokenConditionalSwapMakerFeeRate: number | null;
flashLoanSwapFeeRate: number | null;
flashLoanDepositFeeRate: number | null;
}
export const NullTokenEditParams: TokenEditParams = {
@ -119,7 +119,7 @@ export const NullTokenEditParams: TokenEditParams = {
forceClose: null,
tokenConditionalSwapTakerFeeRate: null,
tokenConditionalSwapMakerFeeRate: null,
flashLoanSwapFeeRate: null,
flashLoanDepositFeeRate: null,
};
export interface PerpEditParams {

View File

@ -594,7 +594,7 @@ export type MangoV4 = {
"type": "f32"
},
{
"name": "flashLoanSwapFeeRate",
"name": "flashLoanDepositFeeRate",
"type": "f32"
}
]
@ -926,7 +926,7 @@ export type MangoV4 = {
}
},
{
"name": "flashLoanSwapFeeRateOpt",
"name": "flashLoanDepositFeeRateOpt",
"type": {
"option": "f32"
}
@ -6670,7 +6670,7 @@ export type MangoV4 = {
"type": "f32"
},
{
"name": "flashLoanSwapFeeRate",
"name": "flashLoanDepositFeeRate",
"type": "f32"
},
{
@ -8256,7 +8256,7 @@ export type MangoV4 = {
"type": "i128"
},
{
"name": "swapFee",
"name": "depositFee",
"type": "i128"
}
]
@ -13131,7 +13131,7 @@ export const IDL: MangoV4 = {
"type": "f32"
},
{
"name": "flashLoanSwapFeeRate",
"name": "flashLoanDepositFeeRate",
"type": "f32"
}
]
@ -13463,7 +13463,7 @@ export const IDL: MangoV4 = {
}
},
{
"name": "flashLoanSwapFeeRateOpt",
"name": "flashLoanDepositFeeRateOpt",
"type": {
"option": "f32"
}
@ -19207,7 +19207,7 @@ export const IDL: MangoV4 = {
"type": "f32"
},
{
"name": "flashLoanSwapFeeRate",
"name": "flashLoanDepositFeeRate",
"type": "f32"
},
{
@ -20793,7 +20793,7 @@ export const IDL: MangoV4 = {
"type": "i128"
},
{
"name": "swapFee",
"name": "depositFee",
"type": "i128"
}
]