Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix cw20 ics20 packets #684

Merged
merged 6 commits into from
Mar 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions contracts/cw20-ics20/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use cw_storage_plus::Bound;
use crate::amount::Amount;
use crate::error::ContractError;
use crate::ibc::Ics20Packet;
use crate::migrations::v1;
use crate::migrations::{v1, v2};
use crate::msg::{
AllowMsg, AllowedInfo, AllowedResponse, ChannelResponse, ConfigResponse, ExecuteMsg, InitMsg,
ListAllowedResponse, ListChannelsResponse, MigrateMsg, PortResponse, QueryMsg, TransferMsg,
Expand Down Expand Up @@ -199,9 +199,10 @@ pub fn execute_allow(

const MIGRATE_MIN_VERSION: &str = "0.11.1";
const MIGRATE_VERSION_2: &str = "0.12.0-alpha1";
const MIGRATE_VERSION_3: &str = "0.13.1";

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(mut deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
pub fn migrate(mut deps: DepsMut, env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
let version: Version = CONTRACT_VERSION.parse().map_err(from_semver)?;
let stored = get_contract_version(deps.storage)?;
let storage_version: Version = stored.version.parse().map_err(from_semver)?;
Expand Down Expand Up @@ -235,6 +236,10 @@ pub fn migrate(mut deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Respons
};
CONFIG.save(deps.storage, &config)?;
}
// run the v2->v3 converstion if we are v2 style
if storage_version <= MIGRATE_VERSION_3.parse().map_err(from_semver)? {
v2::update_balances(deps.branch(), &env)?;
}
// otherwise no migration (yet) - add them here

// we don't need to save anything if migrating from the same version
Expand Down Expand Up @@ -360,9 +365,10 @@ mod test {
use super::*;
use crate::test_helpers::*;

use cosmwasm_std::testing::{mock_env, mock_info};
use cosmwasm_std::testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR};
use cosmwasm_std::{coin, coins, CosmosMsg, IbcMsg, StdError, Uint128};

use crate::state::ChannelState;
use cw_utils::PaymentError;

#[test]
Expand Down Expand Up @@ -527,4 +533,36 @@ mod test {
let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err();
assert_eq!(err, ContractError::NotOnAllowList);
}

#[test]
fn v3_migration_works() {
// basic state with one channel
let send_channel = "channel-15";
let cw20_addr = "my-token";
let native = "ucosm";
let mut deps = setup(&[send_channel], &[(cw20_addr, 123456)]);

// mock that we sent some tokens in both native and cw20 (TODO: cw20)
// balances set high
deps.querier
.update_balance(MOCK_CONTRACT_ADDR, coins(50000, native));

// channel state a bit lower (some in-flight acks)
let state = ChannelState {
// 14000 not accounted for (in-flight)
outstanding: Uint128::new(36000),
total_sent: Uint128::new(100000),
};
CHANNEL_STATE
.save(deps.as_mut().storage, (send_channel, native), &state)
.unwrap();

// run migration
migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap();

// check new channel state
let chan = query_channel(deps.as_ref(), send_channel.into()).unwrap();
assert_eq!(chan.balances, vec![Amount::native(50000, native)]);
assert_eq!(chan.total_sent, vec![Amount::native(114000, native)]);
}
}
27 changes: 6 additions & 21 deletions contracts/cw20-ics20/src/ibc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use cosmwasm_std::{
use crate::amount::Amount;
use crate::error::{ContractError, Never};
use crate::state::{
increase_channel_balance, reduce_channel_balance, undo_reduce_channel_balance, ChannelInfo,
ReplyArgs, ALLOW_LIST, CHANNEL_INFO, REPLY_ARGS,
reduce_channel_balance, undo_reduce_channel_balance, ChannelInfo, ReplyArgs, ALLOW_LIST,
CHANNEL_INFO, REPLY_ARGS,
};
use cw20::Cw20ExecuteMsg;

Expand All @@ -32,20 +32,15 @@ pub struct Ics20Packet {
pub receiver: String,
/// the sender address
pub sender: String,
/// used only by us to control ack handling
pub v: Option<u32>,
}

const V2: u32 = 2;

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(),
v: Some(V2),
}
}

Expand Down Expand Up @@ -317,15 +312,9 @@ pub fn ibc_packet_timeout(
}

// update the balance stored on this (channel, denom) index
fn on_packet_success(deps: DepsMut, packet: IbcPacket) -> Result<IbcBasicResponse, ContractError> {
fn on_packet_success(_deps: DepsMut, packet: IbcPacket) -> Result<IbcBasicResponse, ContractError> {
let msg: Ics20Packet = from_binary(&packet.data)?;

// if this was for an older (pre-v2) packet we send continue with old behavior
// (this is needed for transitioning on a system with pending packet)
if msg.v.is_none() {
increase_channel_balance(deps.storage, &packet.src.channel_id, &msg.denom, msg.amount)?;
}

// similar event messages like ibctransfer module
let attributes = vec![
attr("action", "acknowledge"),
Expand All @@ -347,10 +336,8 @@ fn on_packet_failure(
) -> Result<IbcBasicResponse, ContractError> {
let msg: Ics20Packet = from_binary(&packet.data)?;

// undo the balance update (but not for pre-v2/None packets which didn't add before sending)
if msg.v.is_some() {
reduce_channel_balance(deps.storage, &packet.src.channel_id, &msg.denom, msg.amount)?;
}
// undo the balance update on failure (as we pre-emptively added it on send)
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)?;
Expand Down Expand Up @@ -426,7 +413,7 @@ mod test {
"wasm1fucynrfkrt684pm8jrt8la5h2csvs5cnldcgqc",
);
// Example message generated from the SDK
let expected = r#"{"amount":"12345","denom":"ucosm","receiver":"wasm1fucynrfkrt684pm8jrt8la5h2csvs5cnldcgqc","sender":"cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n","v":2}"#;
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());
Expand Down Expand Up @@ -474,7 +461,6 @@ mod test {
amount: amount.into(),
sender: "remote-sender".to_string(),
receiver: receiver.to_string(),
v: Some(V2),
};
print!("Packet denom: {}", &data.denom);
IbcPacket::new(
Expand Down Expand Up @@ -535,7 +521,6 @@ mod test {
amount: Uint128::new(987654321),
sender: "local-sender".to_string(),
receiver: "remote-rcpt".to_string(),
v: Some(V2),
};
let timeout = mock_env().block.time.plus_seconds(DEFAULT_TIMEOUT);
assert_eq!(
Expand Down
71 changes: 71 additions & 0 deletions contracts/cw20-ics20/src/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,74 @@ pub mod v1 {

pub const CONFIG: Item<Config> = Item::new("ics20_config");
}

// v2 format is anything older than 0.13.1 when we only updated the internal balances on success ack
pub mod v2 {
use crate::amount::Amount;
use crate::state::{ChannelState, CHANNEL_INFO, CHANNEL_STATE};
use crate::ContractError;
use cosmwasm_std::{to_binary, Addr, DepsMut, Env, Order, StdResult, WasmQuery};
use cw20::{BalanceResponse, Cw20QueryMsg};

pub fn update_balances(mut deps: DepsMut, env: &Env) -> Result<(), ContractError> {
let channels = CHANNEL_INFO
.keys(deps.storage, None, None, Order::Ascending)
.collect::<StdResult<Vec<_>>>()?;
match channels.len() {
0 => Ok(()),
1 => {
let channel = &channels[0];
let addr = &env.contract.address;
let states = CHANNEL_STATE
.prefix(channel)
.range(deps.storage, None, None, Order::Ascending)
.collect::<StdResult<Vec<_>>>()?;
for (denom, state) in states.into_iter() {
update_denom(deps.branch(), addr, channel, denom, state)?;
}
Ok(())
}
_ => Err(ContractError::CannotMigrate {
previous_contract: "multiple channels open".into(),
}),
}
}

fn update_denom(
deps: DepsMut,
contract: &Addr,
channel: &str,
denom: String,
mut state: ChannelState,
) -> StdResult<()> {
// handle this for both native and cw20
let balance = match Amount::from_parts(denom.clone(), state.outstanding) {
Amount::Native(coin) => deps.querier.query_balance(contract, coin.denom)?.amount,
Amount::Cw20(coin) => {
// FIXME: we should be able to do this with the following line, but QuerierWrapper doesn't play
// with the Querier generics
// `Cw20Contract(contract.clone()).balance(&deps.querier, contract)?`
let query = WasmQuery::Smart {
contract_addr: coin.address,
msg: to_binary(&Cw20QueryMsg::Balance {
address: contract.into(),
})?,
};
let res: BalanceResponse = deps.querier.query(&query.into())?;
res.balance
}
};

// this checks if we have received some coins that are "in flight" and not yet accounted in the state
let diff = balance - state.outstanding;
// if they are in flight, we add them to the internal state now, as if we added them when sent (not when acked)
// to match the current logic
if !diff.is_zero() {
state.outstanding += diff;
state.total_sent += diff;
CHANNEL_STATE.save(deps.storage, (channel, &denom), &state)?;
}

Ok(())
}
}