levana_perpswap_cosmos/contracts/
countertrade.rs

1//! Countertrade contract
2
3use std::fmt::Display;
4
5use crate::{
6    price::PriceBaseInQuote,
7    storage::{
8        Collateral, DirectionToBase, LeverageToBase, LpToken, MarketId, NonZero, RawAddr,
9        TakeProfitTrader,
10    },
11};
12use cosmwasm_std::{Addr, Binary, Decimal256, Uint128};
13
14use super::market::{
15    deferred_execution::DeferredExecId,
16    position::{PositionId, PositionQueryResponse},
17};
18
19/// Message for instantiating a new countertrade contract.
20#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
21#[serde(rename_all = "snake_case")]
22pub struct InstantiateMsg {
23    /// Market contract we're countertrading on
24    pub market: RawAddr,
25    /// Administrator of the contract
26    pub admin: RawAddr,
27    /// Initial configuration values
28    pub config: ConfigUpdate,
29}
30
31/// Full configuration
32#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
33#[serde(rename_all = "snake_case")]
34/// Updates to configuration values.
35pub struct Config {
36    /// Administrator of the contract
37    pub admin: Addr,
38    /// Pending administrator, ready to be accepted, if any.
39    pub pending_admin: Option<Addr>,
40    /// Market address that we are allowed to open positions on
41    pub market: Addr,
42    /// Minimum funding rate for popular side
43    pub min_funding: Decimal256,
44    /// Target funding rate for popular side
45    pub target_funding: Decimal256,
46    /// Maximum funding rate for popular side
47    pub max_funding: Decimal256,
48    /// Allowed iterations to compute delta notional
49    pub iterations: u8,
50    /// Factor used to compute take profit price
51    pub take_profit_factor: Decimal256,
52    /// Factor used to compute stop loss price
53    pub stop_loss_factor: Decimal256,
54    /// Maximum leverage value we'll use
55    ///
56    /// If a market has lower max leverage, we use that instead
57    pub max_leverage: LeverageToBase,
58}
59
60impl Config {
61    /// Check validity of config values
62    pub fn check(&self) -> anyhow::Result<()> {
63        if self.min_funding >= self.target_funding {
64            Err(anyhow::anyhow!(
65                "Minimum funding must be strictly less than target"
66            ))
67        } else if self.target_funding >= self.max_funding {
68            Err(anyhow::anyhow!(
69                "Target funding must be strictly less than max"
70            ))
71        } else if self.max_leverage.into_decimal256() < Decimal256::from_ratio(2u32, 1u32) {
72            Err(anyhow::anyhow!("Max leverage must be at least 2"))
73        } else {
74            Ok(())
75        }
76    }
77}
78
79#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Default)]
80#[serde(rename_all = "snake_case")]
81#[allow(missing_docs)]
82/// Updates to configuration values.
83///
84/// See [Config] for field meanings.
85pub struct ConfigUpdate {
86    pub min_funding: Option<Decimal256>,
87    pub target_funding: Option<Decimal256>,
88    pub max_funding: Option<Decimal256>,
89    pub max_leverage: Option<LeverageToBase>,
90    pub iterations: Option<u8>,
91    pub take_profit_factor: Option<Decimal256>,
92    pub stop_loss_factor: Option<Decimal256>,
93}
94
95/// Executions available on the countertrade contract.
96#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
97#[serde(rename_all = "snake_case")]
98pub enum ExecuteMsg {
99    /// Cw20 interface
100    Receive {
101        /// Owner of funds sent to the contract
102        sender: RawAddr,
103        /// Amount of funds sent
104        amount: Uint128,
105        /// Must parse to a [ExecuteMsg]
106        msg: Binary,
107    },
108    /// Deposit funds for a given market
109    Deposit {},
110    /// Withdraw funds from a given market
111    Withdraw {
112        /// The number of LP shares to remove
113        amount: NonZero<LpToken>,
114    },
115    /// Perform a balancing operation on the given market
116    DoWork {},
117    /// Appoint a new administrator
118    AppointAdmin {
119        /// Address of the new administrator
120        admin: RawAddr,
121    },
122    /// Accept appointment of admin
123    AcceptAdmin {},
124    /// Update configuration values
125    UpdateConfig(ConfigUpdate),
126}
127
128/// Queries that can be performed on the countertrade contract.
129#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
130#[serde(rename_all = "snake_case")]
131pub enum QueryMsg {
132    /// Get the current config
133    ///
134    /// Returns [Config]
135    Config {},
136    /// Check the balance of an address for all markets.
137    ///
138    /// Returns [MarketBalance]
139    Balance {
140        /// Address of the token holder
141        address: RawAddr,
142    },
143    /// Check the status of a single market
144    ///
145    /// Returns [MarketStatus]
146    Status {},
147    /// Check if the given contract has any work to do
148    ///
149    /// Returns [HasWorkResp]
150    HasWork {},
151}
152
153/// Individual market response from [QueryMsg::Balance]
154#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
155#[serde(rename_all = "snake_case")]
156pub struct MarketBalance {
157    /// Market where a balance is held
158    pub market: MarketId,
159    /// Token for this market
160    pub token: crate::token::Token,
161    /// Shares of the pool held by this specific wallet.
162    pub shares: LpToken,
163    /// Collateral equivalent of these shares
164    pub collateral: Collateral,
165    /// Size of the entire pool, in LP tokens
166    pub pool_size: LpToken,
167}
168
169/// Either a native token or CW20 contract
170#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
171#[serde(rename_all = "snake_case")]
172pub enum Token {
173    /// Native coin and its denom
174    Native(String),
175    /// CW20 contract and its address
176    Cw20(Addr),
177}
178impl Token {
179    /// Ensure that the two versions of the token are compatible.
180    pub fn ensure_matches(&self, token: &crate::token::Token) -> anyhow::Result<()> {
181        match (self, token) {
182            (Token::Native(_), crate::token::Token::Cw20 { addr, .. }) => {
183                anyhow::bail!("Provided native funds, but market requires a CW20 (contract {addr})")
184            }
185            (
186                Token::Native(denom1),
187                crate::token::Token::Native {
188                    denom: denom2,
189                    decimal_places: _,
190                },
191            ) => {
192                if denom1 == denom2 {
193                    Ok(())
194                } else {
195                    Err(anyhow::anyhow!("Wrong denom provided. You sent {denom1}, but the contract expects {denom2}"))
196                }
197            }
198            (
199                Token::Cw20(addr1),
200                crate::token::Token::Cw20 {
201                    addr: addr2,
202                    decimal_places: _,
203                },
204            ) => {
205                if addr1.as_str() == addr2.as_str() {
206                    Ok(())
207                } else {
208                    Err(anyhow::anyhow!(
209                        "Wrong CW20 used. You used {addr1}, but the contract expects {addr2}"
210                    ))
211                }
212            }
213            (Token::Cw20(_), crate::token::Token::Native { denom, .. }) => {
214                anyhow::bail!(
215                    "Provided CW20 funds, but market requires native funds with denom {denom}"
216                )
217            }
218        }
219    }
220}
221
222impl Display for Token {
223    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
224        match self {
225            Token::Native(denom) => f.write_str(denom),
226            Token::Cw20(addr) => f.write_str(addr.as_str()),
227        }
228    }
229}
230
231/// Status of a single market
232#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
233#[serde(rename_all = "snake_case")]
234pub struct MarketStatus {
235    /// Which market
236    pub id: MarketId,
237    /// Collateral held inside the contract
238    ///
239    /// Does not include active collateral of a position
240    pub collateral: Collateral,
241    /// Number of outstanding shares
242    pub shares: LpToken,
243    /// Our open position, if we have exactly one
244    pub position: Option<PositionQueryResponse>,
245    /// Do we have too many open positions?
246    pub too_many_positions: bool,
247}
248
249/// Whether or not there is work available.
250#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
251#[serde(rename_all = "snake_case")]
252pub enum HasWorkResp {
253    /// No work is available
254    NoWork {},
255    /// There is work available to be done
256    Work {
257        /// A description of the work, for display and testing purposes.
258        desc: WorkDescription,
259    },
260}
261
262/// Work to be performed for a specific market.
263#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
264#[serde(rename_all = "snake_case")]
265pub enum WorkDescription {
266    /// Open a new position
267    OpenPosition {
268        /// Direction of the new position
269        direction: DirectionToBase,
270        /// Leverage
271        leverage: LeverageToBase,
272        /// Amount of deposit collateral
273        collateral: NonZero<Collateral>,
274        /// Take profit value
275        take_profit: TakeProfitTrader,
276        /// Stop loss price of new position
277        stop_loss_override: Option<PriceBaseInQuote>,
278    },
279    /// Close an unnecessary position
280    ClosePosition {
281        /// Position to be closed
282        pos_id: PositionId,
283    },
284    /// All collateral exhausted, reset shares to 0
285    ResetShares,
286    /// Deferred execution completed, we can continue our processing
287    ClearDeferredExec {
288        /// ID to be cleared
289        id: DeferredExecId,
290    },
291    /// Add collateral to a position, causing notional size to increase
292    UpdatePositionAddCollateralImpactSize {
293        /// ID of position to update
294        pos_id: PositionId,
295        /// Amount of funds to add to the position
296        amount: NonZero<Collateral>,
297    },
298    /// Remove collateral from a position, causing notional size to decrease
299    UpdatePositionRemoveCollateralImpactSize {
300        /// ID of position to update
301        pos_id: PositionId,
302        /// Amount of funds to remove from the position
303        amount: NonZero<Collateral>,
304        /// Crank fee to be paid
305        crank_fee: Collateral,
306    },
307}
308
309impl WorkDescription {
310    /// Is it closed position ?
311    pub fn is_close_position(&self) -> bool {
312        match self {
313            WorkDescription::OpenPosition { .. } => false,
314            WorkDescription::ClosePosition { .. } => true,
315            WorkDescription::ResetShares => false,
316            WorkDescription::ClearDeferredExec { .. } => false,
317            WorkDescription::UpdatePositionAddCollateralImpactSize { .. } => false,
318            WorkDescription::UpdatePositionRemoveCollateralImpactSize { .. } => false,
319        }
320    }
321
322    /// Is it collect closed position ?
323    pub fn is_collect_closed_position(&self) -> bool {
324        match self {
325            WorkDescription::OpenPosition { .. } => false,
326            WorkDescription::ClosePosition { .. } => false,
327            WorkDescription::ResetShares => false,
328            WorkDescription::ClearDeferredExec { .. } => false,
329            WorkDescription::UpdatePositionAddCollateralImpactSize { .. } => false,
330            WorkDescription::UpdatePositionRemoveCollateralImpactSize { .. } => false,
331        }
332    }
333
334    /// Is it update position ?
335    pub fn is_update_position(&self) -> bool {
336        match self {
337            WorkDescription::OpenPosition { .. } => false,
338            WorkDescription::ClosePosition { .. } => false,
339            WorkDescription::ResetShares => false,
340            WorkDescription::ClearDeferredExec { .. } => false,
341            WorkDescription::UpdatePositionAddCollateralImpactSize { .. } => true,
342            WorkDescription::UpdatePositionRemoveCollateralImpactSize { .. } => true,
343        }
344    }
345
346    /// Is it open position ?
347    pub fn is_open_position(&self) -> bool {
348        match self {
349            WorkDescription::OpenPosition { .. } => true,
350            WorkDescription::ClosePosition { .. } => false,
351            WorkDescription::ResetShares => false,
352            WorkDescription::ClearDeferredExec { .. } => false,
353            WorkDescription::UpdatePositionAddCollateralImpactSize { .. } => false,
354            WorkDescription::UpdatePositionRemoveCollateralImpactSize { .. } => false,
355        }
356    }
357
358    /// Is it open position ?
359    pub fn is_handle_deferred_exec(&self) -> bool {
360        match self {
361            WorkDescription::OpenPosition { .. } => false,
362            WorkDescription::ClosePosition { .. } => false,
363            WorkDescription::ResetShares => false,
364            WorkDescription::ClearDeferredExec { .. } => true,
365            WorkDescription::UpdatePositionAddCollateralImpactSize { .. } => false,
366            WorkDescription::UpdatePositionRemoveCollateralImpactSize { .. } => false,
367        }
368    }
369}
370
371impl std::fmt::Display for WorkDescription {
372    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
373        match self {
374            WorkDescription::OpenPosition {
375                direction,
376                leverage,
377                collateral,
378                ..
379            } => write!(
380                f,
381                "Open {direction:?} position with leverage {leverage} and collateral {collateral}"
382            ),
383            WorkDescription::ClosePosition { pos_id } => write!(f, "Close Position {pos_id}"),
384            WorkDescription::ResetShares => write!(f, "Reset Shares"),
385            WorkDescription::ClearDeferredExec { id, .. } => {
386                write!(f, "Handle Deferred Exec Id of {id}")
387            }
388            WorkDescription::UpdatePositionAddCollateralImpactSize { pos_id, amount } => {
389                write!(
390                    f,
391                    "Add {amount} Collateral to Position Id of {pos_id} impacting size"
392                )
393            }
394            WorkDescription::UpdatePositionRemoveCollateralImpactSize {
395                pos_id, amount, ..
396            } => write!(
397                f,
398                "Remove {amount} Collateral to Position Id of {pos_id} impacting size"
399            ),
400        }
401    }
402}
403
404/// Migration message, currently no fields needed
405#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
406#[serde(rename_all = "snake_case")]
407pub struct MigrateMsg {}