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#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)]
30pub struct Ics20Packet {
31 pub amount: Uint128,
33 pub denom: String,
35 pub receiver: String,
37 pub sender: String,
39 #[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#[cw_serde]
72pub enum Ics20Ack {
73 Result(Binary),
74 Error(String),
75}
76
77fn ack_success() -> Binary {
79 let res = Ics20Ack::Result(b"1".into());
80 to_json_binary(&res).unwrap()
81}
82
83fn 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 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)]
130pub 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)]
141pub fn ibc_channel_connect(
143 deps: DepsMut,
144 _env: Env,
145 msg: IbcChannelConnectMsg,
146) -> Result<IbcBasicResponse, ContractError> {
147 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 unimplemented!();
192}
193
194#[cfg_attr(not(feature = "library"), entry_point)]
195pub 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
215fn 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 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
240fn 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 let denom = parse_voucher_denom(&msg.denom, &packet.src)?;
251
252 reduce_channel_balance(deps.storage, &channel, denom, msg.amount)?;
254
255 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 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)]
300pub fn ibc_packet_ack(
302 deps: DepsMut,
303 _env: Env,
304 msg: IbcPacketAckMsg,
305) -> Result<IbcBasicResponse, ContractError> {
306 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)]
317pub fn ibc_packet_timeout(
319 deps: DepsMut,
320 _env: Env,
321 msg: IbcPacketTimeoutMsg,
322) -> Result<IbcBasicResponse, ContractError> {
323 let packet = msg.packet;
325 on_packet_failure(deps, packet, "timeout".to_string())
326}
327
328fn on_packet_success(_deps: DepsMut, packet: IbcPacket) -> Result<IbcBasicResponse, ContractError> {
330 let msg: Ics20Packet = from_json(packet.data)?;
331
332 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
345fn 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let limit = check_gas_limit(deps.as_ref(), &Amount::cw20(500, allowed)).unwrap();
658 assert_eq!(limit, Some(allowed_gas));
659
660 let random = addr!("tokenz");
662 check_gas_limit(deps.as_ref(), &Amount::cw20(500, random)).unwrap_err();
663
664 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 let limit = check_gas_limit(deps.as_ref(), &Amount::cw20(500, allowed)).unwrap();
677 assert_eq!(limit, Some(allowed_gas));
678
679 let limit = check_gas_limit(deps.as_ref(), &Amount::cw20(500, random)).unwrap();
681 assert_eq!(limit, Some(def_limit));
682 }
683}