mars_core/
swapping.rs

1use crate::helpers::cw20_get_balance;
2use astroport::{
3    asset::{Asset as AstroportAsset, AssetInfo, PairInfo},
4    pair::ExecuteMsg as AstroportPairExecuteMsg,
5    querier::query_pair_info,
6};
7use cosmwasm_std::{
8    attr, to_binary, Addr, Coin, CosmosMsg, Decimal as StdDecimal, DepsMut, Empty, Env, Response,
9    StdError, StdResult, Uint128, WasmMsg,
10};
11use cw20::Cw20ExecuteMsg;
12
13/// Swap assets via Astroport
14pub fn execute_swap(
15    deps: DepsMut,
16    env: Env,
17    offer_asset_info: AssetInfo,
18    ask_asset_info: AssetInfo,
19    amount: Option<Uint128>,
20    astroport_factory_addr: Addr,
21    astroport_max_spread: Option<StdDecimal>,
22) -> StdResult<Response> {
23    // Having the same asset as offer and ask asset doesn't make any sense
24    if offer_asset_info == ask_asset_info {
25        return Err(StdError::generic_err(format!(
26            "Cannot swap an asset into itself. Both offer and ask assets were specified as {}",
27            offer_asset_info
28        )));
29    }
30
31    let (contract_offer_asset_balance, offer_asset_label) = match offer_asset_info.clone() {
32        AssetInfo::NativeToken { denom } => (
33            deps.querier
34                .query_balance(env.contract.address, denom.as_str())?
35                .amount,
36            denom,
37        ),
38        AssetInfo::Token { contract_addr } => {
39            let asset_label = String::from(contract_addr.as_str());
40            (
41                cw20_get_balance(
42                    &deps.querier,
43                    deps.api.addr_validate(&contract_addr.to_string())?,
44                    env.contract.address,
45                )?,
46                asset_label,
47            )
48        }
49    };
50
51    let ask_asset_label = match ask_asset_info.clone() {
52        AssetInfo::NativeToken { denom } => denom,
53        AssetInfo::Token { contract_addr } => contract_addr.to_string(),
54    };
55
56    if contract_offer_asset_balance.is_zero() {
57        return Err(StdError::generic_err(format!(
58            "Contract has no balance for the asset {}",
59            offer_asset_label
60        )));
61    }
62
63    let amount_to_swap = match amount {
64        Some(amount) if amount > contract_offer_asset_balance => {
65            return Err(StdError::generic_err(format!(
66                "The amount requested for swap exceeds contract balance for the asset {}",
67                offer_asset_label
68            )));
69        }
70        Some(amount) => amount,
71        None => contract_offer_asset_balance,
72    };
73
74    let pair_info: PairInfo = query_pair_info(
75        &deps.querier,
76        astroport_factory_addr,
77        &[offer_asset_info.clone(), ask_asset_info],
78    )?;
79
80    let offer_asset = AstroportAsset {
81        info: offer_asset_info,
82        amount: amount_to_swap,
83    };
84    let send_msg = asset_into_swap_msg(
85        deps.api
86            .addr_validate(&pair_info.contract_addr.to_string())?,
87        offer_asset,
88        astroport_max_spread,
89    )?;
90
91    let response = Response::new().add_message(send_msg).add_attributes(vec![
92        attr("action", "swap"),
93        attr("offer_asset", offer_asset_label),
94        attr("ask_asset", ask_asset_label),
95        attr("offer_asset_amount", amount_to_swap),
96    ]);
97
98    Ok(response)
99}
100
101/// Construct Astroport message in order to swap assets
102fn asset_into_swap_msg(
103    pair_contract: Addr,
104    offer_asset: AstroportAsset,
105    max_spread: Option<StdDecimal>,
106) -> StdResult<CosmosMsg<Empty>> {
107    let message = match offer_asset.info.clone() {
108        AssetInfo::NativeToken { denom } => CosmosMsg::Wasm(WasmMsg::Execute {
109            contract_addr: pair_contract.to_string(),
110            msg: to_binary(&AstroportPairExecuteMsg::Swap {
111                offer_asset: offer_asset.clone(),
112                belief_price: None,
113                max_spread,
114                to: None,
115            })?,
116            funds: vec![Coin {
117                denom,
118                amount: offer_asset.amount,
119            }],
120        }),
121        AssetInfo::Token { contract_addr } => CosmosMsg::Wasm(WasmMsg::Execute {
122            contract_addr: contract_addr.to_string(),
123            msg: to_binary(&Cw20ExecuteMsg::Send {
124                contract: pair_contract.to_string(),
125                amount: offer_asset.amount,
126                msg: to_binary(&AstroportPairExecuteMsg::Swap {
127                    offer_asset,
128                    belief_price: None,
129                    max_spread,
130                    to: None,
131                })?,
132            })?,
133            funds: vec![],
134        }),
135    };
136    Ok(message)
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::testing::{
143        assert_generic_error_message, mock_dependencies, mock_env, MockEnvParams,
144    };
145    use astroport::factory::PairType;
146    use cosmwasm_std::testing::MOCK_CONTRACT_ADDR;
147    use cosmwasm_std::SubMsg;
148
149    #[test]
150    fn test_cannot_swap_same_assets() {
151        let mut deps = mock_dependencies(&[]);
152        let env = mock_env(MockEnvParams::default());
153
154        let assets = vec![
155            (
156                "somecoin_addr",
157                AssetInfo::Token {
158                    contract_addr: Addr::unchecked("somecoin_addr"),
159                },
160            ),
161            (
162                "uluna",
163                AssetInfo::NativeToken {
164                    denom: "uluna".to_string(),
165                },
166            ),
167        ];
168        for (asset_name, asset_info) in assets {
169            let response = execute_swap(
170                deps.as_mut(),
171                env.clone(),
172                asset_info.clone(),
173                asset_info,
174                None,
175                Addr::unchecked("astroport_factory"),
176                None,
177            );
178            assert_generic_error_message(
179                response,
180                &format!("Cannot swap an asset into itself. Both offer and ask assets were specified as {}", asset_name),
181            );
182        }
183    }
184
185    #[test]
186    fn test_cannot_swap_asset_with_zero_balance() {
187        let mut deps = mock_dependencies(&[]);
188        let env = mock_env(MockEnvParams::default());
189
190        let cw20_contract_address = Addr::unchecked("cw20_zero");
191        deps.querier.set_cw20_balances(
192            cw20_contract_address.clone(),
193            &[(Addr::unchecked(MOCK_CONTRACT_ADDR), Uint128::zero())],
194        );
195
196        let offer_asset_info = AssetInfo::Token {
197            contract_addr: cw20_contract_address,
198        };
199        let ask_asset_info = AssetInfo::NativeToken {
200            denom: "uusd".to_string(),
201        };
202
203        let response = execute_swap(
204            deps.as_mut(),
205            env,
206            offer_asset_info,
207            ask_asset_info,
208            None,
209            Addr::unchecked("astroport_factory"),
210            None,
211        );
212        assert_generic_error_message(response, "Contract has no balance for the asset cw20_zero")
213    }
214
215    #[test]
216    fn test_cannot_swap_more_than_contract_balance() {
217        let mut deps = mock_dependencies(&[Coin {
218            denom: "somecoin".to_string(),
219            amount: Uint128::new(1_000_000),
220        }]);
221        let env = mock_env(MockEnvParams::default());
222
223        let offer_asset_info = AssetInfo::NativeToken {
224            denom: "somecoin".to_string(),
225        };
226        let ask_asset_info = AssetInfo::Token {
227            contract_addr: Addr::unchecked("cw20_token"),
228        };
229
230        let response = execute_swap(
231            deps.as_mut(),
232            env,
233            offer_asset_info,
234            ask_asset_info,
235            Some(Uint128::new(1_000_001)),
236            Addr::unchecked("astroport_factory"),
237            None,
238        );
239        assert_generic_error_message(
240            response,
241            "The amount requested for swap exceeds contract balance for the asset somecoin",
242        )
243    }
244
245    #[test]
246    fn test_swap_contract_token_partial_balance() {
247        let mut deps = mock_dependencies(&[]);
248        let env = mock_env(MockEnvParams::default());
249
250        let cw20_contract_address = Addr::unchecked("cw20");
251        let contract_asset_balance = Uint128::new(1_000_000);
252        deps.querier.set_cw20_balances(
253            cw20_contract_address.clone(),
254            &[(Addr::unchecked(MOCK_CONTRACT_ADDR), contract_asset_balance)],
255        );
256
257        let offer_asset_info = AssetInfo::Token {
258            contract_addr: cw20_contract_address.clone(),
259        };
260        let ask_asset_info = AssetInfo::Token {
261            contract_addr: Addr::unchecked("mars"),
262        };
263
264        deps.querier.set_astroport_pair(PairInfo {
265            asset_infos: [offer_asset_info.clone(), ask_asset_info.clone()],
266            contract_addr: Addr::unchecked("pair_cw20_mars"),
267            liquidity_token: Addr::unchecked("lp_cw20_mars"),
268            pair_type: PairType::Xyk {},
269        });
270
271        let res = execute_swap(
272            deps.as_mut(),
273            env,
274            offer_asset_info,
275            ask_asset_info,
276            Some(Uint128::new(999)),
277            Addr::unchecked("astroport_factory"),
278            None,
279        )
280        .unwrap();
281
282        assert_eq!(
283            res.messages,
284            vec![SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute {
285                contract_addr: cw20_contract_address.to_string(),
286                msg: to_binary(&Cw20ExecuteMsg::Send {
287                    contract: String::from("pair_cw20_mars"),
288                    amount: Uint128::new(999),
289                    msg: to_binary(&AstroportPairExecuteMsg::Swap {
290                        offer_asset: AstroportAsset {
291                            info: AssetInfo::Token {
292                                contract_addr: cw20_contract_address.clone(),
293                            },
294                            amount: Uint128::new(999),
295                        },
296                        belief_price: None,
297                        max_spread: None,
298                        to: None,
299                    })
300                    .unwrap(),
301                })
302                .unwrap(),
303                funds: vec![],
304            }))]
305        );
306
307        assert_eq!(
308            res.attributes,
309            vec![
310                attr("action", "swap"),
311                attr("offer_asset", cw20_contract_address.as_str()),
312                attr("ask_asset", "mars"),
313                attr("offer_asset_amount", "999"),
314            ]
315        );
316    }
317
318    #[test]
319    fn test_swap_native_token_total_balance() {
320        let contract_asset_balance = Uint128::new(1_234_567);
321        let mut deps = mock_dependencies(&[Coin {
322            denom: "uusd".to_string(),
323            amount: contract_asset_balance,
324        }]);
325        let env = mock_env(MockEnvParams::default());
326
327        let offer_asset_info = AssetInfo::NativeToken {
328            denom: "uusd".to_string(),
329        };
330        let ask_asset_info = AssetInfo::Token {
331            contract_addr: Addr::unchecked("mars"),
332        };
333
334        deps.querier.set_astroport_pair(PairInfo {
335            asset_infos: [offer_asset_info.clone(), ask_asset_info.clone()],
336            contract_addr: Addr::unchecked("pair_uusd_mars"),
337            liquidity_token: Addr::unchecked("lp_uusd_mars"),
338            pair_type: PairType::Xyk {},
339        });
340
341        let res = execute_swap(
342            deps.as_mut(),
343            env,
344            offer_asset_info,
345            ask_asset_info,
346            None,
347            Addr::unchecked("astroport_factory"),
348            Some(StdDecimal::from_ratio(1u128, 100u128)),
349        )
350        .unwrap();
351
352        assert_eq!(
353            res.messages,
354            vec![SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute {
355                contract_addr: String::from("pair_uusd_mars"),
356                msg: to_binary(&AstroportPairExecuteMsg::Swap {
357                    offer_asset: AstroportAsset {
358                        info: AssetInfo::NativeToken {
359                            denom: "uusd".to_string(),
360                        },
361                        amount: contract_asset_balance,
362                    },
363                    belief_price: None,
364                    max_spread: Some(StdDecimal::from_ratio(1u128, 100u128)),
365                    to: None,
366                })
367                .unwrap(),
368                funds: vec![Coin {
369                    denom: "uusd".to_string(),
370                    amount: contract_asset_balance,
371                }],
372            }))]
373        );
374
375        assert_eq!(
376            res.attributes,
377            vec![
378                attr("action", "swap"),
379                attr("offer_asset", "uusd"),
380                attr("ask_asset", "mars"),
381                attr("offer_asset_amount", contract_asset_balance.to_string()),
382            ]
383        );
384    }
385}