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
13pub 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 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
101fn 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}