1#[cfg(not(feature = "library"))]
2use cosmwasm_std::entry_point;
3use cosmwasm_std::{
4 attr, coin, coins, ensure, from_json, to_json_binary, wasm_execute, Api, BankMsg, Binary,
5 CosmosMsg, CustomMsg, Deps, DepsMut, Env, IbcMsg, IbcTimeout, MessageInfo, QuerierWrapper,
6 Response, StdError, StdResult,
7};
8use cw2::set_contract_version;
9use cw20::{Cw20ExecuteMsg, Cw20QueryMsg, Cw20ReceiveMsg};
10use cw_utils::{must_pay, nonpayable};
11
12use astroport::asset::{addr_opt_validate, validate_native_denom, AssetInfo};
13use astroport::astro_converter::{
14 Config, Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, DEFAULT_TIMEOUT, TIMEOUT_LIMITS,
15};
16
17use crate::error::ContractError;
18use crate::state::CONFIG;
19
20const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");
21const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
22
23#[cfg_attr(not(feature = "library"), entry_point)]
24pub fn instantiate(
25 deps: DepsMut,
26 env: Env,
27 info: MessageInfo,
28 msg: InstantiateMsg,
29) -> Result<Response, ContractError> {
30 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
31 init(deps, env, info, msg)
32}
33
34pub fn init(
35 deps: DepsMut,
36 _env: Env,
37 _info: MessageInfo,
38 msg: InstantiateMsg,
39) -> Result<Response, ContractError> {
40 validate_native_denom(&msg.new_astro_denom)?;
41 msg.old_astro_asset_info.check(deps.api)?;
42
43 ensure!(
44 msg.old_astro_asset_info != AssetInfo::native(&msg.new_astro_denom),
45 StdError::generic_err("Cannot convert to the same asset")
46 );
47
48 if msg.old_astro_asset_info.is_native_token() {
49 ensure!(
50 msg.old_astro_asset_info.is_ibc(),
51 StdError::generic_err("If old ASTRO is native it must be IBC denom")
52 );
53 }
54
55 if matches!(msg.old_astro_asset_info, AssetInfo::Token { .. }) {
56 ensure!(
57 msg.outpost_burn_params.is_none(),
58 StdError::generic_err("Burn params must be unset on the old Hub (Terra)")
59 );
60 }
61
62 if msg.old_astro_asset_info.is_ibc() {
63 ensure!(
64 msg.outpost_burn_params.is_some(),
65 StdError::generic_err("Burn params must be specified on outpost")
66 );
67 }
68
69 let attrs = [
70 attr("contract_name", CONTRACT_NAME),
71 attr("astro_old_denom", msg.old_astro_asset_info.to_string()),
72 attr("astro_new_denom", &msg.new_astro_denom),
73 ];
74
75 CONFIG.save(
76 deps.storage,
77 &Config {
78 old_astro_asset_info: msg.old_astro_asset_info,
79 new_astro_denom: msg.new_astro_denom,
80 outpost_burn_params: msg.outpost_burn_params,
81 },
82 )?;
83
84 Ok(Response::default().add_attributes(attrs))
85}
86
87#[cfg_attr(not(feature = "library"), entry_point)]
88pub fn execute(
89 deps: DepsMut,
90 env: Env,
91 info: MessageInfo,
92 msg: ExecuteMsg,
93) -> Result<Response, ContractError> {
94 let config = CONFIG.load(deps.storage)?;
95
96 match msg {
97 ExecuteMsg::Receive(cw20_msg) => cw20_receive(deps.api, config, info, cw20_msg),
98 ExecuteMsg::Convert { receiver } => convert(deps.api, config, info, receiver),
99 ExecuteMsg::TransferForBurning { timeout } => {
100 ibc_transfer_for_burning(deps.querier, env, info, config, timeout)
101 }
102 ExecuteMsg::Burn {} => burn(deps.querier, env, info, config),
103 }
104}
105
106pub fn cw20_receive<M: CustomMsg>(
107 api: &dyn Api,
108 config: Config,
109 info: MessageInfo,
110 cw20_msg: Cw20ReceiveMsg,
111) -> Result<Response<M>, ContractError> {
112 match config.old_astro_asset_info {
113 AssetInfo::Token { contract_addr } => {
114 if info.sender == contract_addr {
115 let receiver = from_json::<Cw20HookMsg>(&cw20_msg.msg)?.receiver;
116 addr_opt_validate(api, &receiver)?;
117
118 let receiver = receiver.unwrap_or(cw20_msg.sender);
119 let bank_msg = BankMsg::Send {
120 to_address: receiver.clone(),
121 amount: coins(cw20_msg.amount.u128(), config.new_astro_denom),
122 };
123
124 Ok(Response::new().add_message(bank_msg).add_attributes([
125 attr("action", "convert"),
126 attr("receiver", receiver),
127 attr("type", "cw20:astro"),
128 attr("amount", cw20_msg.amount),
129 ]))
130 } else {
131 Err(ContractError::UnsupportedCw20Token(info.sender))
132 }
133 }
134 AssetInfo::NativeToken { .. } => Err(ContractError::InvalidEndpoint {}),
135 }
136}
137
138pub fn convert<M: CustomMsg>(
139 api: &dyn Api,
140 config: Config,
141 info: MessageInfo,
142 receiver: Option<String>,
143) -> Result<Response<M>, ContractError> {
144 match config.old_astro_asset_info {
145 AssetInfo::NativeToken { denom } => {
146 let amount = must_pay(&info, &denom)?;
147 addr_opt_validate(api, &receiver)?;
148
149 let receiver = receiver.unwrap_or_else(|| info.sender.to_string());
150 let bank_msg = BankMsg::Send {
151 to_address: receiver.clone(),
152 amount: coins(amount.u128(), config.new_astro_denom),
153 };
154
155 Ok(Response::new().add_message(bank_msg).add_attributes([
156 attr("action", "convert"),
157 attr("receiver", receiver),
158 attr("type", "ibc:astro"),
159 attr("amount", amount),
160 ]))
161 }
162 AssetInfo::Token { .. } => Err(ContractError::InvalidEndpoint {}),
163 }
164}
165
166pub fn ibc_transfer_for_burning(
167 querier: QuerierWrapper,
168 env: Env,
169 info: MessageInfo,
170 config: Config,
171 timeout: Option<u64>,
172) -> Result<Response, ContractError> {
173 nonpayable(&info)?;
174 match config.old_astro_asset_info {
175 AssetInfo::NativeToken { denom } => {
176 let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT);
177 ensure!(
178 TIMEOUT_LIMITS.contains(&timeout),
179 ContractError::InvalidTimeout {}
180 );
181
182 let amount = querier.query_balance(&env.contract.address, &denom)?.amount;
183
184 ensure!(
185 !amount.is_zero(),
186 StdError::generic_err("No tokens to transfer")
187 );
188
189 let burn_params = config.outpost_burn_params.expect("No outpost burn params");
190
191 let ibc_transfer_msg = IbcMsg::Transfer {
192 channel_id: burn_params.old_astro_transfer_channel,
193 to_address: burn_params.terra_burn_addr,
194 amount: coin(amount.u128(), denom),
195 timeout: IbcTimeout::with_timestamp(env.block.time.plus_seconds(timeout)),
196 };
197
198 Ok(Response::new()
199 .add_message(CosmosMsg::Ibc(ibc_transfer_msg))
200 .add_attributes([
201 attr("action", "ibc_transfer_for_burning"),
202 attr("type", "ibc:astro"),
203 attr("amount", amount),
204 ]))
205 }
206 AssetInfo::Token { .. } => Err(ContractError::IbcTransferError {}),
207 }
208}
209
210pub fn burn<M: CustomMsg>(
211 querier: QuerierWrapper,
212 env: Env,
213 info: MessageInfo,
214 config: Config,
215) -> Result<Response<M>, ContractError> {
216 nonpayable(&info)?;
217 match config.old_astro_asset_info {
218 AssetInfo::Token { contract_addr } => {
219 let amount = querier
220 .query_wasm_smart::<cw20::BalanceResponse>(
221 &contract_addr,
222 &Cw20QueryMsg::Balance {
223 address: env.contract.address.to_string(),
224 },
225 )?
226 .balance;
227
228 ensure!(
229 !amount.is_zero(),
230 StdError::generic_err("No tokens to burn")
231 );
232
233 let burn_msg = wasm_execute(contract_addr, &Cw20ExecuteMsg::Burn { amount }, vec![])?;
234
235 Ok(Response::new().add_message(burn_msg).add_attributes([
236 attr("action", "burn"),
237 attr("type", "cw20:astro"),
238 attr("amount", amount),
239 ]))
240 }
241 AssetInfo::NativeToken { .. } => Err(ContractError::BurnError {}),
242 }
243}
244
245#[cfg_attr(not(feature = "library"), entry_point)]
246pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
247 match msg {
248 QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?),
249 }
250}
251
252#[cfg(test)]
253mod testing {
254 use cosmwasm_std::testing::{
255 mock_dependencies, mock_dependencies_with_balance, mock_env, mock_info, MockApi,
256 MockQuerier,
257 };
258 use cosmwasm_std::{
259 from_json, to_json_binary, Addr, ContractResult, Empty, SubMsg, SystemResult, Uint128,
260 WasmMsg, WasmQuery,
261 };
262 use cw_utils::PaymentError::{MissingDenom, NoFunds};
263
264 use astroport::astro_converter::OutpostBurnParams;
265
266 use super::*;
267
268 #[test]
269 fn test_instantiate() {
270 let mut deps = mock_dependencies();
271 let mut msg = InstantiateMsg {
272 old_astro_asset_info: AssetInfo::native("uastro"),
273 new_astro_denom: "uastro".to_string(),
274 outpost_burn_params: None,
275 };
276 let info = mock_info("creator", &[]);
277 let err = instantiate(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap_err();
278
279 assert_eq!(
280 err.to_string(),
281 "Generic error: Cannot convert to the same asset"
282 );
283
284 msg.old_astro_asset_info = AssetInfo::native("ibc/old_astro");
285
286 let err = instantiate(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap_err();
287 assert_eq!(
288 err.to_string(),
289 "Generic error: Burn params must be specified on outpost"
290 );
291
292 msg.outpost_burn_params = Some(OutpostBurnParams {
293 terra_burn_addr: "terra1xxx".to_string(),
294 old_astro_transfer_channel: "channel-1".to_string(),
295 });
296
297 instantiate(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap();
298
299 let config_data = query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap();
300 let config = from_json::<Config>(&config_data).unwrap();
301
302 assert_eq!(
303 config,
304 Config {
305 old_astro_asset_info: AssetInfo::native("ibc/old_astro"),
306 new_astro_denom: "uastro".to_string(),
307 outpost_burn_params: Some(OutpostBurnParams {
308 terra_burn_addr: "terra1xxx".to_string(),
309 old_astro_transfer_channel: "channel-1".to_string(),
310 }),
311 }
312 );
313
314 msg.old_astro_asset_info = AssetInfo::native("untrn");
315 let err = instantiate(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap_err();
316 assert_eq!(
317 err.to_string(),
318 "Generic error: If old ASTRO is native it must be IBC denom"
319 );
320
321 msg.old_astro_asset_info = AssetInfo::cw20_unchecked("terra1xxx_old_astro");
322 let err = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap_err();
323 assert_eq!(
324 err.to_string(),
325 "Generic error: Burn params must be unset on the old Hub (Terra)"
326 );
327 }
328
329 #[test]
330 fn test_cw20_convert() {
331 let mut config = Config {
332 old_astro_asset_info: AssetInfo::native("uastro"),
333 new_astro_denom: "ibc/astro".to_string(),
334 outpost_burn_params: None,
335 };
336 let mock_api = MockApi::default();
337
338 let mut cw20_msg = Cw20ReceiveMsg {
339 sender: "sender".to_string(),
340 amount: 100u128.into(),
341 msg: to_json_binary(&Empty {}).unwrap(),
342 };
343 let err = cw20_receive::<Empty>(
344 &mock_api,
345 config.clone(),
346 mock_info("random_cw20", &[]),
347 cw20_msg.clone(),
348 )
349 .unwrap_err();
350 assert_eq!(err, ContractError::InvalidEndpoint {});
351
352 config.old_astro_asset_info = AssetInfo::cw20_unchecked("terra1xxx");
353
354 let err = cw20_receive::<Empty>(
355 &mock_api,
356 config.clone(),
357 mock_info("random_cw20", &[]),
358 cw20_msg.clone(),
359 )
360 .unwrap_err();
361 assert_eq!(
362 err,
363 ContractError::UnsupportedCw20Token(Addr::unchecked("random_cw20"))
364 );
365
366 let res = cw20_receive::<Empty>(
367 &mock_api,
368 config.clone(),
369 mock_info("terra1xxx", &[]),
370 cw20_msg.clone(),
371 )
372 .unwrap();
373
374 assert_eq!(
375 res.messages,
376 [SubMsg::new(CosmosMsg::Bank(BankMsg::Send {
377 to_address: cw20_msg.sender.clone(),
378 amount: coins(cw20_msg.amount.u128(), config.new_astro_denom.clone())
379 }))]
380 );
381
382 cw20_msg.msg = to_json_binary(&Cw20HookMsg {
383 receiver: Some("receiver".to_string()),
384 })
385 .unwrap();
386 let res = cw20_receive::<Empty>(
387 &mock_api,
388 config.clone(),
389 mock_info("terra1xxx", &[]),
390 cw20_msg.clone(),
391 )
392 .unwrap();
393
394 assert_eq!(
395 res.messages,
396 [SubMsg::new(CosmosMsg::Bank(BankMsg::Send {
397 to_address: "receiver".to_string(),
398 amount: coins(cw20_msg.amount.u128(), config.new_astro_denom)
399 }))]
400 );
401 }
402
403 #[test]
404 fn test_native_convert() {
405 let mut config = Config {
406 old_astro_asset_info: AssetInfo::cw20_unchecked("terra1xxx"),
407 new_astro_denom: "ibc/astro".to_string(),
408 outpost_burn_params: None,
409 };
410 let mock_api = MockApi::default();
411
412 let info = mock_info("sender", &[]);
413 let err = convert::<Empty>(&mock_api, config.clone(), info, None).unwrap_err();
414 assert_eq!(err, ContractError::InvalidEndpoint {});
415
416 config.old_astro_asset_info = AssetInfo::native("ibc/old_astro");
417
418 let info = mock_info("sender", &[]);
419 let err = convert::<Empty>(&mock_api, config.clone(), info, None).unwrap_err();
420 assert_eq!(err, ContractError::PaymentError(NoFunds {}));
421
422 let info = mock_info("sender", &coins(100, "random_coin"));
423 let err = convert::<Empty>(&mock_api, config.clone(), info, None).unwrap_err();
424 assert_eq!(
425 err,
426 ContractError::PaymentError(MissingDenom("ibc/old_astro".to_string()))
427 );
428
429 let info = mock_info("sender", &coins(100, "ibc/old_astro"));
430 let res = convert::<Empty>(&mock_api, config.clone(), info.clone(), None).unwrap();
431 assert_eq!(
432 res.messages,
433 [SubMsg::new(CosmosMsg::Bank(BankMsg::Send {
434 to_address: info.sender.to_string(),
435 amount: coins(100, config.new_astro_denom.clone())
436 }))]
437 );
438
439 let res = convert::<Empty>(
440 &mock_api,
441 config.clone(),
442 info.clone(),
443 Some("receiver".to_string()),
444 )
445 .unwrap();
446 assert_eq!(
447 res.messages,
448 [SubMsg::new(CosmosMsg::Bank(BankMsg::Send {
449 to_address: "receiver".to_string(),
450 amount: coins(100, config.new_astro_denom)
451 }))]
452 );
453 }
454
455 #[test]
456 fn test_ibc_transfer() {
457 let deps = mock_dependencies();
458 let outpost_params = OutpostBurnParams {
459 terra_burn_addr: "terra1xxx".to_string(),
460 old_astro_transfer_channel: "channel-1".to_string(),
461 };
462 let mut config = Config {
463 old_astro_asset_info: AssetInfo::cw20_unchecked("terra1xxx"),
464 new_astro_denom: "ibc/astro".to_string(),
465 outpost_burn_params: Some(outpost_params.clone()),
466 };
467
468 let info = mock_info("permissionless", &[]);
469 let err = ibc_transfer_for_burning(
470 deps.as_ref().querier,
471 mock_env(),
472 info.clone(),
473 config.clone(),
474 None,
475 )
476 .unwrap_err();
477 assert_eq!(err, ContractError::IbcTransferError {});
478
479 config.old_astro_asset_info = AssetInfo::native("ibc/old_astro");
480
481 let err = ibc_transfer_for_burning(
482 deps.as_ref().querier,
483 mock_env(),
484 info.clone(),
485 config.clone(),
486 None,
487 )
488 .unwrap_err();
489 assert_eq!(err.to_string(), "Generic error: No tokens to transfer");
490
491 let deps = mock_dependencies_with_balance(&coins(100, "ibc/old_astro"));
492 let env = mock_env();
493 let res = ibc_transfer_for_burning(
494 deps.as_ref().querier,
495 env.clone(),
496 info.clone(),
497 config.clone(),
498 None,
499 )
500 .unwrap();
501
502 assert_eq!(
503 res.messages,
504 [SubMsg::new(CosmosMsg::Ibc(IbcMsg::Transfer {
505 channel_id: outpost_params.old_astro_transfer_channel,
506 to_address: outpost_params.terra_burn_addr,
507 amount: coin(100, "ibc/old_astro"),
508 timeout: env.block.time.plus_seconds(DEFAULT_TIMEOUT).into(),
509 }))]
510 );
511
512 let err = ibc_transfer_for_burning(
513 deps.as_ref().querier,
514 env.clone(),
515 info,
516 config.clone(),
517 Some(1),
518 )
519 .unwrap_err();
520 assert_eq!(err, ContractError::InvalidTimeout {})
521 }
522
523 fn querier_wrapper_with_cw20_balances(
524 mock_querier: &mut MockQuerier,
525 balances: Vec<(Addr, Uint128)>,
526 ) -> QuerierWrapper {
527 let wasm_handler = move |query: &WasmQuery| match query {
528 WasmQuery::Smart { contract_addr, msg } if contract_addr == "terra1xxx" => {
529 let contract_result: ContractResult<_> = match from_json(msg) {
530 Ok(Cw20QueryMsg::Balance { address }) => {
531 let balance = balances
532 .iter()
533 .find_map(|(addr, balance)| {
534 if addr == &address {
535 Some(balance)
536 } else {
537 None
538 }
539 })
540 .cloned()
541 .unwrap_or_else(Uint128::zero);
542 to_json_binary(&cw20::BalanceResponse { balance }).into()
543 }
544 _ => unimplemented!(),
545 };
546 SystemResult::Ok(contract_result)
547 }
548 _ => unimplemented!(),
549 };
550 mock_querier.update_wasm(wasm_handler);
551
552 QuerierWrapper::new(&*mock_querier)
553 }
554
555 #[test]
556 fn test_burn() {
557 let deps = mock_dependencies();
558 let mut config = Config {
559 old_astro_asset_info: AssetInfo::native("ibc/old_astro"),
560 new_astro_denom: "ibc/astro".to_string(),
561 outpost_burn_params: None,
562 };
563
564 let info = mock_info("permissionless", &[]);
565 let err = burn::<Empty>(
566 deps.as_ref().querier,
567 mock_env(),
568 info.clone(),
569 config.clone(),
570 )
571 .unwrap_err();
572 assert_eq!(err, ContractError::BurnError {});
573
574 config.old_astro_asset_info = AssetInfo::cw20_unchecked("terra1xxx");
575
576 let env = mock_env();
577 let mut mock_querier: MockQuerier = MockQuerier::new(&[]);
578 let querier_wrapper = querier_wrapper_with_cw20_balances(
579 &mut mock_querier,
580 vec![(env.contract.address.clone(), 0u128.into())],
581 );
582 let err =
583 burn::<Empty>(querier_wrapper, mock_env(), info.clone(), config.clone()).unwrap_err();
584 assert_eq!(err.to_string(), "Generic error: No tokens to burn");
585
586 let env = mock_env();
587 let mut mock_querier: MockQuerier = MockQuerier::new(&[]);
588 let querier_wrapper = querier_wrapper_with_cw20_balances(
589 &mut mock_querier,
590 vec![(env.contract.address.clone(), 100u128.into())],
591 );
592 let res = burn::<Empty>(querier_wrapper, env.clone(), info, config.clone()).unwrap();
593
594 assert_eq!(
595 res.messages,
596 [SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute {
597 contract_addr: config.old_astro_asset_info.to_string(),
598 msg: to_json_binary(&Cw20ExecuteMsg::Burn {
599 amount: 100u128.into()
600 })
601 .unwrap(),
602 funds: vec![],
603 }))]
604 );
605 }
606}