use num_traits::{CheckedAdd, CheckedMul, CheckedSub, Signed, Zero};
use crate::{
market::{
BaseMarket, BaseMarketExt, BaseMarketMutExt, LiquidityMarketExt, LiquidityMarketMut,
SwapMarketExt, SwapMarketMutExt,
},
num::{MulDiv, Unsigned, UnsignedAbs},
params::Fees,
pool::delta::{BalanceChange, PriceImpact},
price::{Price, Prices},
utils, BalanceExt, PnlFactorKind, PoolExt,
};
use super::MarketAction;
#[must_use = "actions do nothing unless you `execute` them"]
pub struct Deposit<M: BaseMarket<DECIMALS>, const DECIMALS: u8> {
market: M,
params: DepositParams<M::Num>,
include_virtual_inventory_impact: bool,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(
feature = "anchor-lang",
derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
)]
pub struct DepositParams<T> {
long_token_amount: T,
short_token_amount: T,
prices: Prices<T>,
}
#[cfg(feature = "gmsol-utils")]
impl<T: gmsol_utils::InitSpace> gmsol_utils::InitSpace for DepositParams<T> {
const INIT_SPACE: usize = 2 * T::INIT_SPACE + Prices::<T>::INIT_SPACE;
}
impl<T> DepositParams<T> {
pub fn long_token_amount(&self) -> &T {
&self.long_token_amount
}
pub fn short_token_amount(&self) -> &T {
&self.short_token_amount
}
pub fn long_token_price(&self) -> &Price<T> {
&self.prices.long_token_price
}
pub fn short_token_price(&self) -> &Price<T> {
&self.prices.short_token_price
}
fn reassign_values(&self, is_long_token: bool) -> ReassignedValues<T>
where
T: Clone,
{
if is_long_token {
ReassignedValues {
amount: self.long_token_amount.clone(),
price: self.long_token_price(),
opposite_price: self.short_token_price(),
}
} else {
ReassignedValues {
amount: self.short_token_amount.clone(),
price: self.short_token_price(),
opposite_price: self.long_token_price(),
}
}
}
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(
feature = "anchor-lang",
derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
)]
pub struct DepositReport<Unsigned, Signed> {
params: DepositParams<Unsigned>,
minted: Unsigned,
price_impact: Signed,
fees: [Fees<Unsigned>; 2],
}
#[cfg(feature = "gmsol-utils")]
impl<Unsigned, Signed> gmsol_utils::InitSpace for DepositReport<Unsigned, Signed>
where
Unsigned: gmsol_utils::InitSpace,
Signed: gmsol_utils::InitSpace,
{
const INIT_SPACE: usize = DepositParams::<Unsigned>::INIT_SPACE
+ Unsigned::INIT_SPACE
+ Signed::INIT_SPACE
+ 2 * Fees::<Unsigned>::INIT_SPACE;
}
impl<T> DepositReport<T, T::Signed>
where
T: Unsigned,
{
fn new(
params: DepositParams<T>,
price_impact: PriceImpact<T::Signed>,
minted: T,
fees: [Fees<T>; 2],
) -> Self {
Self {
params,
minted,
price_impact: price_impact.value,
fees,
}
}
pub fn minted(&self) -> &T {
&self.minted
}
pub fn price_impact(&self) -> &T::Signed {
&self.price_impact
}
pub fn params(&self) -> &DepositParams<T> {
&self.params
}
pub fn long_token_fees(&self) -> &Fees<T> {
&self.fees[0]
}
pub fn short_token_fees(&self) -> &Fees<T> {
&self.fees[1]
}
}
impl<const DECIMALS: u8, M: LiquidityMarketMut<DECIMALS>> Deposit<M, DECIMALS> {
pub fn try_new(
market: M,
long_token_amount: M::Num,
short_token_amount: M::Num,
prices: Prices<M::Num>,
) -> Result<Self, crate::Error> {
if long_token_amount.is_zero() && short_token_amount.is_zero() {
return Err(crate::Error::EmptyDeposit);
}
Ok(Self {
market,
params: DepositParams {
long_token_amount,
short_token_amount,
prices,
},
include_virtual_inventory_impact: true,
})
}
pub fn with_virtual_inventory_impact(mut self, include: bool) -> Self {
self.include_virtual_inventory_impact = include;
self
}
fn price_impact(&self) -> crate::Result<PriceImpactWithDeltas<M::Num>> {
let delta = self.market.liquidity_pool()?.pool_delta_with_amounts(
&self
.params
.long_token_amount
.clone()
.try_into()
.map_err(|_| crate::Error::Convert)?,
&self
.params
.short_token_amount
.clone()
.try_into()
.map_err(|_| crate::Error::Convert)?,
&self.params.long_token_price().mid(),
&self.params.short_token_price().mid(),
)?;
let price_impact = self
.market
.swap_impact_value(&delta, self.include_virtual_inventory_impact)?;
let delta = delta.delta();
debug_assert!(!delta.long_value().is_negative(), "must be non-negative");
debug_assert!(!delta.short_value().is_negative(), "must be non-negative");
Ok(PriceImpactWithDeltas {
price_impact,
long_token_usd_value: delta.long_value().unsigned_abs(),
short_token_usd_value: delta.short_value().unsigned_abs(),
})
}
fn charge_fees(
&self,
balance_change: BalanceChange,
amount: &mut M::Num,
) -> crate::Result<Fees<M::Num>> {
let (amount_after_fees, fees) = self
.market
.swap_fee_params()?
.apply_fees(balance_change, amount)
.ok_or(crate::Error::Computation("apply fees"))?;
*amount = amount_after_fees;
Ok(fees)
}
fn execute_deposit(
&mut self,
is_long_token: bool,
pool_value: M::Num,
PriceImpact {
value: mut price_impact,
balance_change,
}: PriceImpact<M::Signed>,
) -> Result<(M::Num, Fees<M::Num>), crate::Error> {
let mut mint_amount: M::Num = Zero::zero();
let supply = self.market.total_supply();
if pool_value.is_zero() && !supply.is_zero() {
return Err(crate::Error::InvalidPoolValue("deposit"));
}
let ReassignedValues {
mut amount,
price,
opposite_price,
} = self.params.reassign_values(is_long_token);
let fees = self.charge_fees(balance_change, &mut amount)?;
self.market.claimable_fee_pool_mut()?.apply_delta_amount(
is_long_token,
&fees
.fee_amount_for_receiver()
.clone()
.try_into()
.map_err(|_| crate::Error::Convert)?,
)?;
if price_impact.is_positive() && supply.is_zero() {
price_impact = Zero::zero();
}
if price_impact.is_positive() {
let positive_impact_amount = self.market.apply_swap_impact_value_with_cap(
!is_long_token,
opposite_price,
&price_impact,
)?;
mint_amount = mint_amount
.checked_add(
&utils::usd_to_market_token_amount(
positive_impact_amount
.checked_mul(opposite_price.pick_price(true))
.ok_or(crate::Error::Overflow)?,
pool_value.clone(),
supply.clone(),
self.market.usd_to_amount_divisor(),
)
.ok_or(crate::Error::Computation("convert positive usd to amount"))?,
)
.ok_or(crate::Error::Overflow)?;
self.market.apply_delta(
!is_long_token,
&positive_impact_amount
.try_into()
.map_err(|_| crate::Error::Convert)?,
)?;
self.market.validate_pool_amount(!is_long_token)?;
} else if price_impact.is_negative() {
let negative_impact_amount = self.market.apply_swap_impact_value_with_cap(
is_long_token,
price,
&price_impact,
)?;
amount =
amount
.checked_sub(&negative_impact_amount)
.ok_or(crate::Error::Computation(
"deposit: not enough fund to pay negative impact amount",
))?;
}
mint_amount = mint_amount
.checked_add(
&utils::usd_to_market_token_amount(
amount
.checked_mul(price.pick_price(false))
.ok_or(crate::Error::Overflow)?,
pool_value,
supply.clone(),
self.market.usd_to_amount_divisor(),
)
.ok_or(crate::Error::Computation("convert negative usd to amount"))?,
)
.ok_or(crate::Error::Overflow)?;
self.market.apply_delta(
is_long_token,
&(amount
.checked_add(fees.fee_amount_for_pool())
.ok_or(crate::Error::Overflow)?)
.clone()
.try_into()
.map_err(|_| crate::Error::Convert)?,
)?;
self.market.validate_pool_amount(is_long_token)?;
self.market
.validate_pool_value_for_deposit(&self.params.prices, is_long_token)?;
Ok((mint_amount, fees))
}
}
impl<const DECIMALS: u8, M> MarketAction for Deposit<M, DECIMALS>
where
M: LiquidityMarketMut<DECIMALS>,
{
type Report = DepositReport<M::Num, <M::Num as Unsigned>::Signed>;
fn execute(mut self) -> crate::Result<Self::Report> {
debug_assert!(
!self.params.long_token_amount.is_zero() || !self.params.short_token_amount.is_zero(),
"shouldn't be empty deposit"
);
self.market.validate_max_pnl(
&self.params.prices,
PnlFactorKind::MaxAfterDeposit,
PnlFactorKind::MaxAfterDeposit,
)?;
let report = {
let PriceImpactWithDeltas {
price_impact,
long_token_usd_value,
short_token_usd_value,
} = self.price_impact()?;
let mut market_token_to_mint: M::Num = Zero::zero();
let pool_value = self.market.pool_value(
&self.params.prices,
PnlFactorKind::MaxAfterDeposit,
true,
)?;
if pool_value.is_negative() {
return Err(crate::Error::InvalidPoolValue(
"deposit: current pool value is negative",
));
}
let mut all_fees = [Default::default(), Default::default()];
if !self.params.long_token_amount.is_zero() {
let adjusted_price_impact = long_token_usd_value
.clone()
.checked_mul_div_with_signed_numerator(
&price_impact.value,
&long_token_usd_value
.checked_add(&short_token_usd_value)
.ok_or(crate::Error::Overflow)?,
)
.ok_or(crate::Error::Computation("price impact for long"))?;
let (mint_amount, fees) = self.execute_deposit(
true,
pool_value.unsigned_abs(),
PriceImpact {
value: adjusted_price_impact,
balance_change: price_impact.balance_change,
},
)?;
market_token_to_mint = market_token_to_mint
.checked_add(&mint_amount)
.ok_or(crate::Error::Overflow)?;
all_fees[0] = fees;
}
if !self.params.short_token_amount.is_zero() {
let adjusted_price_impact = short_token_usd_value
.clone()
.checked_mul_div_with_signed_numerator(
&price_impact.value,
&long_token_usd_value
.checked_add(&short_token_usd_value)
.ok_or(crate::Error::Overflow)?,
)
.ok_or(crate::Error::Computation("price impact for short"))?;
let (mint_amount, fees) = self.execute_deposit(
false,
pool_value.unsigned_abs(),
PriceImpact {
value: adjusted_price_impact,
balance_change: price_impact.balance_change,
},
)?;
market_token_to_mint = market_token_to_mint
.checked_add(&mint_amount)
.ok_or(crate::Error::Overflow)?;
all_fees[1] = fees;
}
DepositReport::new(self.params, price_impact, market_token_to_mint, all_fees)
};
self.market.mint(&report.minted)?;
Ok(report)
}
}
struct ReassignedValues<'a, T> {
amount: T,
price: &'a Price<T>,
opposite_price: &'a Price<T>,
}
struct PriceImpactWithDeltas<T: Unsigned> {
price_impact: PriceImpact<T::Signed>,
long_token_usd_value: T,
short_token_usd_value: T,
}
#[cfg(test)]
mod tests {
use crate::{
market::LiquidityMarketMutExt,
price::Prices,
test::{TestMarket, TestMarketConfig},
MarketAction,
};
#[test]
fn basic() -> Result<(), crate::Error> {
let mut market = TestMarket::<u64, 9>::with_config(TestMarketConfig {
reserve_factor: 1_050_000_000,
..Default::default()
});
let prices = Prices::new_for_test(120, 120, 1);
println!(
"{:#?}",
market.deposit(1_000_000_000, 0, prices)?.execute()?
);
println!("{market:#?}");
println!(
"{:#?}",
market.deposit(1_000_000_000, 0, prices)?.execute()?
);
println!("{market:#?}");
println!(
"{:#?}",
market.deposit(0, 1_000_000_000, prices)?.execute()?
);
println!("{market:#?}");
Ok(())
}
#[test]
fn sequence() -> crate::Result<()> {
let mut market_1 = TestMarket::<u64, 9>::default();
let prices = Prices::new_for_test(120, 120, 1);
println!(
"{:#?}",
market_1.deposit(1_000_000_000, 0, prices)?.execute()?
);
println!(
"{:#?}",
market_1.deposit(1_000_000_000, 0, prices)?.execute()?
);
println!("{market_1:#?}");
let mut market_2 = TestMarket::<u64, 9>::default();
println!(
"{:#?}",
market_2.deposit(2_000_000_000, 0, prices)?.execute()?
);
println!("{market_1:#?}");
println!("{market_2:#?}");
Ok(())
}
#[cfg(feature = "u128")]
#[test]
fn basic_u128() -> Result<(), crate::Error> {
let mut market = TestMarket::<u128, 20>::default();
let prices = Prices::new_for_test(12_000_000_000_000, 12_000_000_000_000, 100_000_000_000);
println!(
"{:#?}",
market.deposit(1_000_000_000, 0, prices)?.execute()?
);
println!(
"{:#?}",
market.deposit(1_000_000_000, 0, prices)?.execute()?
);
println!(
"{:#?}",
market.deposit(0, 1_000_000_000, prices)?.execute()?
);
println!("{market:#?}");
Ok(())
}
#[test]
fn zero_amount_deposit() -> Result<(), crate::Error> {
let mut market = TestMarket::<u64, 9>::default();
let prices = Prices::new_for_test(120, 120, 1);
let result = market.deposit(0, 0, prices);
assert!(result.is_err());
Ok(())
}
#[test]
fn extreme_amount_deposit() -> Result<(), crate::Error> {
let mut market = TestMarket::<u64, 9>::default();
let prices = Prices::new_for_test(120, 120, 1);
let small_amount = 1;
let large_amount = u64::MAX;
let max_pool_amount = 1000000000000000000;
println!("{:#?}", market.deposit(small_amount, 0, prices)?.execute()?);
println!("{market:#?}");
let result = market.deposit(large_amount, 0, prices)?.execute();
assert!(result.is_err());
println!("{market:#?}");
let result = market.deposit(max_pool_amount, 0, prices)?.execute();
assert!(result.is_err());
println!("{market:#?}");
Ok(())
}
#[test]
fn round_attack_deposit() -> Result<(), crate::Error> {
let mut market = TestMarket::<u64, 9>::default();
let prices = Prices::new_for_test(120, 120, 1);
let mut i = 1;
while i < 10000000 {
i += 1;
market.deposit(1, 0, prices)?.execute()?;
}
println!("{market:#?}");
let mut market_compare = TestMarket::<u64, 9>::default();
market_compare.deposit(10000000 - 1, 0, prices)?.execute()?;
println!("{market_compare:#?}");
Ok(())
}
#[test]
fn concurrent_deposits() -> Result<(), crate::Error> {
use std::sync::{Arc, Mutex};
use std::thread;
let market = Arc::new(Mutex::new(TestMarket::<u64, 9>::default()));
let prices = Prices::new_for_test(120, 120, 1);
let handles: Vec<_> = (0..10)
.map(|_| {
let market = Arc::clone(&market);
thread::spawn(move || {
let mut market = market.lock().unwrap();
market
.deposit(1_000_000_000, 0, prices)
.unwrap()
.execute()
.unwrap();
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
let market = market.lock().unwrap();
println!("{:#?}", *market);
Ok(())
}
#[test]
fn deposit_with_price_fluctuations() -> Result<(), crate::Error> {
let mut market = TestMarket::<u64, 9>::default();
let initial_prices = Prices::new_for_test(120, 120, 1);
let fluctuated_prices = Prices::new_for_test(240, 240, 1);
println!(
"{:#?}",
market
.deposit(1_000_000_000, 0, initial_prices)?
.execute()?
);
println!("{market:#?}");
println!(
"{:#?}",
market
.deposit(1_000_000_000, 0, fluctuated_prices)?
.execute()?
);
println!("{market:#?}");
Ok(())
}
}