cw20_ics20/
ibc.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use cosmwasm_schema::cw_serde;
5#[cfg(not(feature = "library"))]
6use cosmwasm_std::entry_point;
7use cosmwasm_std::{
8    attr, from_json, to_json_binary, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env,
9    Ibc3ChannelOpenResponse, IbcBasicResponse, IbcChannel, IbcChannelCloseMsg,
10    IbcChannelConnectMsg, IbcChannelOpenMsg, IbcEndpoint, IbcOrder, IbcPacket, IbcPacketAckMsg,
11    IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, Reply, Response, SubMsg,
12    SubMsgResult, Uint128, WasmMsg,
13};
14
15use crate::amount::Amount;
16use crate::error::{ContractError, Never};
17use crate::state::{
18    reduce_channel_balance, undo_reduce_channel_balance, ChannelInfo, ReplyArgs, ALLOW_LIST,
19    CHANNEL_INFO, CONFIG, REPLY_ARGS,
20};
21use cw20::Cw20ExecuteMsg;
22
23pub const ICS20_VERSION: &str = "ics20-1";
24pub const ICS20_ORDERING: IbcOrder = IbcOrder::Unordered;
25
26/// The format for sending an ics20 packet.
27/// Proto defined here: https://github.com/cosmos/cosmos-sdk/blob/v0.42.0/proto/ibc/applications/transfer/v1/transfer.proto#L11-L20
28/// This is compatible with the JSON serialization
29#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)]
30pub struct Ics20Packet {
31    /// amount of tokens to transfer is encoded as a string, but limited to u64 max
32    pub amount: Uint128,
33    /// the token denomination to be transferred
34    pub denom: String,
35    /// the recipient address on the destination chain
36    pub receiver: String,
37    /// the sender address
38    pub sender: String,
39    /// optional memo for the IBC transfer
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub memo: Option<String>,
42}
43
44impl Ics20Packet {
45    pub fn new<T: Into<String>>(amount: Uint128, denom: T, sender: &str, receiver: &str) -> Self {
46        Ics20Packet {
47            denom: denom.into(),
48            amount,
49            sender: sender.to_string(),
50            receiver: receiver.to_string(),
51            memo: None,
52        }
53    }
54
55    pub fn with_memo(self, memo: Option<String>) -> Self {
56        Ics20Packet { memo, ..self }
57    }
58
59    pub fn validate(&self) -> Result<(), ContractError> {
60        if self.amount.u128() > (u64::MAX as u128) {
61            Err(ContractError::AmountOverflow {})
62        } else {
63            Ok(())
64        }
65    }
66}
67
68/// This is a generic ICS acknowledgement format.
69/// Proto defined here: https://github.com/cosmos/cosmos-sdk/blob/v0.42.0/proto/ibc/core/channel/v1/channel.proto#L141-L147
70/// This is compatible with the JSON serialization
71#[cw_serde]
72pub enum Ics20Ack {
73    Result(Binary),
74    Error(String),
75}
76
77// create a serialized success message
78fn ack_success() -> Binary {
79    let res = Ics20Ack::Result(b"1".into());
80    to_json_binary(&res).unwrap()
81}
82
83// create a serialized error message
84fn ack_fail(err: String) -> Binary {
85    let res = Ics20Ack::Error(err);
86    to_json_binary(&res).unwrap()
87}
88
89const RECEIVE_ID: u64 = 1337;
90const ACK_FAILURE_ID: u64 = 0xfa17;
91
92#[cfg_attr(not(feature = "library"), entry_point)]
93pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> Result<Response, ContractError> {
94    match reply.id {
95        RECEIVE_ID => match reply.result {
96            SubMsgResult::Ok(_) => Ok(Response::new()),
97            SubMsgResult::Err(err) => {
98                // Important design note:  with ibcv2 and wasmd 0.22 we can implement this all much easier.
99                // No reply needed... the receive function and submessage should return error on failure and all
100                // state gets reverted with a proper app-level message auto-generated
101
102                // Since we need compatibility with Juno (Jan 2022), we need to ensure that optimisitic
103                // state updates in ibc_packet_receive get reverted in the (unlikely) chance of an
104                // error while sending the token
105
106                // However, this requires passing some state between the ibc_packet_receive function and
107                // the reply handler. We do this with a singleton, with is "okay" for IBC as there is no
108                // reentrancy on these functions (cannot be called by another contract). This pattern
109                // should not be used for ExecuteMsg handlers
110                let reply_args = REPLY_ARGS.load(deps.storage)?;
111                undo_reduce_channel_balance(
112                    deps.storage,
113                    &reply_args.channel,
114                    &reply_args.denom,
115                    reply_args.amount,
116                )?;
117
118                Ok(Response::new().set_data(ack_fail(err)))
119            }
120        },
121        ACK_FAILURE_ID => match reply.result {
122            SubMsgResult::Ok(_) => Ok(Response::new()),
123            SubMsgResult::Err(err) => Ok(Response::new().set_data(ack_fail(err))),
124        },
125        _ => Err(ContractError::UnknownReplyId { id: reply.id }),
126    }
127}
128
129#[cfg_attr(not(feature = "library"), entry_point)]
130/// enforces ordering and versioning constraints
131pub fn ibc_channel_open(
132    _deps: DepsMut,
133    _env: Env,
134    msg: IbcChannelOpenMsg,
135) -> Result<Option<Ibc3ChannelOpenResponse>, ContractError> {
136    enforce_order_and_version(msg.channel(), msg.counterparty_version())?;
137    Ok(None)
138}
139
140#[cfg_attr(not(feature = "library"), entry_point)]
141/// record the channel in CHANNEL_INFO
142pub fn ibc_channel_connect(
143    deps: DepsMut,
144    _env: Env,
145    msg: IbcChannelConnectMsg,
146) -> Result<IbcBasicResponse, ContractError> {
147    // we need to check the counter party version in try and ack (sometimes here)
148    enforce_order_and_version(msg.channel(), msg.counterparty_version())?;
149
150    let channel: IbcChannel = msg.into();
151    let info = ChannelInfo {
152        id: channel.endpoint.channel_id,
153        counterparty_endpoint: channel.counterparty_endpoint,
154        connection_id: channel.connection_id,
155    };
156    CHANNEL_INFO.save(deps.storage, &info.id, &info)?;
157
158    Ok(IbcBasicResponse::default())
159}
160
161fn enforce_order_and_version(
162    channel: &IbcChannel,
163    counterparty_version: Option<&str>,
164) -> Result<(), ContractError> {
165    if channel.version != ICS20_VERSION {
166        return Err(ContractError::InvalidIbcVersion {
167            version: channel.version.clone(),
168        });
169    }
170    if let Some(version) = counterparty_version {
171        if version != ICS20_VERSION {
172            return Err(ContractError::InvalidIbcVersion {
173                version: version.to_string(),
174            });
175        }
176    }
177    if channel.order != ICS20_ORDERING {
178        return Err(ContractError::OnlyOrderedChannel {});
179    }
180    Ok(())
181}
182
183#[cfg_attr(not(feature = "library"), entry_point)]
184pub fn ibc_channel_close(
185    _deps: DepsMut,
186    _env: Env,
187    _channel: IbcChannelCloseMsg,
188) -> Result<IbcBasicResponse, ContractError> {
189    // TODO: what to do here?
190    // we will have locked funds that need to be returned somehow
191    unimplemented!();
192}
193
194#[cfg_attr(not(feature = "library"), entry_point)]
195/// Check to see if we have any balance here
196/// We should not return an error if possible, but rather an acknowledgement of failure
197pub fn ibc_packet_receive(
198    deps: DepsMut,
199    _env: Env,
200    msg: IbcPacketReceiveMsg,
201) -> Result<IbcReceiveResponse, Never> {
202    let packet = msg.packet;
203
204    do_ibc_packet_receive(deps, &packet).or_else(|err| {
205        Ok(
206            IbcReceiveResponse::new(ack_fail(err.to_string())).add_attributes(vec![
207                attr("action", "receive"),
208                attr("success", "false"),
209                attr("error", err.to_string()),
210            ]),
211        )
212    })
213}
214
215// Returns local denom if the denom is an encoded voucher from the expected endpoint
216// Otherwise, error
217fn parse_voucher_denom<'a>(
218    voucher_denom: &'a str,
219    remote_endpoint: &IbcEndpoint,
220) -> Result<&'a str, ContractError> {
221    let split_denom: Vec<&str> = voucher_denom.splitn(3, '/').collect();
222    if split_denom.len() != 3 {
223        return Err(ContractError::NoForeignTokens {});
224    }
225    // a few more sanity checks
226    if split_denom[0] != remote_endpoint.port_id {
227        return Err(ContractError::FromOtherPort {
228            port: split_denom[0].into(),
229        });
230    }
231    if split_denom[1] != remote_endpoint.channel_id {
232        return Err(ContractError::FromOtherChannel {
233            channel: split_denom[1].into(),
234        });
235    }
236
237    Ok(split_denom[2])
238}
239
240// this does the work of ibc_packet_receive, we wrap it to turn errors into acknowledgements
241fn do_ibc_packet_receive(
242    deps: DepsMut,
243    packet: &IbcPacket,
244) -> Result<IbcReceiveResponse, ContractError> {
245    let msg: Ics20Packet = from_json(&packet.data)?;
246    let channel = packet.dest.channel_id.clone();
247
248    // If the token originated on the remote chain, it looks like "ucosm".
249    // If it originated on our chain, it looks like "port/channel/ucosm".
250    let denom = parse_voucher_denom(&msg.denom, &packet.src)?;
251
252    // make sure we have enough balance for this
253    reduce_channel_balance(deps.storage, &channel, denom, msg.amount)?;
254
255    // we need to save the data to update the balances in reply
256    let reply_args = ReplyArgs {
257        channel,
258        denom: denom.to_string(),
259        amount: msg.amount,
260    };
261    REPLY_ARGS.save(deps.storage, &reply_args)?;
262
263    let to_send = Amount::from_parts(denom.to_string(), msg.amount);
264    let gas_limit = check_gas_limit(deps.as_ref(), &to_send)?;
265    let send = send_amount(to_send, msg.receiver.clone());
266    let mut submsg = SubMsg::reply_on_error(send, RECEIVE_ID);
267    submsg.gas_limit = gas_limit;
268
269    let res = IbcReceiveResponse::new(ack_success())
270        .add_submessage(submsg)
271        .add_attribute("action", "receive")
272        .add_attribute("sender", msg.sender)
273        .add_attribute("receiver", msg.receiver)
274        .add_attribute("denom", denom)
275        .add_attribute("amount", msg.amount)
276        .add_attribute("success", "true");
277
278    Ok(res)
279}
280
281fn check_gas_limit(deps: Deps, amount: &Amount) -> Result<Option<u64>, ContractError> {
282    match amount {
283        Amount::Cw20(coin) => {
284            // if cw20 token, use the registered gas limit, or error if not whitelisted
285            let addr = deps.api.addr_validate(&coin.address)?;
286            let allowed = ALLOW_LIST.may_load(deps.storage, &addr)?;
287            match allowed {
288                Some(allow) => Ok(allow.gas_limit),
289                None => match CONFIG.load(deps.storage)?.default_gas_limit {
290                    Some(base) => Ok(Some(base)),
291                    None => Err(ContractError::NotOnAllowList),
292                },
293            }
294        }
295        _ => Ok(None),
296    }
297}
298
299#[cfg_attr(not(feature = "library"), entry_point)]
300/// check if success or failure and update balance, or return funds
301pub fn ibc_packet_ack(
302    deps: DepsMut,
303    _env: Env,
304    msg: IbcPacketAckMsg,
305) -> Result<IbcBasicResponse, ContractError> {
306    // Design decision: should we trap error like in receive?
307    // TODO: unsure... as it is now a failed ack handling would revert the tx and would be
308    // retried again and again. is that good?
309    let ics20msg: Ics20Ack = from_json(&msg.acknowledgement.data)?;
310    match ics20msg {
311        Ics20Ack::Result(_) => on_packet_success(deps, msg.original_packet),
312        Ics20Ack::Error(err) => on_packet_failure(deps, msg.original_packet, err),
313    }
314}
315
316#[cfg_attr(not(feature = "library"), entry_point)]
317/// return fund to original sender (same as failure in ibc_packet_ack)
318pub fn ibc_packet_timeout(
319    deps: DepsMut,
320    _env: Env,
321    msg: IbcPacketTimeoutMsg,
322) -> Result<IbcBasicResponse, ContractError> {
323    // TODO: trap error like in receive? (same question as ack above)
324    let packet = msg.packet;
325    on_packet_failure(deps, packet, "timeout".to_string())
326}
327
328// update the balance stored on this (channel, denom) index
329fn on_packet_success(_deps: DepsMut, packet: IbcPacket) -> Result<IbcBasicResponse, ContractError> {
330    let msg: Ics20Packet = from_json(packet.data)?;
331
332    // similar event messages like ibctransfer module
333    let attributes = vec![
334        attr("action", "acknowledge"),
335        attr("sender", &msg.sender),
336        attr("receiver", &msg.receiver),
337        attr("denom", &msg.denom),
338        attr("amount", msg.amount),
339        attr("success", "true"),
340    ];
341
342    Ok(IbcBasicResponse::new().add_attributes(attributes))
343}
344
345// return the tokens to sender
346fn on_packet_failure(
347    deps: DepsMut,
348    packet: IbcPacket,
349    err: String,
350) -> Result<IbcBasicResponse, ContractError> {
351    let msg: Ics20Packet = from_json(&packet.data)?;
352
353    // undo the balance update on failure (as we pre-emptively added it on send)
354    reduce_channel_balance(deps.storage, &packet.src.channel_id, &msg.denom, msg.amount)?;
355
356    let to_send = Amount::from_parts(msg.denom.clone(), msg.amount);
357    let gas_limit = check_gas_limit(deps.as_ref(), &to_send)?;
358    let send = send_amount(to_send, msg.sender.clone());
359    let mut submsg = SubMsg::reply_on_error(send, ACK_FAILURE_ID);
360    submsg.gas_limit = gas_limit;
361
362    // similar event messages like ibctransfer module
363    let res = IbcBasicResponse::new()
364        .add_submessage(submsg)
365        .add_attribute("action", "acknowledge")
366        .add_attribute("sender", msg.sender)
367        .add_attribute("receiver", msg.receiver)
368        .add_attribute("denom", msg.denom)
369        .add_attribute("amount", msg.amount.to_string())
370        .add_attribute("success", "false")
371        .add_attribute("error", err);
372
373    Ok(res)
374}
375
376fn send_amount(amount: Amount, recipient: String) -> CosmosMsg {
377    match amount {
378        Amount::Native(coin) => BankMsg::Send {
379            to_address: recipient,
380            amount: vec![coin],
381        }
382        .into(),
383        Amount::Cw20(coin) => {
384            let msg = Cw20ExecuteMsg::Transfer {
385                recipient,
386                amount: coin.amount,
387            };
388            WasmMsg::Execute {
389                contract_addr: coin.address,
390                msg: to_json_binary(&msg).unwrap(),
391                funds: vec![],
392            }
393            .into()
394        }
395    }
396}
397
398#[cfg(test)]
399mod test {
400    use super::*;
401    use crate::test_helpers::*;
402
403    use crate::contract::{execute, migrate, query_channel};
404    use crate::msg::{ExecuteMsg, MigrateMsg, TransferMsg};
405    use cosmwasm_std::testing::{mock_env, mock_info};
406    use cosmwasm_std::{coins, to_json_vec, Addr, IbcEndpoint, IbcMsg, IbcTimeout, Timestamp};
407    use cw20::Cw20ReceiveMsg;
408
409    use easy_addr::addr;
410
411    #[test]
412    fn check_ack_json() {
413        let success = Ics20Ack::Result(b"1".into());
414        let fail = Ics20Ack::Error("bad coin".into());
415
416        let success_json = String::from_utf8(to_json_vec(&success).unwrap()).unwrap();
417        assert_eq!(r#"{"result":"MQ=="}"#, success_json.as_str());
418
419        let fail_json = String::from_utf8(to_json_vec(&fail).unwrap()).unwrap();
420        assert_eq!(r#"{"error":"bad coin"}"#, fail_json.as_str());
421    }
422
423    #[test]
424    fn check_packet_json() {
425        let packet = Ics20Packet::new(
426            Uint128::new(12345),
427            "ucosm",
428            "cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n",
429            "wasm1fucynrfkrt684pm8jrt8la5h2csvs5cnldcgqc",
430        );
431        // Example message generated from the SDK
432        let expected = r#"{"amount":"12345","denom":"ucosm","receiver":"wasm1fucynrfkrt684pm8jrt8la5h2csvs5cnldcgqc","sender":"cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n"}"#;
433
434        let encdoded = String::from_utf8(to_json_vec(&packet).unwrap()).unwrap();
435        assert_eq!(expected, encdoded.as_str());
436    }
437
438    fn cw20_payment(
439        amount: u128,
440        address: &str,
441        recipient: &str,
442        gas_limit: Option<u64>,
443    ) -> SubMsg {
444        let msg = Cw20ExecuteMsg::Transfer {
445            recipient: recipient.into(),
446            amount: Uint128::new(amount),
447        };
448        let exec = WasmMsg::Execute {
449            contract_addr: address.into(),
450            msg: to_json_binary(&msg).unwrap(),
451            funds: vec![],
452        };
453        let mut msg = SubMsg::reply_on_error(exec, RECEIVE_ID);
454        msg.gas_limit = gas_limit;
455        msg
456    }
457
458    fn native_payment(amount: u128, denom: &str, recipient: &str) -> SubMsg {
459        SubMsg::reply_on_error(
460            BankMsg::Send {
461                to_address: recipient.into(),
462                amount: coins(amount, denom),
463            },
464            RECEIVE_ID,
465        )
466    }
467
468    fn mock_receive_packet(
469        my_channel: &str,
470        amount: u128,
471        denom: &str,
472        receiver: &str,
473    ) -> IbcPacket {
474        let data = Ics20Packet {
475            // this is returning a foreign (our) token, thus denom is <port>/<channel>/<denom>
476            denom: format!("{}/{}/{}", REMOTE_PORT, "channel-1234", denom),
477            amount: amount.into(),
478            sender: "remote-sender".to_string(),
479            receiver: receiver.to_string(),
480            memo: None,
481        };
482        print!("Packet denom: {}", &data.denom);
483        IbcPacket::new(
484            to_json_binary(&data).unwrap(),
485            IbcEndpoint {
486                port_id: REMOTE_PORT.to_string(),
487                channel_id: "channel-1234".to_string(),
488            },
489            IbcEndpoint {
490                port_id: CONTRACT_PORT.to_string(),
491                channel_id: my_channel.to_string(),
492            },
493            3,
494            Timestamp::from_seconds(1665321069).into(),
495        )
496    }
497
498    #[test]
499    fn send_receive_cw20() {
500        let send_channel = "channel-9";
501        let cw20_addr = addr!("token-addr");
502        let cw20_denom = concat!("cw20:", addr!("token-addr"));
503        let local_rcpt = addr!("local-rcpt");
504        let local_sender = addr!("local-sender");
505        let remote_rcpt = addr!("remote-rcpt");
506        let gas_limit = 1234567;
507        let mut deps = setup(
508            &["channel-1", "channel-7", send_channel],
509            &[(cw20_addr, gas_limit)],
510        );
511
512        // prepare some mock packets
513        let recv_packet = mock_receive_packet(send_channel, 876543210, cw20_denom, local_rcpt);
514        let recv_high_packet =
515            mock_receive_packet(send_channel, 1876543210, cw20_denom, local_rcpt);
516
517        // cannot receive this denom yet
518        let msg = IbcPacketReceiveMsg::new(recv_packet.clone(), Addr::unchecked(""));
519        let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap();
520        assert!(res.messages.is_empty());
521        let ack: Ics20Ack = from_json(res.acknowledgement.unwrap()).unwrap();
522        let no_funds = Ics20Ack::Error(ContractError::InsufficientFunds {}.to_string());
523        assert_eq!(ack, no_funds);
524
525        // we send some cw20 tokens over
526        let transfer = TransferMsg {
527            channel: send_channel.to_string(),
528            remote_address: remote_rcpt.to_string(),
529            timeout: None,
530            memo: None,
531        };
532        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
533            sender: local_sender.to_string(),
534            amount: Uint128::new(987654321),
535            msg: to_json_binary(&transfer).unwrap(),
536        });
537        let info = mock_info(cw20_addr, &[]);
538        let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap();
539        assert_eq!(1, res.messages.len());
540        let expected = Ics20Packet {
541            denom: cw20_denom.into(),
542            amount: Uint128::new(987654321),
543            sender: local_sender.to_string(),
544            receiver: remote_rcpt.to_string(),
545            memo: None,
546        };
547        let timeout = mock_env().block.time.plus_seconds(DEFAULT_TIMEOUT);
548
549        assert_eq!(
550            &res.messages[0],
551            &SubMsg::new(IbcMsg::SendPacket {
552                channel_id: send_channel.to_string(),
553                data: to_json_binary(&expected).unwrap(),
554                timeout: IbcTimeout::with_timestamp(timeout),
555            })
556        );
557
558        // query channel state|_|
559        let state = query_channel(deps.as_ref(), send_channel.to_string()).unwrap();
560        assert_eq!(state.balances, vec![Amount::cw20(987654321, cw20_addr)]);
561        assert_eq!(state.total_sent, vec![Amount::cw20(987654321, cw20_addr)]);
562
563        // cannot receive more than we sent
564        let msg = IbcPacketReceiveMsg::new(recv_high_packet, Addr::unchecked(""));
565        let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap();
566        assert!(res.messages.is_empty());
567        let ack: Ics20Ack = from_json(res.acknowledgement.unwrap()).unwrap();
568        assert_eq!(ack, no_funds);
569
570        // we can receive less than we sent
571        let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked(""));
572        let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap();
573        assert_eq!(1, res.messages.len());
574        assert_eq!(
575            cw20_payment(876543210, cw20_addr, local_rcpt, Some(gas_limit)),
576            res.messages[0]
577        );
578        let ack: Ics20Ack = from_json(res.acknowledgement.unwrap()).unwrap();
579        assert!(matches!(ack, Ics20Ack::Result(_)));
580
581        // TODO: we need to call the reply block
582
583        // query channel state
584        let state = query_channel(deps.as_ref(), send_channel.to_string()).unwrap();
585        assert_eq!(state.balances, vec![Amount::cw20(111111111, cw20_addr)]);
586        assert_eq!(state.total_sent, vec![Amount::cw20(987654321, cw20_addr)]);
587    }
588
589    #[test]
590    fn send_receive_native() {
591        let send_channel = "channel-9";
592        let mut deps = setup(&["channel-1", "channel-7", send_channel], &[]);
593
594        let denom = "uatom";
595
596        // prepare some mock packets
597        let recv_packet = mock_receive_packet(send_channel, 876543210, denom, "local-rcpt");
598        let recv_high_packet = mock_receive_packet(send_channel, 1876543210, denom, "local-rcpt");
599
600        // cannot receive this denom yet
601        let msg = IbcPacketReceiveMsg::new(recv_packet.clone(), Addr::unchecked(""));
602        let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap();
603        assert!(res.messages.is_empty());
604        let ack: Ics20Ack = from_json(res.acknowledgement.unwrap()).unwrap();
605        let no_funds = Ics20Ack::Error(ContractError::InsufficientFunds {}.to_string());
606        assert_eq!(ack, no_funds);
607
608        // we transfer some tokens
609        let msg = ExecuteMsg::Transfer(TransferMsg {
610            channel: send_channel.to_string(),
611            remote_address: "my-remote-address".to_string(),
612            timeout: None,
613            memo: None,
614        });
615        let info = mock_info("local-sender", &coins(987654321, denom));
616        execute(deps.as_mut(), mock_env(), info, msg).unwrap();
617
618        // query channel state|_|
619        let state = query_channel(deps.as_ref(), send_channel.to_string()).unwrap();
620        assert_eq!(state.balances, vec![Amount::native(987654321, denom)]);
621        assert_eq!(state.total_sent, vec![Amount::native(987654321, denom)]);
622
623        // cannot receive more than we sent
624        let msg = IbcPacketReceiveMsg::new(recv_high_packet, Addr::unchecked(""));
625        let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap();
626        assert!(res.messages.is_empty());
627        let ack: Ics20Ack = from_json(res.acknowledgement.unwrap()).unwrap();
628        assert_eq!(ack, no_funds);
629
630        // we can receive less than we sent
631        let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked(""));
632        let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap();
633        assert_eq!(1, res.messages.len());
634        assert_eq!(
635            native_payment(876543210, denom, "local-rcpt"),
636            res.messages[0]
637        );
638        let ack: Ics20Ack = from_json(res.acknowledgement.unwrap()).unwrap();
639        assert!(matches!(ack, Ics20Ack::Result(_)));
640
641        // only need to call reply block on error case
642
643        // query channel state
644        let state = query_channel(deps.as_ref(), send_channel.to_string()).unwrap();
645        assert_eq!(state.balances, vec![Amount::native(111111111, denom)]);
646        assert_eq!(state.total_sent, vec![Amount::native(987654321, denom)]);
647    }
648
649    #[test]
650    fn check_gas_limit_handles_all_cases() {
651        let send_channel = "channel-9";
652        let allowed = addr!("foobar");
653        let allowed_gas = 777666;
654        let mut deps = setup(&[send_channel], &[(allowed, allowed_gas)]);
655
656        // allow list will get proper gas
657        let limit = check_gas_limit(deps.as_ref(), &Amount::cw20(500, allowed)).unwrap();
658        assert_eq!(limit, Some(allowed_gas));
659
660        // non-allow list will error
661        let random = addr!("tokenz");
662        check_gas_limit(deps.as_ref(), &Amount::cw20(500, random)).unwrap_err();
663
664        // add default_gas_limit
665        let def_limit = 54321;
666        migrate(
667            deps.as_mut(),
668            mock_env(),
669            MigrateMsg {
670                default_gas_limit: Some(def_limit),
671            },
672        )
673        .unwrap();
674
675        // allow list still gets proper gas
676        let limit = check_gas_limit(deps.as_ref(), &Amount::cw20(500, allowed)).unwrap();
677        assert_eq!(limit, Some(allowed_gas));
678
679        // non-allow list will now get default
680        let limit = check_gas_limit(deps.as_ref(), &Amount::cw20(500, random)).unwrap();
681        assert_eq!(limit, Some(def_limit));
682    }
683}