1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
use anchor_lang::prelude::*;
use derivative::Derivative;
use fixed::types::I80F48;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use static_assertions::const_assert_eq;
use std::mem::size_of;
use crate::error::MangoError;
use crate::i80f48::ClampToInt;
use crate::state::*;
/// Incentive to pay to callers who start an auction, in $1e-6
pub const TCS_START_INCENTIVE: u64 = 1_000; // $0.001 around 10x tx fee right now
#[derive(
Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, AnchorDeserialize, AnchorSerialize,
)]
#[repr(u8)]
pub enum TokenConditionalSwapDisplayPriceStyle {
SellTokenPerBuyToken,
BuyTokenPerSellToken,
}
#[derive(
Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, AnchorDeserialize, AnchorSerialize,
)]
#[repr(u8)]
pub enum TokenConditionalSwapIntention {
Unknown,
/// Reducing a position when the price gets worse
StopLoss,
/// Reducing a position when the price gets better
TakeProfit,
}
#[derive(
Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, AnchorDeserialize, AnchorSerialize,
)]
#[repr(u8)]
pub enum TokenConditionalSwapType {
FixedPremium,
PremiumAuction,
LinearAuction,
}
#[zero_copy]
#[derive(AnchorDeserialize, AnchorSerialize, Derivative, PartialEq)]
#[derivative(Debug)]
pub struct TokenConditionalSwap {
pub id: u64,
/// maximum amount of native tokens to buy or sell
pub max_buy: u64,
pub max_sell: u64,
/// how many native tokens were already bought/sold
pub bought: u64,
pub sold: u64,
/// timestamp until which the conditional swap is valid
pub expiry_timestamp: u64,
/// The lower or starting price:
/// - For FixedPremium or PremiumAuctions, it's the lower end of the price range:
/// the tcs can only be triggered if the oracle price exceeds this value.
/// - For LinearAuctions it's the starting price that's offered at start_timestamp.
///
/// The price is always in "sell_token per buy_token" units, which can be computed
/// by dividing the buy token price by the sell token price.
///
/// For FixedPremium or PremiumAuctions:
///
/// The price must exceed this threshold to allow execution.
///
/// This threshold is compared to the "sell_token per buy_token" oracle price.
/// If that price is >= lower_limit and <= upper_limit the tcs may be executable.
///
/// Example: Stop loss to get out of a SOL long: The user bought SOL at 20 USDC/SOL
/// and wants to stop loss at 18 USDC/SOL. They'd set buy_token=USDC, sell_token=SOL
/// so the reference price is in SOL/USDC units. Set price_lower_limit=toNative(1/18)
/// and price_upper_limit=toNative(1/10). Also set allow_borrows=false.
///
/// Example: Want to buy SOL with USDC if the price falls below 22 USDC/SOL.
/// buy_token=SOL, sell_token=USDC, reference price is in USDC/SOL units. Set
/// price_upper_limit=toNative(22), price_lower_limit=0.
pub price_lower_limit: f64,
/// Parallel to price_lower_limit, but an upper limit / auction end price.
pub price_upper_limit: f64,
/// The premium to pay over oracle price to incentivize execution.
pub price_premium_rate: f64,
/// The taker receives only premium_price * (1 - taker_fee_rate)
pub taker_fee_rate: f32,
/// The maker has to pay premium_price * (1 + maker_fee_rate)
pub maker_fee_rate: f32,
/// indexes of tokens for the swap
pub buy_token_index: TokenIndex,
pub sell_token_index: TokenIndex,
/// If this struct is in use. (tcs are stored in a static-length array)
pub is_configured: u8,
/// may token purchases create deposits? (often users just want to get out of a borrow)
pub allow_creating_deposits: u8,
/// may token selling create borrows? (often users just want to get out of a long)
pub allow_creating_borrows: u8,
/// The stored prices are always "sell token per buy token", but if the user
/// used "buy token per sell token" when creating the tcs order, we should continue
/// to show them prices in that way.
///
/// Stores a TokenConditionalSwapDisplayPriceStyle enum value
pub display_price_style: u8,
/// The intention the user had when placing this order, display-only
///
/// Stores a TokenConditionalSwapIntention enum value
pub intention: u8,
/// Stores a TokenConditionalSwapType enum value
pub tcs_type: u8,
pub padding: [u8; 6],
/// In seconds since epoch. 0 means not-started.
///
/// FixedPremium: Time of first trigger call. No other effect.
/// PremiumAuction: Time of start or first trigger call. Can continue to trigger once started.
/// LinearAuction: Set during creation, auction starts with price_lower_limit at this timestamp.
pub start_timestamp: u64,
/// Duration of the auction mechanism
///
/// FixedPremium: ignored
/// PremiumAuction: time after start that the premium needs to scale to price_premium_rate
/// LinearAuction: time after start to go from price_lower_limit to price_upper_limit
pub duration_seconds: u64,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 88],
}
const_assert_eq!(
size_of::<TokenConditionalSwap>(),
8 * 6 + 8 * 3 + 2 * 4 + 2 * 2 + 1 * 6 + 6 + 2 * 8 + 88
);
const_assert_eq!(size_of::<TokenConditionalSwap>(), 200);
const_assert_eq!(size_of::<TokenConditionalSwap>() % 8, 0);
impl Default for TokenConditionalSwap {
fn default() -> Self {
Self {
id: 0,
max_buy: 0,
max_sell: 0,
bought: 0,
sold: 0,
expiry_timestamp: u64::MAX,
price_lower_limit: 0.0,
price_upper_limit: 0.0,
price_premium_rate: 0.0,
taker_fee_rate: 0.0,
maker_fee_rate: 0.0,
buy_token_index: TokenIndex::MAX,
sell_token_index: TokenIndex::MAX,
is_configured: 0,
allow_creating_borrows: 0,
allow_creating_deposits: 0,
display_price_style: TokenConditionalSwapDisplayPriceStyle::SellTokenPerBuyToken.into(),
intention: TokenConditionalSwapIntention::Unknown.into(),
tcs_type: TokenConditionalSwapType::FixedPremium.into(),
padding: Default::default(),
start_timestamp: 0,
duration_seconds: 0,
reserved: [0; 88],
}
}
}
impl TokenConditionalSwap {
/// Whether the entry is in use
///
/// Note that it's possible for an entry to be configured but expired.
/// Or to be configured but not started yet.
pub fn is_configured(&self) -> bool {
self.is_configured == 1
}
pub fn set_is_configured(&mut self, is_configured: bool) {
self.is_configured = u8::from(is_configured);
}
pub fn tcs_type(&self) -> TokenConditionalSwapType {
self.tcs_type.try_into().unwrap()
}
pub fn is_expired(&self, now_ts: u64) -> bool {
now_ts >= self.expiry_timestamp
}
pub fn passed_start(&self, now_ts: u64) -> bool {
self.start_timestamp > 0 && now_ts >= self.start_timestamp
}
/// Does this tcs type support an explicit tcs_start instruction call?
pub fn is_startable_type(&self) -> bool {
self.tcs_type() == TokenConditionalSwapType::PremiumAuction
}
pub fn allow_creating_deposits(&self) -> bool {
self.allow_creating_deposits == 1
}
pub fn allow_creating_borrows(&self) -> bool {
self.allow_creating_borrows == 1
}
pub fn remaining_buy(&self) -> u64 {
self.max_buy - self.bought
}
pub fn remaining_sell(&self) -> u64 {
self.max_sell - self.sold
}
fn start_timestamp_or_now(&self, now_ts: u64) -> u64 {
if self.start_timestamp > 0 {
self.start_timestamp
} else {
now_ts
}
}
/// Base price adjusted for the premium
///
/// Base price is the amount of sell_token to pay for one buy_token.
pub fn premium_price(&self, base_price: f64, now_ts: u64) -> f64 {
match self.tcs_type() {
TokenConditionalSwapType::FixedPremium => base_price * (1.0 + self.price_premium_rate),
TokenConditionalSwapType::PremiumAuction => {
// Start dynamically when triggerable
let start = self.start_timestamp_or_now(now_ts);
assert!(start <= now_ts);
let duration = self.duration_seconds as f64;
let current = (now_ts - start) as f64;
let progress = (current / duration).min(1.0);
base_price * (1.0 + progress * self.price_premium_rate)
}
TokenConditionalSwapType::LinearAuction => {
// Start time is fixed
assert!(self.passed_start(now_ts));
let duration = self.duration_seconds;
let current = now_ts - self.start_timestamp;
if current < duration {
let progress = (current as f64) / (duration as f64);
self.price_lower_limit
+ progress * (self.price_upper_limit - self.price_lower_limit)
} else {
// explicitly handle the end to avoid rounding issues
self.price_upper_limit
}
}
}
}
/// Premium price adjusted for the maker fee
pub fn maker_price(&self, premium_price: f64) -> f64 {
premium_price * (1.0 + self.maker_fee_rate as f64)
}
/// Premium price adjusted for the taker fee
pub fn taker_price(&self, premium_price: f64) -> f64 {
premium_price * (1.0 - self.taker_fee_rate as f64)
}
pub fn maker_fee(&self, base_sell_amount: I80F48) -> u64 {
(base_sell_amount * I80F48::from_num(self.maker_fee_rate))
.floor()
.to_num()
}
pub fn taker_fee(&self, base_sell_amount: I80F48) -> u64 {
(base_sell_amount * I80F48::from_num(self.taker_fee_rate))
.floor()
.to_num()
}
fn price_in_range(&self, price: f64) -> bool {
price >= self.price_lower_limit && price <= self.price_upper_limit
}
/// Do the current conditions and tcs type allow starting?
pub fn check_startable(&self, price: f64, now_ts: u64) -> Result<()> {
require!(
!self.is_expired(now_ts),
MangoError::TokenConditionalSwapExpired
);
require!(
self.start_timestamp == 0,
MangoError::TokenConditionalSwapAlreadyStarted
);
require!(
self.is_startable_type(),
MangoError::TokenConditionalSwapTypeNotStartable
);
require!(
self.price_in_range(price),
MangoError::TokenConditionalSwapPriceNotInRange
);
Ok(())
}
pub fn is_startable(&self, price: f64, now_ts: u64) -> bool {
self.check_startable(price, now_ts).is_ok()
}
pub fn check_triggerable(&self, price: f64, now_ts: u64) -> Result<()> {
require!(
!self.is_expired(now_ts),
MangoError::TokenConditionalSwapExpired
);
match self.tcs_type() {
TokenConditionalSwapType::FixedPremium => {
require!(
self.price_in_range(price),
MangoError::TokenConditionalSwapPriceNotInRange
);
}
TokenConditionalSwapType::PremiumAuction | TokenConditionalSwapType::LinearAuction => {
// Triggerable once started, whatever the current oracle price
require!(
self.passed_start(now_ts),
MangoError::TokenConditionalSwapNotStarted
);
}
}
Ok(())
}
pub fn is_triggerable(&self, price: f64, now_ts: u64) -> bool {
self.check_triggerable(price, now_ts).is_ok()
}
/// The remaining buy amount, taking the current buy token position and
/// buy bank's reduce-only status into account.
///
/// Note that the account health might further restrict execution.
pub fn max_buy_for_position(&self, buy_position: I80F48, buy_bank: &Bank) -> u64 {
self.remaining_buy().min(
if self.allow_creating_deposits() && !buy_bank.are_deposits_reduce_only() {
u64::MAX
} else {
// ceil() because we're ok reaching 0..1 deposited native tokens
(-buy_position).ceil().clamp_to_u64()
},
)
}
/// The remaining sell amount, taking the current sell token position and
/// sell bank's reduce-only status into account.
///
/// Note that the account health might further restrict execution.
pub fn max_sell_for_position(&self, sell_position: I80F48, sell_bank: &Bank) -> u64 {
self.remaining_sell().min(
if self.allow_creating_borrows() && !sell_bank.are_borrows_reduce_only() {
u64::MAX
} else {
// floor() so we never go below 0
sell_position.floor().clamp_to_u64()
},
)
}
}