astro_token_converter/
contract.rs

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}