use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cosmwasm_std::{
attr, entry_point, from_binary, to_binary, BankMsg, Binary, CosmosMsg, DepsMut, Env, HumanAddr,
IbcAcknowledgement, IbcBasicResponse, IbcChannel, IbcEndpoint, IbcOrder, IbcPacket,
IbcReceiveResponse, StdResult, Uint128, WasmMsg,
};
use crate::amount::Amount;
use crate::error::{ContractError, Never};
use crate::state::{ChannelInfo, CHANNEL_INFO, CHANNEL_STATE};
use cw20::Cw20ExecuteMsg;
pub const ICS20_VERSION: &str = "ics20-1";
pub const ICS20_ORDERING: IbcOrder = IbcOrder::Unordered;
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)]
pub struct Ics20Packet {
pub amount: Uint128,
pub denom: String,
pub receiver: String,
pub sender: 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(),
}
}
pub fn validate(&self) -> Result<(), ContractError> {
if self.amount.u128() > (u64::MAX as u128) {
Err(ContractError::AmountOverflow {})
} else {
Ok(())
}
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
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()
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_open(
_deps: DepsMut,
_env: Env,
channel: IbcChannel,
) -> Result<(), ContractError> {
enforce_order_and_version(&channel)?;
Ok(())
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_connect(
deps: DepsMut,
_env: Env,
channel: IbcChannel,
) -> Result<IbcBasicResponse, ContractError> {
enforce_order_and_version(&channel)?;
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) -> Result<(), ContractError> {
if channel.version != ICS20_VERSION {
return Err(ContractError::InvalidIbcVersion {
version: channel.version.clone(),
});
}
if let Some(version) = &channel.counterparty_version {
if version != ICS20_VERSION {
return Err(ContractError::InvalidIbcVersion {
version: version.clone(),
});
}
}
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: IbcChannel,
) -> Result<IbcBasicResponse, ContractError> {
unimplemented!();
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_receive(
deps: DepsMut,
_env: Env,
packet: IbcPacket,
) -> Result<IbcReceiveResponse, Never> {
let res = match do_ibc_packet_receive(deps, &packet) {
Ok(msg) => {
let denom = parse_voucher_denom(&msg.denom, &packet.src).unwrap();
let attributes = vec![
attr("action", "receive"),
attr("sender", &msg.sender),
attr("receiver", &msg.receiver),
attr("denom", denom),
attr("amount", msg.amount),
attr("success", "true"),
];
let to_send = Amount::from_parts(denom.into(), msg.amount);
let msg = send_amount(to_send, HumanAddr::from(msg.receiver));
IbcReceiveResponse {
acknowledgement: ack_success(),
submessages: vec![],
messages: vec![msg],
attributes,
}
}
Err(err) => IbcReceiveResponse {
acknowledgement: ack_fail(err.to_string()),
submessages: vec![],
messages: vec![],
attributes: vec![
attr("action", "receive"),
attr("success", "false"),
attr("error", err),
],
},
};
Ok(res)
}
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<Ics20Packet, ContractError> {
let msg: Ics20Packet = from_binary(&packet.data)?;
let channel = packet.dest.channel_id.clone();
let denom = parse_voucher_denom(&msg.denom, &packet.src)?;
let amount = msg.amount;
CHANNEL_STATE.update(
deps.storage,
(&channel, denom),
|orig| -> Result<_, ContractError> {
let mut cur = orig.ok_or(ContractError::InsufficientFunds {})?;
cur.outstanding =
(cur.outstanding - amount).or(Err(ContractError::InsufficientFunds {}))?;
Ok(cur)
},
)?;
Ok(msg)
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_ack(
deps: DepsMut,
_env: Env,
ack: IbcAcknowledgement,
) -> Result<IbcBasicResponse, ContractError> {
let msg: Ics20Ack = from_binary(&ack.acknowledgement)?;
match msg {
Ics20Ack::Result(_) => on_packet_success(deps, ack.original_packet),
Ics20Ack::Error(err) => on_packet_failure(deps, ack.original_packet, err),
}
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_timeout(
deps: DepsMut,
_env: Env,
packet: IbcPacket,
) -> Result<IbcBasicResponse, ContractError> {
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"),
];
let channel = packet.src.channel_id;
let denom = msg.denom;
let amount = msg.amount;
CHANNEL_STATE.update(deps.storage, (&channel, &denom), |orig| -> StdResult<_> {
let mut state = orig.unwrap_or_default();
state.outstanding += amount;
state.total_sent += amount;
Ok(state)
})?;
Ok(IbcBasicResponse {
submessages: vec![],
messages: vec![],
attributes,
})
}
fn on_packet_failure(
_deps: DepsMut,
packet: IbcPacket,
err: String,
) -> 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", "false"),
attr("error", err),
];
let amount = Amount::from_parts(msg.denom, msg.amount);
let msg = send_amount(amount, HumanAddr::from(msg.sender));
let res = IbcBasicResponse {
submessages: vec![],
messages: vec![msg],
attributes,
};
Ok(res)
}
fn send_amount(amount: Amount, recipient: HumanAddr) -> 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,
};
let exec = WasmMsg::Execute {
contract_addr: coin.address,
msg: to_binary(&msg).unwrap(),
send: vec![],
};
exec.into()
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_helpers::*;
use crate::contract::query_channel;
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::{coins, to_vec, IbcEndpoint};
#[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(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) -> CosmosMsg {
let msg = Cw20ExecuteMsg::Transfer {
recipient: recipient.into(),
amount: Uint128(amount),
};
let exec = WasmMsg::Execute {
contract_addr: address.into(),
msg: to_binary(&msg).unwrap(),
send: vec![],
};
exec.into()
}
fn native_payment(amount: u128, denom: &str, recipient: &str) -> CosmosMsg {
BankMsg::Send {
to_address: recipient.into(),
amount: coins(amount, denom),
}
.into()
}
fn mock_sent_packet(my_channel: &str, amount: u128, denom: &str, sender: &str) -> IbcPacket {
let data = Ics20Packet {
denom: denom.into(),
amount: amount.into(),
sender: sender.to_string(),
receiver: "remote-rcpt".to_string(),
};
IbcPacket {
data: to_binary(&data).unwrap(),
src: IbcEndpoint {
port_id: CONTRACT_PORT.to_string(),
channel_id: my_channel.to_string(),
},
dest: IbcEndpoint {
port_id: REMOTE_PORT.to_string(),
channel_id: "channel-1234".to_string(),
},
sequence: 2,
timeout_block: None,
timeout_timestamp: Some(1665321069000000000u64),
}
}
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(),
};
print!("Packet denom: {}", &data.denom);
IbcPacket {
data: to_binary(&data).unwrap(),
src: IbcEndpoint {
port_id: REMOTE_PORT.to_string(),
channel_id: "channel-1234".to_string(),
},
dest: IbcEndpoint {
port_id: CONTRACT_PORT.to_string(),
channel_id: my_channel.to_string(),
},
sequence: 3,
timeout_block: None,
timeout_timestamp: Some(1665321069000000000u64),
}
}
#[test]
fn send_receive_cw20() {
let send_channel = "channel-9";
let mut deps = setup(&["channel-1", "channel-7", send_channel]);
let cw20_addr = "token-addr";
let cw20_denom = "cw20:token-addr";
let sent_packet = mock_sent_packet(send_channel, 987654321, cw20_denom, "local-sender");
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 res = ibc_packet_receive(deps.as_mut(), mock_env(), recv_packet.clone()).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 ack = IbcAcknowledgement {
acknowledgement: ack_success(),
original_packet: sent_packet.clone(),
};
let res = ibc_packet_ack(deps.as_mut(), mock_env(), ack).unwrap();
assert_eq!(0, res.messages.len());
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 res = ibc_packet_receive(deps.as_mut(), mock_env(), recv_high_packet).unwrap();
assert!(res.messages.is_empty());
let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap();
assert_eq!(ack, no_funds);
let res = ibc_packet_receive(deps.as_mut(), mock_env(), recv_packet).unwrap();
assert_eq!(1, res.messages.len());
assert_eq!(
cw20_payment(876543210, cw20_addr, "local-rcpt"),
res.messages[0]
);
let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap();
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 sent_packet = mock_sent_packet(send_channel, 987654321, denom, "local-sender");
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 res = ibc_packet_receive(deps.as_mut(), mock_env(), recv_packet.clone()).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 ack = IbcAcknowledgement {
acknowledgement: ack_success(),
original_packet: sent_packet.clone(),
};
let res = ibc_packet_ack(deps.as_mut(), mock_env(), ack).unwrap();
assert_eq!(0, res.messages.len());
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 res = ibc_packet_receive(deps.as_mut(), mock_env(), recv_high_packet).unwrap();
assert!(res.messages.is_empty());
let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap();
assert_eq!(ack, no_funds);
let res = ibc_packet_receive(deps.as_mut(), mock_env(), recv_packet).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();
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)]);
}
}