use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{
attr, entry_point, from_binary, to_binary, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env,
IbcBasicResponse, IbcChannel, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg,
IbcEndpoint, IbcOrder, IbcPacket, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg,
IbcReceiveResponse, Reply, Response, SubMsg, SubMsgResult, Uint128, WasmMsg,
};
use crate::amount::Amount;
use crate::error::{ContractError, Never};
use crate::state::{
reduce_channel_balance, undo_reduce_channel_balance, ChannelInfo, ReplyArgs, ALLOW_LIST,
CHANNEL_INFO, CONFIG, REPLY_ARGS,
};
use cw20::Cw20ExecuteMsg;
pub const ICS20_VERSION: &str = "ics20-1";
pub const ICS20_ORDERING: IbcOrder = IbcOrder::Unordered;
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)]
pub struct Ics20Packet {
pub amount: Uint128,
pub denom: String,
pub receiver: String,
pub sender: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub memo: Option<String>,
}
impl Ics20Packet {
pub fn new<T: Into<String>>(amount: Uint128, denom: T, sender: &str, receiver: &str) -> Self {
Ics20Packet {
denom: denom.into(),
amount,
sender: sender.to_string(),
receiver: receiver.to_string(),
memo: None,
}
}
pub fn with_memo(self, memo: Option<String>) -> Self {
Ics20Packet { memo, ..self }
}
pub fn validate(&self) -> Result<(), ContractError> {
if self.amount.u128() > (u64::MAX as u128) {
Err(ContractError::AmountOverflow {})
} else {
Ok(())
}
}
}
#[cw_serde]
pub enum Ics20Ack {
Result(Binary),
Error(String),
}
fn ack_success() -> Binary {
let res = Ics20Ack::Result(b"1".into());
to_binary(&res).unwrap()
}
fn ack_fail(err: String) -> Binary {
let res = Ics20Ack::Error(err);
to_binary(&res).unwrap()
}
const RECEIVE_ID: u64 = 1337;
const ACK_FAILURE_ID: u64 = 0xfa17;
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> Result<Response, ContractError> {
match reply.id {
RECEIVE_ID => match reply.result {
SubMsgResult::Ok(_) => Ok(Response::new()),
SubMsgResult::Err(err) => {
let reply_args = REPLY_ARGS.load(deps.storage)?;
undo_reduce_channel_balance(
deps.storage,
&reply_args.channel,
&reply_args.denom,
reply_args.amount,
)?;
Ok(Response::new().set_data(ack_fail(err)))
}
},
ACK_FAILURE_ID => match reply.result {
SubMsgResult::Ok(_) => Ok(Response::new()),
SubMsgResult::Err(err) => Ok(Response::new().set_data(ack_fail(err))),
},
_ => Err(ContractError::UnknownReplyId { id: reply.id }),
}
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_open(
_deps: DepsMut,
_env: Env,
msg: IbcChannelOpenMsg,
) -> Result<(), ContractError> {
enforce_order_and_version(msg.channel(), msg.counterparty_version())?;
Ok(())
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_connect(
deps: DepsMut,
_env: Env,
msg: IbcChannelConnectMsg,
) -> Result<IbcBasicResponse, ContractError> {
enforce_order_and_version(msg.channel(), msg.counterparty_version())?;
let channel: IbcChannel = msg.into();
let info = ChannelInfo {
id: channel.endpoint.channel_id,
counterparty_endpoint: channel.counterparty_endpoint,
connection_id: channel.connection_id,
};
CHANNEL_INFO.save(deps.storage, &info.id, &info)?;
Ok(IbcBasicResponse::default())
}
fn enforce_order_and_version(
channel: &IbcChannel,
counterparty_version: Option<&str>,
) -> Result<(), ContractError> {
if channel.version != ICS20_VERSION {
return Err(ContractError::InvalidIbcVersion {
version: channel.version.clone(),
});
}
if let Some(version) = counterparty_version {
if version != ICS20_VERSION {
return Err(ContractError::InvalidIbcVersion {
version: version.to_string(),
});
}
}
if channel.order != ICS20_ORDERING {
return Err(ContractError::OnlyOrderedChannel {});
}
Ok(())
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_close(
_deps: DepsMut,
_env: Env,
_channel: IbcChannelCloseMsg,
) -> Result<IbcBasicResponse, ContractError> {
unimplemented!();
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_receive(
deps: DepsMut,
_env: Env,
msg: IbcPacketReceiveMsg,
) -> Result<IbcReceiveResponse, Never> {
let packet = msg.packet;
do_ibc_packet_receive(deps, &packet).or_else(|err| {
Ok(IbcReceiveResponse::new()
.set_ack(ack_fail(err.to_string()))
.add_attributes(vec![
attr("action", "receive"),
attr("success", "false"),
attr("error", err.to_string()),
]))
})
}
fn parse_voucher_denom<'a>(
voucher_denom: &'a str,
remote_endpoint: &IbcEndpoint,
) -> Result<&'a str, ContractError> {
let split_denom: Vec<&str> = voucher_denom.splitn(3, '/').collect();
if split_denom.len() != 3 {
return Err(ContractError::NoForeignTokens {});
}
if split_denom[0] != remote_endpoint.port_id {
return Err(ContractError::FromOtherPort {
port: split_denom[0].into(),
});
}
if split_denom[1] != remote_endpoint.channel_id {
return Err(ContractError::FromOtherChannel {
channel: split_denom[1].into(),
});
}
Ok(split_denom[2])
}
fn do_ibc_packet_receive(
deps: DepsMut,
packet: &IbcPacket,
) -> Result<IbcReceiveResponse, ContractError> {
let msg: Ics20Packet = from_binary(&packet.data)?;
let channel = packet.dest.channel_id.clone();
let denom = parse_voucher_denom(&msg.denom, &packet.src)?;
reduce_channel_balance(deps.storage, &channel, denom, msg.amount)?;
let reply_args = ReplyArgs {
channel,
denom: denom.to_string(),
amount: msg.amount,
};
REPLY_ARGS.save(deps.storage, &reply_args)?;
let to_send = Amount::from_parts(denom.to_string(), msg.amount);
let gas_limit = check_gas_limit(deps.as_ref(), &to_send)?;
let send = send_amount(to_send, msg.receiver.clone());
let mut submsg = SubMsg::reply_on_error(send, RECEIVE_ID);
submsg.gas_limit = gas_limit;
let res = IbcReceiveResponse::new()
.set_ack(ack_success())
.add_submessage(submsg)
.add_attribute("action", "receive")
.add_attribute("sender", msg.sender)
.add_attribute("receiver", msg.receiver)
.add_attribute("denom", denom)
.add_attribute("amount", msg.amount)
.add_attribute("success", "true");
Ok(res)
}
fn check_gas_limit(deps: Deps, amount: &Amount) -> Result<Option<u64>, ContractError> {
match amount {
Amount::Cw20(coin) => {
let addr = deps.api.addr_validate(&coin.address)?;
let allowed = ALLOW_LIST.may_load(deps.storage, &addr)?;
match allowed {
Some(allow) => Ok(allow.gas_limit),
None => match CONFIG.load(deps.storage)?.default_gas_limit {
Some(base) => Ok(Some(base)),
None => Err(ContractError::NotOnAllowList),
},
}
}
_ => Ok(None),
}
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_ack(
deps: DepsMut,
_env: Env,
msg: IbcPacketAckMsg,
) -> Result<IbcBasicResponse, ContractError> {
let ics20msg: Ics20Ack = from_binary(&msg.acknowledgement.data)?;
match ics20msg {
Ics20Ack::Result(_) => on_packet_success(deps, msg.original_packet),
Ics20Ack::Error(err) => on_packet_failure(deps, msg.original_packet, err),
}
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_timeout(
deps: DepsMut,
_env: Env,
msg: IbcPacketTimeoutMsg,
) -> Result<IbcBasicResponse, ContractError> {
let packet = msg.packet;
on_packet_failure(deps, packet, "timeout".to_string())
}
fn on_packet_success(_deps: DepsMut, packet: IbcPacket) -> Result<IbcBasicResponse, ContractError> {
let msg: Ics20Packet = from_binary(&packet.data)?;
let attributes = vec![
attr("action", "acknowledge"),
attr("sender", &msg.sender),
attr("receiver", &msg.receiver),
attr("denom", &msg.denom),
attr("amount", msg.amount),
attr("success", "true"),
];
Ok(IbcBasicResponse::new().add_attributes(attributes))
}
fn on_packet_failure(
deps: DepsMut,
packet: IbcPacket,
err: String,
) -> Result<IbcBasicResponse, ContractError> {
let msg: Ics20Packet = from_binary(&packet.data)?;
reduce_channel_balance(deps.storage, &packet.src.channel_id, &msg.denom, msg.amount)?;
let to_send = Amount::from_parts(msg.denom.clone(), msg.amount);
let gas_limit = check_gas_limit(deps.as_ref(), &to_send)?;
let send = send_amount(to_send, msg.sender.clone());
let mut submsg = SubMsg::reply_on_error(send, ACK_FAILURE_ID);
submsg.gas_limit = gas_limit;
let res = IbcBasicResponse::new()
.add_submessage(submsg)
.add_attribute("action", "acknowledge")
.add_attribute("sender", msg.sender)
.add_attribute("receiver", msg.receiver)
.add_attribute("denom", msg.denom)
.add_attribute("amount", msg.amount.to_string())
.add_attribute("success", "false")
.add_attribute("error", err);
Ok(res)
}
fn send_amount(amount: Amount, recipient: String) -> CosmosMsg {
match amount {
Amount::Native(coin) => BankMsg::Send {
to_address: recipient,
amount: vec![coin],
}
.into(),
Amount::Cw20(coin) => {
let msg = Cw20ExecuteMsg::Transfer {
recipient,
amount: coin.amount,
};
WasmMsg::Execute {
contract_addr: coin.address,
msg: to_binary(&msg).unwrap(),
funds: vec![],
}
.into()
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_helpers::*;
use crate::contract::{execute, migrate, query_channel};
use crate::msg::{ExecuteMsg, MigrateMsg, TransferMsg};
use cosmwasm_std::testing::{mock_env, mock_info};
use cosmwasm_std::{coins, to_vec, IbcEndpoint, IbcMsg, IbcTimeout, Timestamp};
use cw20::Cw20ReceiveMsg;
#[test]
fn check_ack_json() {
let success = Ics20Ack::Result(b"1".into());
let fail = Ics20Ack::Error("bad coin".into());
let success_json = String::from_utf8(to_vec(&success).unwrap()).unwrap();
assert_eq!(r#"{"result":"MQ=="}"#, success_json.as_str());
let fail_json = String::from_utf8(to_vec(&fail).unwrap()).unwrap();
assert_eq!(r#"{"error":"bad coin"}"#, fail_json.as_str());
}
#[test]
fn check_packet_json() {
let packet = Ics20Packet::new(
Uint128::new(12345),
"ucosm",
"cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n",
"wasm1fucynrfkrt684pm8jrt8la5h2csvs5cnldcgqc",
);
let expected = r#"{"amount":"12345","denom":"ucosm","receiver":"wasm1fucynrfkrt684pm8jrt8la5h2csvs5cnldcgqc","sender":"cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n"}"#;
let encdoded = String::from_utf8(to_vec(&packet).unwrap()).unwrap();
assert_eq!(expected, encdoded.as_str());
}
fn cw20_payment(
amount: u128,
address: &str,
recipient: &str,
gas_limit: Option<u64>,
) -> SubMsg {
let msg = Cw20ExecuteMsg::Transfer {
recipient: recipient.into(),
amount: Uint128::new(amount),
};
let exec = WasmMsg::Execute {
contract_addr: address.into(),
msg: to_binary(&msg).unwrap(),
funds: vec![],
};
let mut msg = SubMsg::reply_on_error(exec, RECEIVE_ID);
msg.gas_limit = gas_limit;
msg
}
fn native_payment(amount: u128, denom: &str, recipient: &str) -> SubMsg {
SubMsg::reply_on_error(
BankMsg::Send {
to_address: recipient.into(),
amount: coins(amount, denom),
},
RECEIVE_ID,
)
}
fn mock_receive_packet(
my_channel: &str,
amount: u128,
denom: &str,
receiver: &str,
) -> IbcPacket {
let data = Ics20Packet {
denom: format!("{}/{}/{}", REMOTE_PORT, "channel-1234", denom),
amount: amount.into(),
sender: "remote-sender".to_string(),
receiver: receiver.to_string(),
memo: None,
};
print!("Packet denom: {}", &data.denom);
IbcPacket::new(
to_binary(&data).unwrap(),
IbcEndpoint {
port_id: REMOTE_PORT.to_string(),
channel_id: "channel-1234".to_string(),
},
IbcEndpoint {
port_id: CONTRACT_PORT.to_string(),
channel_id: my_channel.to_string(),
},
3,
Timestamp::from_seconds(1665321069).into(),
)
}
#[test]
fn send_receive_cw20() {
let send_channel = "channel-9";
let cw20_addr = "token-addr";
let cw20_denom = "cw20:token-addr";
let gas_limit = 1234567;
let mut deps = setup(
&["channel-1", "channel-7", send_channel],
&[(cw20_addr, gas_limit)],
);
let recv_packet = mock_receive_packet(send_channel, 876543210, cw20_denom, "local-rcpt");
let recv_high_packet =
mock_receive_packet(send_channel, 1876543210, cw20_denom, "local-rcpt");
let msg = IbcPacketReceiveMsg::new(recv_packet.clone());
let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap();
assert!(res.messages.is_empty());
let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap();
let no_funds = Ics20Ack::Error(ContractError::InsufficientFunds {}.to_string());
assert_eq!(ack, no_funds);
let transfer = TransferMsg {
channel: send_channel.to_string(),
remote_address: "remote-rcpt".to_string(),
timeout: None,
memo: None,
};
let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
sender: "local-sender".to_string(),
amount: Uint128::new(987654321),
msg: to_binary(&transfer).unwrap(),
});
let info = mock_info(cw20_addr, &[]);
let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap();
assert_eq!(1, res.messages.len());
let expected = Ics20Packet {
denom: cw20_denom.into(),
amount: Uint128::new(987654321),
sender: "local-sender".to_string(),
receiver: "remote-rcpt".to_string(),
memo: None,
};
let timeout = mock_env().block.time.plus_seconds(DEFAULT_TIMEOUT);
assert_eq!(
&res.messages[0],
&SubMsg::new(IbcMsg::SendPacket {
channel_id: send_channel.to_string(),
data: to_binary(&expected).unwrap(),
timeout: IbcTimeout::with_timestamp(timeout),
})
);
let state = query_channel(deps.as_ref(), send_channel.to_string()).unwrap();
assert_eq!(state.balances, vec![Amount::cw20(987654321, cw20_addr)]);
assert_eq!(state.total_sent, vec![Amount::cw20(987654321, cw20_addr)]);
let msg = IbcPacketReceiveMsg::new(recv_high_packet);
let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap();
assert!(res.messages.is_empty());
let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap();
assert_eq!(ack, no_funds);
let msg = IbcPacketReceiveMsg::new(recv_packet);
let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap();
assert_eq!(1, res.messages.len());
assert_eq!(
cw20_payment(876543210, cw20_addr, "local-rcpt", Some(gas_limit)),
res.messages[0]
);
let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap();
assert!(matches!(ack, Ics20Ack::Result(_)));
let state = query_channel(deps.as_ref(), send_channel.to_string()).unwrap();
assert_eq!(state.balances, vec![Amount::cw20(111111111, cw20_addr)]);
assert_eq!(state.total_sent, vec![Amount::cw20(987654321, cw20_addr)]);
}
#[test]
fn send_receive_native() {
let send_channel = "channel-9";
let mut deps = setup(&["channel-1", "channel-7", send_channel], &[]);
let denom = "uatom";
let recv_packet = mock_receive_packet(send_channel, 876543210, denom, "local-rcpt");
let recv_high_packet = mock_receive_packet(send_channel, 1876543210, denom, "local-rcpt");
let msg = IbcPacketReceiveMsg::new(recv_packet.clone());
let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap();
assert!(res.messages.is_empty());
let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap();
let no_funds = Ics20Ack::Error(ContractError::InsufficientFunds {}.to_string());
assert_eq!(ack, no_funds);
let msg = ExecuteMsg::Transfer(TransferMsg {
channel: send_channel.to_string(),
remote_address: "my-remote-address".to_string(),
timeout: None,
memo: None,
});
let info = mock_info("local-sender", &coins(987654321, denom));
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
let state = query_channel(deps.as_ref(), send_channel.to_string()).unwrap();
assert_eq!(state.balances, vec![Amount::native(987654321, denom)]);
assert_eq!(state.total_sent, vec![Amount::native(987654321, denom)]);
let msg = IbcPacketReceiveMsg::new(recv_high_packet);
let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap();
assert!(res.messages.is_empty());
let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap();
assert_eq!(ack, no_funds);
let msg = IbcPacketReceiveMsg::new(recv_packet);
let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap();
assert_eq!(1, res.messages.len());
assert_eq!(
native_payment(876543210, denom, "local-rcpt"),
res.messages[0]
);
let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap();
assert!(matches!(ack, Ics20Ack::Result(_)));
let state = query_channel(deps.as_ref(), send_channel.to_string()).unwrap();
assert_eq!(state.balances, vec![Amount::native(111111111, denom)]);
assert_eq!(state.total_sent, vec![Amount::native(987654321, denom)]);
}
#[test]
fn check_gas_limit_handles_all_cases() {
let send_channel = "channel-9";
let allowed = "foobar";
let allowed_gas = 777666;
let mut deps = setup(&[send_channel], &[(allowed, allowed_gas)]);
let limit = check_gas_limit(deps.as_ref(), &Amount::cw20(500, allowed)).unwrap();
assert_eq!(limit, Some(allowed_gas));
let random = "tokenz";
check_gas_limit(deps.as_ref(), &Amount::cw20(500, random)).unwrap_err();
let def_limit = 54321;
migrate(
deps.as_mut(),
mock_env(),
MigrateMsg {
default_gas_limit: Some(def_limit),
},
)
.unwrap();
let limit = check_gas_limit(deps.as_ref(), &Amount::cw20(500, allowed)).unwrap();
assert_eq!(limit, Some(allowed_gas));
let limit = check_gas_limit(deps.as_ref(), &Amount::cw20(500, random)).unwrap();
assert_eq!(limit, Some(def_limit));
}
}