diff --git a/Cargo.lock b/Cargo.lock index e1dc37ef6d..2f192441a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5277,7 +5277,6 @@ dependencies = [ "penumbra-keys", "penumbra-num", "penumbra-proto", - "penumbra-shielded-pool", "penumbra-storage", "prost 0.12.1", "serde", @@ -5561,12 +5560,14 @@ dependencies = [ "decaf377-ka", "decaf377-rdsa", "hex", + "ibc-types", "im", "metrics", "once_cell", "penumbra-asset", "penumbra-chain", "penumbra-component", + "penumbra-ibc", "penumbra-keys", "penumbra-num", "penumbra-proof-params", @@ -5576,9 +5577,11 @@ dependencies = [ "penumbra-tct", "poseidon377", "proptest", + "prost 0.12.1", "rand 0.8.5", "rand_core 0.6.4", "serde", + "serde_json", "tendermint", "thiserror", "tonic", diff --git a/crates/bin/pcli/src/command/tx.rs b/crates/bin/pcli/src/command/tx.rs index 5fcd611525..1e6e9c2aac 100644 --- a/crates/bin/pcli/src/command/tx.rs +++ b/crates/bin/pcli/src/command/tx.rs @@ -18,7 +18,6 @@ use penumbra_asset::{asset, asset::DenomMetadata, Value, STAKING_TOKEN_ASSET_ID} use penumbra_dex::{lp::position, swap_claim::SwapClaimPlan}; use penumbra_fee::Fee; use penumbra_governance::{proposal::ProposalToml, Vote}; -use penumbra_ibc::Ics20Withdrawal; use penumbra_keys::keys::AddressIndex; use penumbra_num::Amount; use penumbra_proto::{ @@ -42,6 +41,7 @@ use penumbra_proto::{ }, view::v1alpha1::GasPricesRequest, }; +use penumbra_shielded_pool::Ics20Withdrawal; use penumbra_stake::rate::RateData; use penumbra_stake::{DelegationToken, IdentityKey, Penalty, UnbondingToken, UndelegateClaimPlan}; use penumbra_transaction::{gas::swap_claim_gas_cost, memo::MemoPlaintext}; diff --git a/crates/core/app/src/action_handler/actions.rs b/crates/core/app/src/action_handler/actions.rs index d82124aabd..344da024bb 100644 --- a/crates/core/app/src/action_handler/actions.rs +++ b/crates/core/app/src/action_handler/actions.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use anyhow::Result; use async_trait::async_trait; use penumbra_chain::TransactionContext; -use penumbra_ibc::component::transfer::Ics20Transfer; use penumbra_ibc::component::StateReadExt as _; +use penumbra_shielded_pool::component::Ics20Transfer; use penumbra_storage::{StateRead, StateWrite}; use penumbra_transaction::Action; diff --git a/crates/core/component/ibc/Cargo.toml b/crates/core/component/ibc/Cargo.toml index 01775392aa..1fa43e6a8a 100644 --- a/crates/core/component/ibc/Cargo.toml +++ b/crates/core/component/ibc/Cargo.toml @@ -11,7 +11,7 @@ component = [ "penumbra-storage", "penumbra-proto/penumbra-storage", "penumbra-chain/component", - "penumbra-shielded-pool/component", + #"penumbra-shielded-pool/component", ] default = ["component", "std"] std = ["ibc-types/std"] @@ -24,7 +24,7 @@ penumbra-proto = { path = "../../../proto", default-features = false } penumbra-storage = { path = "../../../storage", optional = true } penumbra-component = { path = "../component", optional = true } penumbra-chain = { path = "../chain", default-features = false } -penumbra-shielded-pool = { path = "../shielded-pool", default-features = false } +# penumbra-shielded-pool = { path = "../shielded-pool", default-features = false } penumbra-asset = { path = "../../../core/asset", default-features = false } penumbra-num = { path = "../../../core/num", default-features = false } penumbra-keys = { path = "../../../core/keys", default-features = false } diff --git a/crates/core/component/ibc/src/component.rs b/crates/core/component/ibc/src/component.rs index c7dd9bfe77..c557477981 100644 --- a/crates/core/component/ibc/src/component.rs +++ b/crates/core/component/ibc/src/component.rs @@ -12,11 +12,11 @@ pub mod rpc; mod ibc_component; mod metrics; mod msg_handler; -mod packet; +pub mod packet; mod proof_verification; -mod state_key; +pub mod state_key; // TODO: move this to the shielded pool crate -pub mod transfer; +// pub mod transfer; mod view; use msg_handler::MsgHandler; diff --git a/crates/core/component/ibc/src/component/action_handler.rs b/crates/core/component/ibc/src/component/action_handler.rs index 2012fdab34..3c3334cebd 100644 --- a/crates/core/component/ibc/src/component/action_handler.rs +++ b/crates/core/component/ibc/src/component/action_handler.rs @@ -1,2 +1,2 @@ mod ibc_action; -mod ics20_withdrawal; +//mod ics20_withdrawal; diff --git a/crates/core/component/ibc/src/component/action_handler/ics20_withdrawal.rs b/crates/core/component/ibc/src/component/action_handler/ics20_withdrawal.rs index 37a1186b35..97d2804d61 100644 --- a/crates/core/component/ibc/src/component/action_handler/ics20_withdrawal.rs +++ b/crates/core/component/ibc/src/component/action_handler/ics20_withdrawal.rs @@ -1,29 +1,29 @@ -use std::sync::Arc; +// use std::sync::Arc; -use anyhow::Result; -use async_trait::async_trait; -use penumbra_component::ActionHandler; -use penumbra_storage::{StateRead, StateWrite}; +// use anyhow::Result; +// use async_trait::async_trait; +// use penumbra_component::ActionHandler; +// use penumbra_storage::{StateRead, StateWrite}; -use crate::{ - component::transfer::{Ics20TransferReadExt as _, Ics20TransferWriteExt as _}, - Ics20Withdrawal, -}; +// use crate::{ +// component::transfer::{Ics20TransferReadExt as _, Ics20TransferWriteExt as _}, +// Ics20Withdrawal, +// }; -#[async_trait] -impl ActionHandler for Ics20Withdrawal { - type CheckStatelessContext = (); - async fn check_stateless(&self, _context: ()) -> Result<()> { - self.validate() - } +// #[async_trait] +// impl ActionHandler for Ics20Withdrawal { +// type CheckStatelessContext = (); +// async fn check_stateless(&self, _context: ()) -> Result<()> { +// self.validate() +// } - async fn check_stateful(&self, state: Arc) -> Result<()> { - state.withdrawal_check(self).await - } +// async fn check_stateful(&self, state: Arc) -> Result<()> { +// state.withdrawal_check(self).await +// } - async fn execute(&self, mut state: S) -> Result<()> { - state.withdrawal_execute(self).await; +// async fn execute(&self, mut state: S) -> Result<()> { +// state.withdrawal_execute(self).await; - Ok(()) - } -} +// Ok(()) +// } +// } diff --git a/crates/core/component/ibc/src/component/client.rs b/crates/core/component/ibc/src/component/client.rs index 943b0fb879..7c223a4c69 100644 --- a/crates/core/component/ibc/src/component/client.rs +++ b/crates/core/component/ibc/src/component/client.rs @@ -366,7 +366,84 @@ mod tests { use crate::ibc_action::IbcActionWithHandler; use crate::IbcAction; - use crate::component::transfer::Ics20Transfer; + use crate::component::app_handler::{AppHandler, AppHandlerCheck, AppHandlerExecute}; + use ibc_types::core::channel::msgs::{ + MsgAcknowledgement, MsgChannelCloseConfirm, MsgChannelCloseInit, MsgChannelOpenAck, + MsgChannelOpenConfirm, MsgChannelOpenInit, MsgChannelOpenTry, MsgRecvPacket, MsgTimeout, + }; + + struct MockAppHandler {} + + #[async_trait] + impl AppHandlerCheck for MockAppHandler { + async fn chan_open_init_check( + state: S, + msg: &MsgChannelOpenInit, + ) -> Result<()> { + Ok(()) + } + async fn chan_open_try_check( + state: S, + msg: &MsgChannelOpenTry, + ) -> Result<()> { + Ok(()) + } + async fn chan_open_ack_check( + state: S, + msg: &MsgChannelOpenAck, + ) -> Result<()> { + Ok(()) + } + async fn chan_open_confirm_check( + state: S, + msg: &MsgChannelOpenConfirm, + ) -> Result<()> { + Ok(()) + } + async fn chan_close_confirm_check( + state: S, + msg: &MsgChannelCloseConfirm, + ) -> Result<()> { + Ok(()) + } + async fn chan_close_init_check( + state: S, + msg: &MsgChannelCloseInit, + ) -> Result<()> { + Ok(()) + } + + async fn recv_packet_check(state: S, msg: &MsgRecvPacket) -> Result<()> { + Ok(()) + } + async fn timeout_packet_check(state: S, msg: &MsgTimeout) -> Result<()> { + Ok(()) + } + async fn acknowledge_packet_check( + state: S, + msg: &MsgAcknowledgement, + ) -> Result<()> { + Ok(()) + } + } + + #[async_trait] + impl AppHandlerExecute for MockAppHandler { + async fn chan_open_init_execute(state: S, msg: &MsgChannelOpenInit) {} + async fn chan_open_try_execute(state: S, msg: &MsgChannelOpenTry) {} + async fn chan_open_ack_execute(state: S, msg: &MsgChannelOpenAck) {} + async fn chan_open_confirm_execute(state: S, msg: &MsgChannelOpenConfirm) {} + async fn chan_close_confirm_execute(state: S, msg: &MsgChannelCloseConfirm) { + } + async fn chan_close_init_execute(state: S, msg: &MsgChannelCloseInit) {} + + async fn recv_packet_execute(state: S, msg: &MsgRecvPacket) {} + async fn timeout_packet_execute(state: S, msg: &MsgTimeout) {} + async fn acknowledge_packet_execute(state: S, msg: &MsgAcknowledgement) {} + } + + #[async_trait] + impl AppHandler for MockAppHandler {} // test that we can create and update a light client. #[tokio::test] @@ -436,10 +513,10 @@ mod tests { msg_update_stargaze_client.client_id = ClientId::from_str("07-tendermint-0").unwrap(); - let create_client_action = IbcActionWithHandler::::new( + let create_client_action = IbcActionWithHandler::::new( IbcAction::CreateClient(msg_create_stargaze_client), ); - let update_client_action = IbcActionWithHandler::::new( + let update_client_action = IbcActionWithHandler::::new( IbcAction::UpdateClient(msg_update_stargaze_client), ); @@ -467,7 +544,7 @@ mod tests { let mut second_update = MsgUpdateClient::decode(msg_update_second.as_slice()).unwrap(); second_update.client_id = ClientId::from_str("07-tendermint-0").unwrap(); let second_update_client_action = - IbcActionWithHandler::::new(IbcAction::UpdateClient(second_update)); + IbcActionWithHandler::::new(IbcAction::UpdateClient(second_update)); second_update_client_action.check_stateless(()).await?; second_update_client_action diff --git a/crates/core/component/ibc/src/component/packet.rs b/crates/core/component/ibc/src/component/packet.rs index 84db18189a..570e6c317e 100644 --- a/crates/core/component/ibc/src/component/packet.rs +++ b/crates/core/component/ibc/src/component/packet.rs @@ -6,13 +6,10 @@ use ibc_types::core::{ }; use penumbra_storage::{StateRead, StateWrite}; -use crate::{ - component::{ - channel::{StateReadExt as _, StateWriteExt as _}, - client::StateReadExt as _, - connection::StateReadExt as _, - }, - Ics20Withdrawal, +use crate::component::{ + channel::{StateReadExt as _, StateWriteExt as _}, + client::StateReadExt as _, + connection::StateReadExt as _, }; pub trait CheckStatus: private::Sealed {} @@ -44,6 +41,23 @@ pub struct IBCPacket { } impl IBCPacket { + pub fn new( + source_port: PortId, + source_channel: ChannelId, + timeout_height: Height, + timeout_timestamp: u64, + data: Vec, + ) -> Self { + Self { + source_port, + source_channel, + timeout_height, + timeout_timestamp, + data, + m: std::marker::PhantomData, + } + } + pub fn assume_checked(self) -> IBCPacket { IBCPacket { source_port: self.source_port, @@ -56,19 +70,19 @@ impl IBCPacket { } } -impl From for IBCPacket { - fn from(withdrawal: Ics20Withdrawal) -> Self { - Self { - source_port: PortId::transfer(), - source_channel: withdrawal.source_channel.clone(), - timeout_height: withdrawal.timeout_height, - timeout_timestamp: withdrawal.timeout_time, - data: withdrawal.packet_data(), - - m: std::marker::PhantomData, - } - } -} +// impl From for IBCPacket { +// fn from(withdrawal: Ics20Withdrawal) -> Self { +// Self { +// source_port: PortId::transfer(), +// source_channel: withdrawal.source_channel.clone(), +// timeout_height: withdrawal.timeout_height, +// timeout_timestamp: withdrawal.timeout_time, +// data: withdrawal.packet_data(), + +// m: std::marker::PhantomData, +// } +// } +// } #[async_trait] pub trait SendPacketRead: StateRead { diff --git a/crates/core/component/ibc/src/component/transfer.rs b/crates/core/component/ibc/src/component/transfer.rs index 052cebb589..6ce6b4b0d5 100644 --- a/crates/core/component/ibc/src/component/transfer.rs +++ b/crates/core/component/ibc/src/component/transfer.rs @@ -1,451 +1,451 @@ -use std::str::FromStr; - -use anyhow::{Context, Result}; -use async_trait::async_trait; -use ibc_types::{ - core::channel::{ - channel::Order as ChannelOrder, - msgs::{ - MsgAcknowledgement, MsgChannelCloseConfirm, MsgChannelCloseInit, MsgChannelOpenAck, - MsgChannelOpenConfirm, MsgChannelOpenInit, MsgChannelOpenTry, MsgRecvPacket, - MsgTimeout, - }, - ChannelId, PortId, Version, - }, - transfer::acknowledgement::TokenTransferAcknowledgement, -}; -use penumbra_asset::{asset, asset::DenomMetadata, Value}; -use penumbra_keys::Address; -use penumbra_num::Amount; -use penumbra_proto::{ - penumbra::core::component::ibc::v1alpha1::FungibleTokenPacketData, StateReadProto, - StateWriteProto, -}; -use penumbra_shielded_pool::component::{NoteManager, SupplyWrite}; -use penumbra_storage::{StateRead, StateWrite}; -use prost::Message; - -use crate::{ - component::{ - app_handler::{AppHandler, AppHandlerCheck, AppHandlerExecute}, - packet::{ - IBCPacket, SendPacketRead as _, SendPacketWrite as _, Unchecked, - WriteAcknowledgement as _, - }, - state_key, - }, - Ics20Withdrawal, -}; - -// returns a bool indicating if the provided denom was issued locally or if it was bridged in. -// this logic is a bit tricky, and adapted from https://github.com/cosmos/ibc/tree/main/spec/app/ics-020-fungible-token-transfer (sendFungibleTokens). -// -// what we want to do is to determine if the denom being withdrawn is a native token (one -// that originates from Penumbra) or a bridged token (one that was sent into penumbra from -// IBC). -// -// A simple way of doing this is by parsing the denom, looking for a prefix that is only -// appended in the case of a bridged token. That is what this logic does. -fn is_source(source_port: &PortId, source_channel: &ChannelId, denom: &DenomMetadata) -> bool { - let prefix = format!("{source_port}/{source_channel}/"); - - denom.starts_with(&prefix) -} - -#[derive(Clone)] -pub struct Ics20Transfer {} - -#[async_trait] -pub trait Ics20TransferReadExt: StateRead { - async fn withdrawal_check(&self, withdrawal: &Ics20Withdrawal) -> Result<()> { - // create packet - let packet: IBCPacket = withdrawal.clone().into(); - - // send packet - self.send_packet_check(packet).await?; - - Ok(()) - } -} - -impl Ics20TransferReadExt for T {} - -#[async_trait] -pub trait Ics20TransferWriteExt: StateWrite { - async fn withdrawal_execute(&mut self, withdrawal: &Ics20Withdrawal) { - // create packet, assume it's already checked since the component caller contract calls `check` before `execute` - let checked_packet = IBCPacket::::from(withdrawal.clone()).assume_checked(); - - let prefix = format!("transfer/{}/", &withdrawal.source_channel); - if !withdrawal.denom.starts_with(&prefix) { - // we are the source. add the value balance to the escrow channel. - let existing_value_balance: Amount = self - .get(&state_key::ics20_value_balance( - &withdrawal.source_channel, - &withdrawal.denom.id(), - )) - .await - .expect("able to retrieve value balance in ics20 withdrawal! (execute)") - .unwrap_or_else(Amount::zero); - - let new_value_balance = existing_value_balance + withdrawal.amount; - self.put( - state_key::ics20_value_balance(&withdrawal.source_channel, &withdrawal.denom.id()), - new_value_balance, - ); - } else { - // receiver is the source, burn utxos - - // NOTE: this burning should already be accomplished by the value balance check from - // the withdrawal's balance commitment, so nothing to do here. - // - - // update supply tracking of burned note - self.update_token_supply(&withdrawal.denom.id(), -(withdrawal.amount.value() as i128)) - .await - .expect("couldn't update token supply in ics20 withdrawal!"); - } - - self.send_packet_execute(checked_packet).await; - } -} - -impl Ics20TransferWriteExt for T {} - -// TODO: Ics20 implementation. -// see: https://github.com/cosmos/ibc/tree/master/spec/app/ics-020-fungible-token-transfer -// TODO (ava): add versioning to AppHandlers -#[async_trait] -impl AppHandlerCheck for Ics20Transfer { - async fn chan_open_init_check(_state: S, msg: &MsgChannelOpenInit) -> Result<()> { - if msg.ordering != ChannelOrder::Unordered { - anyhow::bail!("channel order must be unordered for Ics20 transfer"); - } - let ics20_version = Version::new("ics20-1".to_string()); - if msg.version_proposal != ics20_version { - anyhow::bail!("channel version must be ics20 for Ics20 transfer"); - } - - Ok(()) - } - - async fn chan_open_try_check(_state: S, msg: &MsgChannelOpenTry) -> Result<()> { - if msg.ordering != ChannelOrder::Unordered { - anyhow::bail!("channel order must be unordered for Ics20 transfer"); - } - let ics20_version = Version::new("ics20-1".to_string()); - - if msg.version_supported_on_a != ics20_version { - anyhow::bail!("counterparty version must be ics20-1 for Ics20 transfer"); - } - - Ok(()) - } - - async fn chan_open_ack_check(_state: S, msg: &MsgChannelOpenAck) -> Result<()> { - let ics20_version = Version::new("ics20-1".to_string()); - if msg.version_on_b != ics20_version { - anyhow::bail!("counterparty version must be ics20-1 for Ics20 transfer"); - } - - Ok(()) - } - - async fn chan_open_confirm_check( - _state: S, - _msg: &MsgChannelOpenConfirm, - ) -> Result<()> { - // accept channel confirmations, port has already been validated, version has already been validated - Ok(()) - } - - async fn chan_close_confirm_check( - _state: S, - _msg: &MsgChannelCloseConfirm, - ) -> Result<()> { - // no action necessary - Ok(()) - } - - async fn chan_close_init_check( - _state: S, - _msg: &MsgChannelCloseInit, - ) -> Result<()> { - // always abort transaction - anyhow::bail!("ics20 always aborts on close init"); - } - - async fn recv_packet_check(_state: S, _msg: &MsgRecvPacket) -> Result<()> { - // all checks on recv_packet done in execute - Ok(()) - } - - async fn timeout_packet_check(state: S, msg: &MsgTimeout) -> Result<()> { - let packet_data = FungibleTokenPacketData::decode(msg.packet.data.as_slice())?; - let denom: asset::DenomMetadata = packet_data.denom.as_str().try_into()?; - - if is_source(&msg.packet.port_on_a, &msg.packet.chan_on_a, &denom) { - // check if we have enough balance to refund tokens to sender - let value_balance: Amount = state - .get(&state_key::ics20_value_balance( - &msg.packet.chan_on_a, - &denom.id(), - )) - .await? - .unwrap_or_else(Amount::zero); - - let amount_penumbra: Amount = packet_data.amount.try_into()?; - if value_balance < amount_penumbra { - anyhow::bail!("insufficient balance to refund tokens to sender"); - } - } - - Ok(()) - } - - async fn acknowledge_packet_check( - _state: S, - _msg: &MsgAcknowledgement, - ) -> Result<()> { - Ok(()) - } -} - -// the main entry point for ICS20 transfer packet handling -async fn recv_transfer_packet_inner( - mut state: S, - msg: &MsgRecvPacket, -) -> Result<()> { - // parse if we are source or dest, and mint or burn accordingly - // - // see this part of the spec for this logic: - // - // https://github.com/cosmos/ibc/tree/main/spec/app/ics-020-fungible-token-transfer (onRecvPacket) - // - // NOTE: spec says proto but thsi is actualy JSON according to the ibc-go implementation - let packet_data: FungibleTokenPacketData = serde_json::from_slice(msg.packet.data.as_slice()) - .with_context(|| "failed to decode FTPD packet")?; - let denom: asset::DenomMetadata = packet_data - .denom - .as_str() - .try_into() - .context("couldnt decode denom in ICS20 transfer")?; - let receiver_amount: Amount = packet_data - .amount - .try_into() - .context("couldnt decode amount in ICS20 transfer")?; - let receiver_address = Address::from_str(&packet_data.receiver)?; - - // NOTE: here we assume we are chain A. - - // 2. check if we are the source chain for the denom. - if is_source(&msg.packet.port_on_a, &msg.packet.chan_on_a, &denom) { - // mint tokens to receiver in the amount of packet_data.amount in the denom of denom (with - // the source removed, since we're the source) - let prefix = format!( - "{source_port}/{source_chan}/", - source_port = msg.packet.port_on_a, - source_chan = msg.packet.chan_on_a - ); - - let unprefixed_denom: asset::DenomMetadata = packet_data - .denom - .replace(&prefix, "") - .as_str() - .try_into() - .context("couldnt decode denom in ICS20 transfer")?; - - let value: Value = Value { - amount: receiver_amount, - asset_id: unprefixed_denom.id(), - }; - - // assume AppHandlerCheck has already been called, and we have enough balance to mint tokens to receiver - // check if we have enough balance to unescrow tokens to receiver - let value_balance: Amount = state - .get(&state_key::ics20_value_balance( - &msg.packet.chan_on_b, - &unprefixed_denom.id(), - )) - .await? - .unwrap_or_else(Amount::zero); - - if value_balance < receiver_amount { - // error text here is from the ics20 spec - anyhow::bail!("transfer coins failed"); - } - - state - .mint_note( - value, - &receiver_address, - penumbra_chain::NoteSource::Ics20Transfer, // TODO - ) - .await - .context("unable to mint note when receiving ics20 transfer packet")?; - - // update the value balance - let value_balance: Amount = state - .get(&state_key::ics20_value_balance( - &msg.packet.chan_on_b, - &unprefixed_denom.id(), - )) - .await? - .unwrap_or_else(Amount::zero); - - // note: this arithmetic was checked above, but we do it again anyway. - let new_value_balance = value_balance - .checked_sub(&receiver_amount) - .context("underflow subtracing value balance in ics20 transfer")?; - state.put( - state_key::ics20_value_balance(&msg.packet.chan_on_b, &denom.id()), - new_value_balance, - ); - } else { - // create new denom: - // - // prefix = "{packet.destPort}/{packet.destChannel}/" - // prefixedDenomination = prefix + data.denom - // - // then mint that denom to packet_data.receiver in packet_data.amount - // no value balance to update here since this is an exogenous denom - let prefixed_denomination = format!( - "{}/{}/{}", - msg.packet.port_on_b, msg.packet.chan_on_b, packet_data.denom - ); - - let denom: asset::DenomMetadata = prefixed_denomination - .as_str() - .try_into() - .context("unable to parse denom in ics20 transfer as DenomMetadata")?; - state - .register_denom(&denom) - .await - .context("unable to register denom in ics20 transfer")?; - - let value = Value { - amount: receiver_amount, - asset_id: denom.id(), - }; - - state - .mint_note( - value, - &receiver_address, - penumbra_chain::NoteSource::Ics20Transfer, - ) - .await - .context("failed to mint notes in ibc transfer")?; - } - - Ok(()) -} - -// see: https://github.com/cosmos/ibc/blob/8326e26e7e1188b95c32481ff00348a705b23700/spec/app/ics-020-fungible-token-transfer/README.md?plain=1#L297 -async fn timeout_packet_inner(mut state: S, msg: &MsgTimeout) -> Result<()> { - let packet_data = FungibleTokenPacketData::decode(msg.packet.data.as_slice())?; - let denom: asset::DenomMetadata = packet_data // CRITICAL: verify that this denom is validated in upstream timeout handling - .denom - .as_str() - .try_into() - .context("couldn't decode denom in ics20 transfer timeout")?; - // receiver was source chain, mint vouchers back to sender - let amount: Amount = packet_data - .amount - .try_into() - .context("couldn't decode amount in ics20 transfer timeout")?; - - let receiver = Address::from_str(&packet_data.receiver) - .context("couldn't decode receiver address in ics20 timeout")?; - - let value: Value = Value { - amount, - asset_id: denom.id(), - }; - - if is_source(&msg.packet.port_on_a, &msg.packet.chan_on_a, &denom) { - // sender was source chain, unescrow tokens back to sender - let value_balance: Amount = state - .get(&state_key::ics20_value_balance( - &msg.packet.chan_on_a, - &denom.id(), - )) - .await? - .unwrap_or_else(Amount::zero); - - if value_balance < amount { - anyhow::bail!("couldn't return coins in timeout: not enough value balance"); - } - - state - .mint_note(value, &receiver, penumbra_chain::NoteSource::Ics20Transfer) - .await - .context("couldn't mint note in timeout_packet_inner")?; - - // update the value balance - let value_balance: Amount = state - .get(&state_key::ics20_value_balance( - &msg.packet.chan_on_a, - &denom.id(), - )) - .await? - .unwrap_or_else(Amount::zero); - - // note: this arithmetic was checked above, but we do it again anyway. - let new_value_balance = value_balance - .checked_sub(&amount) - .context("underflow in ics20 timeout packet value balance subtraction")?; - state.put( - state_key::ics20_value_balance(&msg.packet.chan_on_a, &denom.id()), - new_value_balance, - ); - } else { - state - .mint_note(value, &receiver, penumbra_chain::NoteSource::Ics20Transfer) // NOTE: should this be Ics20TransferTimeout? - .await - .context("failed to mint return voucher in ics20 transfer timeout")?; - } - - Ok(()) -} - -// NOTE: should these be fallible, now that our enclosing state machine is fallible in execution? -#[async_trait] -impl AppHandlerExecute for Ics20Transfer { - async fn chan_open_init_execute(_state: S, _msg: &MsgChannelOpenInit) {} - async fn chan_open_try_execute(_state: S, _msg: &MsgChannelOpenTry) {} - async fn chan_open_ack_execute(_state: S, _msg: &MsgChannelOpenAck) {} - async fn chan_open_confirm_execute(_state: S, _msg: &MsgChannelOpenConfirm) {} - async fn chan_close_confirm_execute(_state: S, _msg: &MsgChannelCloseConfirm) {} - async fn chan_close_init_execute(_state: S, _msg: &MsgChannelCloseInit) {} - async fn recv_packet_execute(mut state: S, msg: &MsgRecvPacket) { - // recv packet should never fail a transaction, but it should record a failure acknowledgement. - let ack: Vec = match recv_transfer_packet_inner(&mut state, msg).await { - Ok(_) => { - // record packet acknowledgement without error - TokenTransferAcknowledgement::success().into() - } - Err(e) => { - tracing::debug!("couldnt execute transfer: {:#}", e); - // record packet acknowledgement with error - TokenTransferAcknowledgement::Error(e.to_string()).into() - } - }; - - state - .write_acknowledgement(&msg.packet, &ack) - .await - .expect("able to write acknowledgement"); - } - - async fn timeout_packet_execute(mut state: S, msg: &MsgTimeout) { - // timeouts should never fail - timeout_packet_inner(&mut state, msg) - .await - .expect("able to timeout packet"); - } - - async fn acknowledge_packet_execute(_state: S, _msg: &MsgAcknowledgement) {} -} - -impl AppHandler for Ics20Transfer {} +// use std::str::FromStr; + +// use anyhow::{Context, Result}; +// use async_trait::async_trait; +// use ibc_types::{ +// core::channel::{ +// channel::Order as ChannelOrder, +// msgs::{ +// MsgAcknowledgement, MsgChannelCloseConfirm, MsgChannelCloseInit, MsgChannelOpenAck, +// MsgChannelOpenConfirm, MsgChannelOpenInit, MsgChannelOpenTry, MsgRecvPacket, +// MsgTimeout, +// }, +// ChannelId, PortId, Version, +// }, +// transfer::acknowledgement::TokenTransferAcknowledgement, +// }; +// use penumbra_asset::{asset, asset::DenomMetadata, Value}; +// use penumbra_keys::Address; +// use penumbra_num::Amount; +// use penumbra_proto::{ +// penumbra::core::component::ibc::v1alpha1::FungibleTokenPacketData, StateReadProto, +// StateWriteProto, +// }; +// use penumbra_shielded_pool::component::{NoteManager, SupplyWrite}; +// use penumbra_storage::{StateRead, StateWrite}; +// use prost::Message; + +// use crate::{ +// component::{ +// app_handler::{AppHandler, AppHandlerCheck, AppHandlerExecute}, +// packet::{ +// IBCPacket, SendPacketRead as _, SendPacketWrite as _, Unchecked, +// WriteAcknowledgement as _, +// }, +// state_key, +// }, +// Ics20Withdrawal, +// }; + +// // returns a bool indicating if the provided denom was issued locally or if it was bridged in. +// // this logic is a bit tricky, and adapted from https://github.com/cosmos/ibc/tree/main/spec/app/ics-020-fungible-token-transfer (sendFungibleTokens). +// // +// // what we want to do is to determine if the denom being withdrawn is a native token (one +// // that originates from Penumbra) or a bridged token (one that was sent into penumbra from +// // IBC). +// // +// // A simple way of doing this is by parsing the denom, looking for a prefix that is only +// // appended in the case of a bridged token. That is what this logic does. +// fn is_source(source_port: &PortId, source_channel: &ChannelId, denom: &DenomMetadata) -> bool { +// let prefix = format!("{source_port}/{source_channel}/"); + +// denom.starts_with(&prefix) +// } + +// #[derive(Clone)] +// pub struct Ics20Transfer {} + +// #[async_trait] +// pub trait Ics20TransferReadExt: StateRead { +// async fn withdrawal_check(&self, withdrawal: &Ics20Withdrawal) -> Result<()> { +// // create packet +// let packet: IBCPacket = withdrawal.clone().into(); + +// // send packet +// self.send_packet_check(packet).await?; + +// Ok(()) +// } +// } + +// impl Ics20TransferReadExt for T {} + +// #[async_trait] +// pub trait Ics20TransferWriteExt: StateWrite { +// async fn withdrawal_execute(&mut self, withdrawal: &Ics20Withdrawal) { +// // create packet, assume it's already checked since the component caller contract calls `check` before `execute` +// let checked_packet = IBCPacket::::from(withdrawal.clone()).assume_checked(); + +// let prefix = format!("transfer/{}/", &withdrawal.source_channel); +// if !withdrawal.denom.starts_with(&prefix) { +// // we are the source. add the value balance to the escrow channel. +// let existing_value_balance: Amount = self +// .get(&state_key::ics20_value_balance( +// &withdrawal.source_channel, +// &withdrawal.denom.id(), +// )) +// .await +// .expect("able to retrieve value balance in ics20 withdrawal! (execute)") +// .unwrap_or_else(Amount::zero); + +// let new_value_balance = existing_value_balance + withdrawal.amount; +// self.put( +// state_key::ics20_value_balance(&withdrawal.source_channel, &withdrawal.denom.id()), +// new_value_balance, +// ); +// } else { +// // receiver is the source, burn utxos + +// // NOTE: this burning should already be accomplished by the value balance check from +// // the withdrawal's balance commitment, so nothing to do here. +// // + +// // update supply tracking of burned note +// self.update_token_supply(&withdrawal.denom.id(), -(withdrawal.amount.value() as i128)) +// .await +// .expect("couldn't update token supply in ics20 withdrawal!"); +// } + +// self.send_packet_execute(checked_packet).await; +// } +// } + +// impl Ics20TransferWriteExt for T {} + +// // TODO: Ics20 implementation. +// // see: https://github.com/cosmos/ibc/tree/master/spec/app/ics-020-fungible-token-transfer +// // TODO (ava): add versioning to AppHandlers +// #[async_trait] +// impl AppHandlerCheck for Ics20Transfer { +// async fn chan_open_init_check(_state: S, msg: &MsgChannelOpenInit) -> Result<()> { +// if msg.ordering != ChannelOrder::Unordered { +// anyhow::bail!("channel order must be unordered for Ics20 transfer"); +// } +// let ics20_version = Version::new("ics20-1".to_string()); +// if msg.version_proposal != ics20_version { +// anyhow::bail!("channel version must be ics20 for Ics20 transfer"); +// } + +// Ok(()) +// } + +// async fn chan_open_try_check(_state: S, msg: &MsgChannelOpenTry) -> Result<()> { +// if msg.ordering != ChannelOrder::Unordered { +// anyhow::bail!("channel order must be unordered for Ics20 transfer"); +// } +// let ics20_version = Version::new("ics20-1".to_string()); + +// if msg.version_supported_on_a != ics20_version { +// anyhow::bail!("counterparty version must be ics20-1 for Ics20 transfer"); +// } + +// Ok(()) +// } + +// async fn chan_open_ack_check(_state: S, msg: &MsgChannelOpenAck) -> Result<()> { +// let ics20_version = Version::new("ics20-1".to_string()); +// if msg.version_on_b != ics20_version { +// anyhow::bail!("counterparty version must be ics20-1 for Ics20 transfer"); +// } + +// Ok(()) +// } + +// async fn chan_open_confirm_check( +// _state: S, +// _msg: &MsgChannelOpenConfirm, +// ) -> Result<()> { +// // accept channel confirmations, port has already been validated, version has already been validated +// Ok(()) +// } + +// async fn chan_close_confirm_check( +// _state: S, +// _msg: &MsgChannelCloseConfirm, +// ) -> Result<()> { +// // no action necessary +// Ok(()) +// } + +// async fn chan_close_init_check( +// _state: S, +// _msg: &MsgChannelCloseInit, +// ) -> Result<()> { +// // always abort transaction +// anyhow::bail!("ics20 always aborts on close init"); +// } + +// async fn recv_packet_check(_state: S, _msg: &MsgRecvPacket) -> Result<()> { +// // all checks on recv_packet done in execute +// Ok(()) +// } + +// async fn timeout_packet_check(state: S, msg: &MsgTimeout) -> Result<()> { +// let packet_data = FungibleTokenPacketData::decode(msg.packet.data.as_slice())?; +// let denom: asset::DenomMetadata = packet_data.denom.as_str().try_into()?; + +// if is_source(&msg.packet.port_on_a, &msg.packet.chan_on_a, &denom) { +// // check if we have enough balance to refund tokens to sender +// let value_balance: Amount = state +// .get(&state_key::ics20_value_balance( +// &msg.packet.chan_on_a, +// &denom.id(), +// )) +// .await? +// .unwrap_or_else(Amount::zero); + +// let amount_penumbra: Amount = packet_data.amount.try_into()?; +// if value_balance < amount_penumbra { +// anyhow::bail!("insufficient balance to refund tokens to sender"); +// } +// } + +// Ok(()) +// } + +// async fn acknowledge_packet_check( +// _state: S, +// _msg: &MsgAcknowledgement, +// ) -> Result<()> { +// Ok(()) +// } +// } + +// // the main entry point for ICS20 transfer packet handling +// async fn recv_transfer_packet_inner( +// mut state: S, +// msg: &MsgRecvPacket, +// ) -> Result<()> { +// // parse if we are source or dest, and mint or burn accordingly +// // +// // see this part of the spec for this logic: +// // +// // https://github.com/cosmos/ibc/tree/main/spec/app/ics-020-fungible-token-transfer (onRecvPacket) +// // +// // NOTE: spec says proto but thsi is actualy JSON according to the ibc-go implementation +// let packet_data: FungibleTokenPacketData = serde_json::from_slice(msg.packet.data.as_slice()) +// .with_context(|| "failed to decode FTPD packet")?; +// let denom: asset::DenomMetadata = packet_data +// .denom +// .as_str() +// .try_into() +// .context("couldnt decode denom in ICS20 transfer")?; +// let receiver_amount: Amount = packet_data +// .amount +// .try_into() +// .context("couldnt decode amount in ICS20 transfer")?; +// let receiver_address = Address::from_str(&packet_data.receiver)?; + +// // NOTE: here we assume we are chain A. + +// // 2. check if we are the source chain for the denom. +// if is_source(&msg.packet.port_on_a, &msg.packet.chan_on_a, &denom) { +// // mint tokens to receiver in the amount of packet_data.amount in the denom of denom (with +// // the source removed, since we're the source) +// let prefix = format!( +// "{source_port}/{source_chan}/", +// source_port = msg.packet.port_on_a, +// source_chan = msg.packet.chan_on_a +// ); + +// let unprefixed_denom: asset::DenomMetadata = packet_data +// .denom +// .replace(&prefix, "") +// .as_str() +// .try_into() +// .context("couldnt decode denom in ICS20 transfer")?; + +// let value: Value = Value { +// amount: receiver_amount, +// asset_id: unprefixed_denom.id(), +// }; + +// // assume AppHandlerCheck has already been called, and we have enough balance to mint tokens to receiver +// // check if we have enough balance to unescrow tokens to receiver +// let value_balance: Amount = state +// .get(&state_key::ics20_value_balance( +// &msg.packet.chan_on_b, +// &unprefixed_denom.id(), +// )) +// .await? +// .unwrap_or_else(Amount::zero); + +// if value_balance < receiver_amount { +// // error text here is from the ics20 spec +// anyhow::bail!("transfer coins failed"); +// } + +// state +// .mint_note( +// value, +// &receiver_address, +// penumbra_chain::NoteSource::Ics20Transfer, // TODO +// ) +// .await +// .context("unable to mint note when receiving ics20 transfer packet")?; + +// // update the value balance +// let value_balance: Amount = state +// .get(&state_key::ics20_value_balance( +// &msg.packet.chan_on_b, +// &unprefixed_denom.id(), +// )) +// .await? +// .unwrap_or_else(Amount::zero); + +// // note: this arithmetic was checked above, but we do it again anyway. +// let new_value_balance = value_balance +// .checked_sub(&receiver_amount) +// .context("underflow subtracing value balance in ics20 transfer")?; +// state.put( +// state_key::ics20_value_balance(&msg.packet.chan_on_b, &denom.id()), +// new_value_balance, +// ); +// } else { +// // create new denom: +// // +// // prefix = "{packet.destPort}/{packet.destChannel}/" +// // prefixedDenomination = prefix + data.denom +// // +// // then mint that denom to packet_data.receiver in packet_data.amount +// // no value balance to update here since this is an exogenous denom +// let prefixed_denomination = format!( +// "{}/{}/{}", +// msg.packet.port_on_b, msg.packet.chan_on_b, packet_data.denom +// ); + +// let denom: asset::DenomMetadata = prefixed_denomination +// .as_str() +// .try_into() +// .context("unable to parse denom in ics20 transfer as DenomMetadata")?; +// state +// .register_denom(&denom) +// .await +// .context("unable to register denom in ics20 transfer")?; + +// let value = Value { +// amount: receiver_amount, +// asset_id: denom.id(), +// }; + +// state +// .mint_note( +// value, +// &receiver_address, +// penumbra_chain::NoteSource::Ics20Transfer, +// ) +// .await +// .context("failed to mint notes in ibc transfer")?; +// } + +// Ok(()) +// } + +// // see: https://github.com/cosmos/ibc/blob/8326e26e7e1188b95c32481ff00348a705b23700/spec/app/ics-020-fungible-token-transfer/README.md?plain=1#L297 +// async fn timeout_packet_inner(mut state: S, msg: &MsgTimeout) -> Result<()> { +// let packet_data = FungibleTokenPacketData::decode(msg.packet.data.as_slice())?; +// let denom: asset::DenomMetadata = packet_data // CRITICAL: verify that this denom is validated in upstream timeout handling +// .denom +// .as_str() +// .try_into() +// .context("couldn't decode denom in ics20 transfer timeout")?; +// // receiver was source chain, mint vouchers back to sender +// let amount: Amount = packet_data +// .amount +// .try_into() +// .context("couldn't decode amount in ics20 transfer timeout")?; + +// let receiver = Address::from_str(&packet_data.receiver) +// .context("couldn't decode receiver address in ics20 timeout")?; + +// let value: Value = Value { +// amount, +// asset_id: denom.id(), +// }; + +// if is_source(&msg.packet.port_on_a, &msg.packet.chan_on_a, &denom) { +// // sender was source chain, unescrow tokens back to sender +// let value_balance: Amount = state +// .get(&state_key::ics20_value_balance( +// &msg.packet.chan_on_a, +// &denom.id(), +// )) +// .await? +// .unwrap_or_else(Amount::zero); + +// if value_balance < amount { +// anyhow::bail!("couldn't return coins in timeout: not enough value balance"); +// } + +// state +// .mint_note(value, &receiver, penumbra_chain::NoteSource::Ics20Transfer) +// .await +// .context("couldn't mint note in timeout_packet_inner")?; + +// // update the value balance +// let value_balance: Amount = state +// .get(&state_key::ics20_value_balance( +// &msg.packet.chan_on_a, +// &denom.id(), +// )) +// .await? +// .unwrap_or_else(Amount::zero); + +// // note: this arithmetic was checked above, but we do it again anyway. +// let new_value_balance = value_balance +// .checked_sub(&amount) +// .context("underflow in ics20 timeout packet value balance subtraction")?; +// state.put( +// state_key::ics20_value_balance(&msg.packet.chan_on_a, &denom.id()), +// new_value_balance, +// ); +// } else { +// state +// .mint_note(value, &receiver, penumbra_chain::NoteSource::Ics20Transfer) // NOTE: should this be Ics20TransferTimeout? +// .await +// .context("failed to mint return voucher in ics20 transfer timeout")?; +// } + +// Ok(()) +// } + +// // NOTE: should these be fallible, now that our enclosing state machine is fallible in execution? +// #[async_trait] +// impl AppHandlerExecute for Ics20Transfer { +// async fn chan_open_init_execute(_state: S, _msg: &MsgChannelOpenInit) {} +// async fn chan_open_try_execute(_state: S, _msg: &MsgChannelOpenTry) {} +// async fn chan_open_ack_execute(_state: S, _msg: &MsgChannelOpenAck) {} +// async fn chan_open_confirm_execute(_state: S, _msg: &MsgChannelOpenConfirm) {} +// async fn chan_close_confirm_execute(_state: S, _msg: &MsgChannelCloseConfirm) {} +// async fn chan_close_init_execute(_state: S, _msg: &MsgChannelCloseInit) {} +// async fn recv_packet_execute(mut state: S, msg: &MsgRecvPacket) { +// // recv packet should never fail a transaction, but it should record a failure acknowledgement. +// let ack: Vec = match recv_transfer_packet_inner(&mut state, msg).await { +// Ok(_) => { +// // record packet acknowledgement without error +// TokenTransferAcknowledgement::success().into() +// } +// Err(e) => { +// tracing::debug!("couldnt execute transfer: {:#}", e); +// // record packet acknowledgement with error +// TokenTransferAcknowledgement::Error(e.to_string()).into() +// } +// }; + +// state +// .write_acknowledgement(&msg.packet, &ack) +// .await +// .expect("able to write acknowledgement"); +// } + +// async fn timeout_packet_execute(mut state: S, msg: &MsgTimeout) { +// // timeouts should never fail +// timeout_packet_inner(&mut state, msg) +// .await +// .expect("able to timeout packet"); +// } + +// async fn acknowledge_packet_execute(_state: S, _msg: &MsgAcknowledgement) {} +// } + +// impl AppHandler for Ics20Transfer {} diff --git a/crates/core/component/ibc/src/ics20_withdrawal.rs b/crates/core/component/ibc/src/ics20_withdrawal.rs index 9a0b0dd423..f41ffeccdd 100644 --- a/crates/core/component/ibc/src/ics20_withdrawal.rs +++ b/crates/core/component/ibc/src/ics20_withdrawal.rs @@ -1,130 +1,130 @@ -use ibc_types::core::{channel::ChannelId, client::Height as IbcHeight}; -use penumbra_asset::{ - asset::{self, DenomMetadata}, - Balance, Value, -}; -use penumbra_keys::Address; -use penumbra_num::Amount; -use penumbra_proto::{ - penumbra::core::component::ibc::v1alpha1::{self as pb, FungibleTokenPacketData}, - DomainType, TypeUrl, -}; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; +// use ibc_types::core::{channel::ChannelId, client::Height as IbcHeight}; +// use penumbra_asset::{ +// asset::{self, DenomMetadata}, +// Balance, Value, +// }; +// use penumbra_keys::Address; +// use penumbra_num::Amount; +// use penumbra_proto::{ +// penumbra::core::component::ibc::v1alpha1::{self as pb, FungibleTokenPacketData}, +// DomainType, TypeUrl, +// }; +// use serde::{Deserialize, Serialize}; +// use std::str::FromStr; -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(try_from = "pb::Ics20Withdrawal", into = "pb::Ics20Withdrawal")] -pub struct Ics20Withdrawal { - // a transparent value consisting of an amount and a denom. - pub amount: Amount, - pub denom: asset::DenomMetadata, - // the address on the destination chain to send the transfer to - pub destination_chain_address: String, - // a "sender" penumbra address to use to return funds from this withdrawal. - // this should be an ephemeral address - pub return_address: Address, - // the height (on Penumbra) at which this transfer expires (and funds are sent - // back to the sender address?). NOTE: if funds are sent back to the sender, - // we MUST verify a nonexistence proof before accepting the timeout, to - // prevent relayer censorship attacks. The core IBC implementation does this - // in its handling of validation of timeouts. - pub timeout_height: IbcHeight, - // the timestamp at which this transfer expires. - pub timeout_time: u64, - // the source channel used for the withdrawal - pub source_channel: ChannelId, -} +// #[derive(Debug, Clone, Serialize, Deserialize)] +// #[serde(try_from = "pb::Ics20Withdrawal", into = "pb::Ics20Withdrawal")] +// pub struct Ics20Withdrawal { +// // a transparent value consisting of an amount and a denom. +// pub amount: Amount, +// pub denom: asset::DenomMetadata, +// // the address on the destination chain to send the transfer to +// pub destination_chain_address: String, +// // a "sender" penumbra address to use to return funds from this withdrawal. +// // this should be an ephemeral address +// pub return_address: Address, +// // the height (on Penumbra) at which this transfer expires (and funds are sent +// // back to the sender address?). NOTE: if funds are sent back to the sender, +// // we MUST verify a nonexistence proof before accepting the timeout, to +// // prevent relayer censorship attacks. The core IBC implementation does this +// // in its handling of validation of timeouts. +// pub timeout_height: IbcHeight, +// // the timestamp at which this transfer expires. +// pub timeout_time: u64, +// // the source channel used for the withdrawal +// pub source_channel: ChannelId, +// } -impl Ics20Withdrawal { - pub fn value(&self) -> Value { - Value { - amount: self.amount, - asset_id: self.denom.id(), - } - } +// impl Ics20Withdrawal { +// pub fn value(&self) -> Value { +// Value { +// amount: self.amount, +// asset_id: self.denom.id(), +// } +// } - pub fn balance(&self) -> Balance { - -Balance::from(self.value()) - } +// pub fn balance(&self) -> Balance { +// -Balance::from(self.value()) +// } - pub fn packet_data(&self) -> Vec { - let ftpd: FungibleTokenPacketData = self.clone().into(); +// pub fn packet_data(&self) -> Vec { +// let ftpd: FungibleTokenPacketData = self.clone().into(); - // In violation of the ICS20 spec, ibc-go encodes transfer packets as JSON. - serde_json::to_vec(&ftpd).expect("can serialize FungibleTokenPacketData as JSON") - } +// // In violation of the ICS20 spec, ibc-go encodes transfer packets as JSON. +// serde_json::to_vec(&ftpd).expect("can serialize FungibleTokenPacketData as JSON") +// } - // stateless validation of an Ics20 withdrawal action. - pub fn validate(&self) -> anyhow::Result<()> { - if self.timeout_time == 0 { - anyhow::bail!("timeout time must be non-zero"); - } +// // stateless validation of an Ics20 withdrawal action. +// pub fn validate(&self) -> anyhow::Result<()> { +// if self.timeout_time == 0 { +// anyhow::bail!("timeout time must be non-zero"); +// } - // NOTE: we could validate the destination chain address as bech32 to prevent mistyped - // addresses, but this would preclude sending to chains that don't use bech32 addresses. +// // NOTE: we could validate the destination chain address as bech32 to prevent mistyped +// // addresses, but this would preclude sending to chains that don't use bech32 addresses. - Ok(()) - } -} +// Ok(()) +// } +// } -impl TypeUrl for Ics20Withdrawal { - const TYPE_URL: &'static str = "/penumbra.core.ibc.v1alpha1.Ics20Withdrawal"; -} +// impl TypeUrl for Ics20Withdrawal { +// const TYPE_URL: &'static str = "/penumbra.core.ibc.v1alpha1.Ics20Withdrawal"; +// } -impl DomainType for Ics20Withdrawal { - type Proto = pb::Ics20Withdrawal; -} +// impl DomainType for Ics20Withdrawal { +// type Proto = pb::Ics20Withdrawal; +// } -impl From for pb::Ics20Withdrawal { - fn from(w: Ics20Withdrawal) -> Self { - pb::Ics20Withdrawal { - amount: Some(w.amount.into()), - denom: Some(w.denom.base_denom().into()), - destination_chain_address: w.destination_chain_address, - return_address: Some(w.return_address.into()), - timeout_height: Some(w.timeout_height.into()), - timeout_time: w.timeout_time, - source_channel: w.source_channel.to_string(), - } - } -} +// impl From for pb::Ics20Withdrawal { +// fn from(w: Ics20Withdrawal) -> Self { +// pb::Ics20Withdrawal { +// amount: Some(w.amount.into()), +// denom: Some(w.denom.base_denom().into()), +// destination_chain_address: w.destination_chain_address, +// return_address: Some(w.return_address.into()), +// timeout_height: Some(w.timeout_height.into()), +// timeout_time: w.timeout_time, +// source_channel: w.source_channel.to_string(), +// } +// } +// } -impl TryFrom for Ics20Withdrawal { - type Error = anyhow::Error; - fn try_from(s: pb::Ics20Withdrawal) -> Result { - Ok(Self { - amount: s - .amount - .ok_or_else(|| anyhow::anyhow!("missing amount"))? - .try_into()?, - denom: DenomMetadata::default_for( - &s.denom - .ok_or_else(|| anyhow::anyhow!("missing denom metadata"))? - .try_into()?, - ) - .ok_or_else(|| anyhow::anyhow!("could not generate default denom metadata"))?, - destination_chain_address: s.destination_chain_address, - return_address: s - .return_address - .ok_or_else(|| anyhow::anyhow!("missing sender"))? - .try_into()?, - timeout_height: s - .timeout_height - .ok_or_else(|| anyhow::anyhow!("missing timeout height"))? - .try_into()?, - timeout_time: s.timeout_time, - source_channel: ChannelId::from_str(&s.source_channel)?, - }) - } -} +// impl TryFrom for Ics20Withdrawal { +// type Error = anyhow::Error; +// fn try_from(s: pb::Ics20Withdrawal) -> Result { +// Ok(Self { +// amount: s +// .amount +// .ok_or_else(|| anyhow::anyhow!("missing amount"))? +// .try_into()?, +// denom: DenomMetadata::default_for( +// &s.denom +// .ok_or_else(|| anyhow::anyhow!("missing denom metadata"))? +// .try_into()?, +// ) +// .ok_or_else(|| anyhow::anyhow!("could not generate default denom metadata"))?, +// destination_chain_address: s.destination_chain_address, +// return_address: s +// .return_address +// .ok_or_else(|| anyhow::anyhow!("missing sender"))? +// .try_into()?, +// timeout_height: s +// .timeout_height +// .ok_or_else(|| anyhow::anyhow!("missing timeout height"))? +// .try_into()?, +// timeout_time: s.timeout_time, +// source_channel: ChannelId::from_str(&s.source_channel)?, +// }) +// } +// } -impl From for pb::FungibleTokenPacketData { - fn from(w: Ics20Withdrawal) -> Self { - pb::FungibleTokenPacketData { - amount: w.value().amount.to_string(), - denom: w.denom.to_string(), - receiver: w.destination_chain_address, - sender: w.return_address.to_string(), - } - } -} +// impl From for pb::FungibleTokenPacketData { +// fn from(w: Ics20Withdrawal) -> Self { +// pb::FungibleTokenPacketData { +// amount: w.value().amount.to_string(), +// denom: w.denom.to_string(), +// receiver: w.destination_chain_address, +// sender: w.return_address.to_string(), +// } +// } +// } diff --git a/crates/core/component/ibc/src/lib.rs b/crates/core/component/ibc/src/lib.rs index c6f4ae471c..692cd27113 100644 --- a/crates/core/component/ibc/src/lib.rs +++ b/crates/core/component/ibc/src/lib.rs @@ -12,14 +12,14 @@ pub mod component; pub mod genesis; mod ibc_action; mod ibc_token; -mod ics20_withdrawal; +// mod ics20_withdrawal; pub mod params; mod version; pub use ibc_action::IbcAction; pub use ibc_action::IbcActionWithHandler; pub use ibc_token::IbcToken; -pub use ics20_withdrawal::Ics20Withdrawal; +// pub use ics20_withdrawal::Ics20Withdrawal; #[cfg_attr(docsrs, doc(cfg(feature = "component")))] #[cfg(feature = "component")] diff --git a/crates/core/component/shielded-pool/Cargo.toml b/crates/core/component/shielded-pool/Cargo.toml index cb40fe6b59..ee495ab443 100644 --- a/crates/core/component/shielded-pool/Cargo.toml +++ b/crates/core/component/shielded-pool/Cargo.toml @@ -37,6 +37,7 @@ penumbra-tct = { path = "../../../crypto/tct" } penumbra-proof-params = { path = "../../../crypto/proof-params", default-features = false } penumbra-sct = { path = "../sct", default-features = false } penumbra-component = { path = "../component", optional = true } +penumbra-ibc = { path = "../ibc" } penumbra-chain = { path = "../chain", default-features = false } penumbra-asset = { path = "../../../core/asset", default-features = false } penumbra-num = { path = "../../../core/num", default-features = false } @@ -45,6 +46,7 @@ decaf377-ka = { path = "../../../crypto/decaf377-ka/" } decaf377-fmd = { path = "../../../crypto/decaf377-fmd/" } # Penumbra dependencies +ibc-types = { version = "0.7.0", default-features = false } decaf377-rdsa = { version = "0.7" } decaf377 = { version = "0.5", features = ["r1cs"] } poseidon377 = { version = "0.6", features = ["r1cs"] } @@ -60,7 +62,9 @@ ark-serialize = "0.4" ark-groth16 = { version = "0.4", default-features = false } ark-snark = "0.4" metrics = "0.19.0" +prost = "0.12" serde = { version = "1", features = ["derive"] } +serde_json = "1" tracing = "0.1" anyhow = "1" async-trait = "0.1.52" diff --git a/crates/core/component/shielded-pool/src/component.rs b/crates/core/component/shielded-pool/src/component.rs index f907ac3826..228f7c4970 100644 --- a/crates/core/component/shielded-pool/src/component.rs +++ b/crates/core/component/shielded-pool/src/component.rs @@ -5,10 +5,12 @@ mod metrics; mod note_manager; mod shielded_pool; mod supply; +mod transfer; pub use self::metrics::register_metrics; pub use note_manager::NoteManager; pub use shielded_pool::{ShieldedPool, StateReadExt}; pub use supply::{SupplyRead, SupplyWrite}; +pub use transfer::Ics20Transfer; pub mod rpc; diff --git a/crates/core/component/shielded-pool/src/component/action_handler.rs b/crates/core/component/shielded-pool/src/component/action_handler.rs index c5dbcf860a..7eedb7cf3c 100644 --- a/crates/core/component/shielded-pool/src/component/action_handler.rs +++ b/crates/core/component/shielded-pool/src/component/action_handler.rs @@ -1,2 +1,3 @@ +mod ics20_withdrawal; mod output; mod spend; diff --git a/crates/core/component/shielded-pool/src/component/action_handler/ics20_withdrawal.rs b/crates/core/component/shielded-pool/src/component/action_handler/ics20_withdrawal.rs new file mode 100644 index 0000000000..37a1186b35 --- /dev/null +++ b/crates/core/component/shielded-pool/src/component/action_handler/ics20_withdrawal.rs @@ -0,0 +1,29 @@ +use std::sync::Arc; + +use anyhow::Result; +use async_trait::async_trait; +use penumbra_component::ActionHandler; +use penumbra_storage::{StateRead, StateWrite}; + +use crate::{ + component::transfer::{Ics20TransferReadExt as _, Ics20TransferWriteExt as _}, + Ics20Withdrawal, +}; + +#[async_trait] +impl ActionHandler for Ics20Withdrawal { + type CheckStatelessContext = (); + async fn check_stateless(&self, _context: ()) -> Result<()> { + self.validate() + } + + async fn check_stateful(&self, state: Arc) -> Result<()> { + state.withdrawal_check(self).await + } + + async fn execute(&self, mut state: S) -> Result<()> { + state.withdrawal_execute(self).await; + + Ok(()) + } +} diff --git a/crates/core/component/shielded-pool/src/component/transfer.rs b/crates/core/component/shielded-pool/src/component/transfer.rs new file mode 100644 index 0000000000..7be4517c79 --- /dev/null +++ b/crates/core/component/shielded-pool/src/component/transfer.rs @@ -0,0 +1,449 @@ +use std::str::FromStr; + +use crate::component::{NoteManager, SupplyWrite}; +use anyhow::{Context, Result}; +use async_trait::async_trait; +use ibc_types::{ + core::channel::{ + channel::Order as ChannelOrder, + msgs::{ + MsgAcknowledgement, MsgChannelCloseConfirm, MsgChannelCloseInit, MsgChannelOpenAck, + MsgChannelOpenConfirm, MsgChannelOpenInit, MsgChannelOpenTry, MsgRecvPacket, + MsgTimeout, + }, + ChannelId, PortId, Version, + }, + transfer::acknowledgement::TokenTransferAcknowledgement, +}; +use penumbra_asset::{asset, asset::DenomMetadata, Value}; +use penumbra_keys::Address; +use penumbra_num::Amount; +use penumbra_proto::{ + penumbra::core::component::ibc::v1alpha1::FungibleTokenPacketData, StateReadProto, + StateWriteProto, +}; +use penumbra_storage::{StateRead, StateWrite}; +use prost::Message; + +use penumbra_ibc::component::{ + app_handler::{AppHandler, AppHandlerCheck, AppHandlerExecute}, + packet::{ + IBCPacket, SendPacketRead as _, SendPacketWrite as _, Unchecked, WriteAcknowledgement as _, + }, + state_key, +}; + +use crate::Ics20Withdrawal; + +// returns a bool indicating if the provided denom was issued locally or if it was bridged in. +// this logic is a bit tricky, and adapted from https://github.com/cosmos/ibc/tree/main/spec/app/ics-020-fungible-token-transfer (sendFungibleTokens). +// +// what we want to do is to determine if the denom being withdrawn is a native token (one +// that originates from Penumbra) or a bridged token (one that was sent into penumbra from +// IBC). +// +// A simple way of doing this is by parsing the denom, looking for a prefix that is only +// appended in the case of a bridged token. That is what this logic does. +fn is_source(source_port: &PortId, source_channel: &ChannelId, denom: &DenomMetadata) -> bool { + let prefix = format!("{source_port}/{source_channel}/"); + + denom.starts_with(&prefix) +} + +#[derive(Clone)] +pub struct Ics20Transfer {} + +#[async_trait] +pub trait Ics20TransferReadExt: StateRead { + async fn withdrawal_check(&self, withdrawal: &Ics20Withdrawal) -> Result<()> { + // create packet + let packet: IBCPacket = withdrawal.clone().into(); + + // send packet + self.send_packet_check(packet).await?; + + Ok(()) + } +} + +impl Ics20TransferReadExt for T {} + +#[async_trait] +pub trait Ics20TransferWriteExt: StateWrite { + async fn withdrawal_execute(&mut self, withdrawal: &Ics20Withdrawal) { + // create packet, assume it's already checked since the component caller contract calls `check` before `execute` + let checked_packet = IBCPacket::::from(withdrawal.clone()).assume_checked(); + + let prefix = format!("transfer/{}/", &withdrawal.source_channel); + if !withdrawal.denom.starts_with(&prefix) { + // we are the source. add the value balance to the escrow channel. + let existing_value_balance: Amount = self + .get(&state_key::ics20_value_balance( + &withdrawal.source_channel, + &withdrawal.denom.id(), + )) + .await + .expect("able to retrieve value balance in ics20 withdrawal! (execute)") + .unwrap_or_else(Amount::zero); + + let new_value_balance = existing_value_balance + withdrawal.amount; + self.put( + state_key::ics20_value_balance(&withdrawal.source_channel, &withdrawal.denom.id()), + new_value_balance, + ); + } else { + // receiver is the source, burn utxos + + // NOTE: this burning should already be accomplished by the value balance check from + // the withdrawal's balance commitment, so nothing to do here. + // + + // update supply tracking of burned note + self.update_token_supply(&withdrawal.denom.id(), -(withdrawal.amount.value() as i128)) + .await + .expect("couldn't update token supply in ics20 withdrawal!"); + } + + self.send_packet_execute(checked_packet).await; + } +} + +impl Ics20TransferWriteExt for T {} + +// TODO: Ics20 implementation. +// see: https://github.com/cosmos/ibc/tree/master/spec/app/ics-020-fungible-token-transfer +// TODO (ava): add versioning to AppHandlers +#[async_trait] +impl AppHandlerCheck for Ics20Transfer { + async fn chan_open_init_check(_state: S, msg: &MsgChannelOpenInit) -> Result<()> { + if msg.ordering != ChannelOrder::Unordered { + anyhow::bail!("channel order must be unordered for Ics20 transfer"); + } + let ics20_version = Version::new("ics20-1".to_string()); + if msg.version_proposal != ics20_version { + anyhow::bail!("channel version must be ics20 for Ics20 transfer"); + } + + Ok(()) + } + + async fn chan_open_try_check(_state: S, msg: &MsgChannelOpenTry) -> Result<()> { + if msg.ordering != ChannelOrder::Unordered { + anyhow::bail!("channel order must be unordered for Ics20 transfer"); + } + let ics20_version = Version::new("ics20-1".to_string()); + + if msg.version_supported_on_a != ics20_version { + anyhow::bail!("counterparty version must be ics20-1 for Ics20 transfer"); + } + + Ok(()) + } + + async fn chan_open_ack_check(_state: S, msg: &MsgChannelOpenAck) -> Result<()> { + let ics20_version = Version::new("ics20-1".to_string()); + if msg.version_on_b != ics20_version { + anyhow::bail!("counterparty version must be ics20-1 for Ics20 transfer"); + } + + Ok(()) + } + + async fn chan_open_confirm_check( + _state: S, + _msg: &MsgChannelOpenConfirm, + ) -> Result<()> { + // accept channel confirmations, port has already been validated, version has already been validated + Ok(()) + } + + async fn chan_close_confirm_check( + _state: S, + _msg: &MsgChannelCloseConfirm, + ) -> Result<()> { + // no action necessary + Ok(()) + } + + async fn chan_close_init_check( + _state: S, + _msg: &MsgChannelCloseInit, + ) -> Result<()> { + // always abort transaction + anyhow::bail!("ics20 always aborts on close init"); + } + + async fn recv_packet_check(_state: S, _msg: &MsgRecvPacket) -> Result<()> { + // all checks on recv_packet done in execute + Ok(()) + } + + async fn timeout_packet_check(state: S, msg: &MsgTimeout) -> Result<()> { + let packet_data = FungibleTokenPacketData::decode(msg.packet.data.as_slice())?; + let denom: asset::DenomMetadata = packet_data.denom.as_str().try_into()?; + + if is_source(&msg.packet.port_on_a, &msg.packet.chan_on_a, &denom) { + // check if we have enough balance to refund tokens to sender + let value_balance: Amount = state + .get(&state_key::ics20_value_balance( + &msg.packet.chan_on_a, + &denom.id(), + )) + .await? + .unwrap_or_else(Amount::zero); + + let amount_penumbra: Amount = packet_data.amount.try_into()?; + if value_balance < amount_penumbra { + anyhow::bail!("insufficient balance to refund tokens to sender"); + } + } + + Ok(()) + } + + async fn acknowledge_packet_check( + _state: S, + _msg: &MsgAcknowledgement, + ) -> Result<()> { + Ok(()) + } +} + +// the main entry point for ICS20 transfer packet handling +async fn recv_transfer_packet_inner( + mut state: S, + msg: &MsgRecvPacket, +) -> Result<()> { + // parse if we are source or dest, and mint or burn accordingly + // + // see this part of the spec for this logic: + // + // https://github.com/cosmos/ibc/tree/main/spec/app/ics-020-fungible-token-transfer (onRecvPacket) + // + // NOTE: spec says proto but thsi is actualy JSON according to the ibc-go implementation + let packet_data: FungibleTokenPacketData = serde_json::from_slice(msg.packet.data.as_slice()) + .with_context(|| "failed to decode FTPD packet")?; + let denom: asset::DenomMetadata = packet_data + .denom + .as_str() + .try_into() + .context("couldnt decode denom in ICS20 transfer")?; + let receiver_amount: Amount = packet_data + .amount + .try_into() + .context("couldnt decode amount in ICS20 transfer")?; + let receiver_address = Address::from_str(&packet_data.receiver)?; + + // NOTE: here we assume we are chain A. + + // 2. check if we are the source chain for the denom. + if is_source(&msg.packet.port_on_a, &msg.packet.chan_on_a, &denom) { + // mint tokens to receiver in the amount of packet_data.amount in the denom of denom (with + // the source removed, since we're the source) + let prefix = format!( + "{source_port}/{source_chan}/", + source_port = msg.packet.port_on_a, + source_chan = msg.packet.chan_on_a + ); + + let unprefixed_denom: asset::DenomMetadata = packet_data + .denom + .replace(&prefix, "") + .as_str() + .try_into() + .context("couldnt decode denom in ICS20 transfer")?; + + let value: Value = Value { + amount: receiver_amount, + asset_id: unprefixed_denom.id(), + }; + + // assume AppHandlerCheck has already been called, and we have enough balance to mint tokens to receiver + // check if we have enough balance to unescrow tokens to receiver + let value_balance: Amount = state + .get(&state_key::ics20_value_balance( + &msg.packet.chan_on_b, + &unprefixed_denom.id(), + )) + .await? + .unwrap_or_else(Amount::zero); + + if value_balance < receiver_amount { + // error text here is from the ics20 spec + anyhow::bail!("transfer coins failed"); + } + + state + .mint_note( + value, + &receiver_address, + penumbra_chain::NoteSource::Ics20Transfer, // TODO + ) + .await + .context("unable to mint note when receiving ics20 transfer packet")?; + + // update the value balance + let value_balance: Amount = state + .get(&state_key::ics20_value_balance( + &msg.packet.chan_on_b, + &unprefixed_denom.id(), + )) + .await? + .unwrap_or_else(Amount::zero); + + // note: this arithmetic was checked above, but we do it again anyway. + let new_value_balance = value_balance + .checked_sub(&receiver_amount) + .context("underflow subtracing value balance in ics20 transfer")?; + state.put( + state_key::ics20_value_balance(&msg.packet.chan_on_b, &denom.id()), + new_value_balance, + ); + } else { + // create new denom: + // + // prefix = "{packet.destPort}/{packet.destChannel}/" + // prefixedDenomination = prefix + data.denom + // + // then mint that denom to packet_data.receiver in packet_data.amount + // no value balance to update here since this is an exogenous denom + let prefixed_denomination = format!( + "{}/{}/{}", + msg.packet.port_on_b, msg.packet.chan_on_b, packet_data.denom + ); + + let denom: asset::DenomMetadata = prefixed_denomination + .as_str() + .try_into() + .context("unable to parse denom in ics20 transfer as DenomMetadata")?; + state + .register_denom(&denom) + .await + .context("unable to register denom in ics20 transfer")?; + + let value = Value { + amount: receiver_amount, + asset_id: denom.id(), + }; + + state + .mint_note( + value, + &receiver_address, + penumbra_chain::NoteSource::Ics20Transfer, + ) + .await + .context("failed to mint notes in ibc transfer")?; + } + + Ok(()) +} + +// see: https://github.com/cosmos/ibc/blob/8326e26e7e1188b95c32481ff00348a705b23700/spec/app/ics-020-fungible-token-transfer/README.md?plain=1#L297 +async fn timeout_packet_inner(mut state: S, msg: &MsgTimeout) -> Result<()> { + let packet_data = FungibleTokenPacketData::decode(msg.packet.data.as_slice())?; + let denom: asset::DenomMetadata = packet_data // CRITICAL: verify that this denom is validated in upstream timeout handling + .denom + .as_str() + .try_into() + .context("couldn't decode denom in ics20 transfer timeout")?; + // receiver was source chain, mint vouchers back to sender + let amount: Amount = packet_data + .amount + .try_into() + .context("couldn't decode amount in ics20 transfer timeout")?; + + let receiver = Address::from_str(&packet_data.receiver) + .context("couldn't decode receiver address in ics20 timeout")?; + + let value: Value = Value { + amount, + asset_id: denom.id(), + }; + + if is_source(&msg.packet.port_on_a, &msg.packet.chan_on_a, &denom) { + // sender was source chain, unescrow tokens back to sender + let value_balance: Amount = state + .get(&state_key::ics20_value_balance( + &msg.packet.chan_on_a, + &denom.id(), + )) + .await? + .unwrap_or_else(Amount::zero); + + if value_balance < amount { + anyhow::bail!("couldn't return coins in timeout: not enough value balance"); + } + + state + .mint_note(value, &receiver, penumbra_chain::NoteSource::Ics20Transfer) + .await + .context("couldn't mint note in timeout_packet_inner")?; + + // update the value balance + let value_balance: Amount = state + .get(&state_key::ics20_value_balance( + &msg.packet.chan_on_a, + &denom.id(), + )) + .await? + .unwrap_or_else(Amount::zero); + + // note: this arithmetic was checked above, but we do it again anyway. + let new_value_balance = value_balance + .checked_sub(&amount) + .context("underflow in ics20 timeout packet value balance subtraction")?; + state.put( + state_key::ics20_value_balance(&msg.packet.chan_on_a, &denom.id()), + new_value_balance, + ); + } else { + state + .mint_note(value, &receiver, penumbra_chain::NoteSource::Ics20Transfer) // NOTE: should this be Ics20TransferTimeout? + .await + .context("failed to mint return voucher in ics20 transfer timeout")?; + } + + Ok(()) +} + +// NOTE: should these be fallible, now that our enclosing state machine is fallible in execution? +#[async_trait] +impl AppHandlerExecute for Ics20Transfer { + async fn chan_open_init_execute(_state: S, _msg: &MsgChannelOpenInit) {} + async fn chan_open_try_execute(_state: S, _msg: &MsgChannelOpenTry) {} + async fn chan_open_ack_execute(_state: S, _msg: &MsgChannelOpenAck) {} + async fn chan_open_confirm_execute(_state: S, _msg: &MsgChannelOpenConfirm) {} + async fn chan_close_confirm_execute(_state: S, _msg: &MsgChannelCloseConfirm) {} + async fn chan_close_init_execute(_state: S, _msg: &MsgChannelCloseInit) {} + async fn recv_packet_execute(mut state: S, msg: &MsgRecvPacket) { + // recv packet should never fail a transaction, but it should record a failure acknowledgement. + let ack: Vec = match recv_transfer_packet_inner(&mut state, msg).await { + Ok(_) => { + // record packet acknowledgement without error + TokenTransferAcknowledgement::success().into() + } + Err(e) => { + tracing::debug!("couldnt execute transfer: {:#}", e); + // record packet acknowledgement with error + TokenTransferAcknowledgement::Error(e.to_string()).into() + } + }; + + state + .write_acknowledgement(&msg.packet, &ack) + .await + .expect("able to write acknowledgement"); + } + + async fn timeout_packet_execute(mut state: S, msg: &MsgTimeout) { + // timeouts should never fail + timeout_packet_inner(&mut state, msg) + .await + .expect("able to timeout packet"); + } + + async fn acknowledge_packet_execute(_state: S, _msg: &MsgAcknowledgement) {} +} + +impl AppHandler for Ics20Transfer {} diff --git a/crates/core/component/shielded-pool/src/ics20_withdrawal.rs b/crates/core/component/shielded-pool/src/ics20_withdrawal.rs new file mode 100644 index 0000000000..54b5077286 --- /dev/null +++ b/crates/core/component/shielded-pool/src/ics20_withdrawal.rs @@ -0,0 +1,144 @@ +use ibc_types::core::{channel::ChannelId, channel::PortId, client::Height as IbcHeight}; +use penumbra_asset::{ + asset::{self, DenomMetadata}, + Balance, Value, +}; +use penumbra_keys::Address; +use penumbra_num::Amount; +use penumbra_proto::{ + penumbra::core::component::ibc::v1alpha1::{self as pb, FungibleTokenPacketData}, + DomainType, TypeUrl, +}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +use penumbra_ibc::component::packet::{IBCPacket, Unchecked}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "pb::Ics20Withdrawal", into = "pb::Ics20Withdrawal")] +pub struct Ics20Withdrawal { + // a transparent value consisting of an amount and a denom. + pub amount: Amount, + pub denom: asset::DenomMetadata, + // the address on the destination chain to send the transfer to + pub destination_chain_address: String, + // a "sender" penumbra address to use to return funds from this withdrawal. + // this should be an ephemeral address + pub return_address: Address, + // the height (on Penumbra) at which this transfer expires (and funds are sent + // back to the sender address?). NOTE: if funds are sent back to the sender, + // we MUST verify a nonexistence proof before accepting the timeout, to + // prevent relayer censorship attacks. The core IBC implementation does this + // in its handling of validation of timeouts. + pub timeout_height: IbcHeight, + // the timestamp at which this transfer expires. + pub timeout_time: u64, + // the source channel used for the withdrawal + pub source_channel: ChannelId, +} + +impl From for IBCPacket { + fn from(withdrawal: Ics20Withdrawal) -> Self { + Self::new( + PortId::transfer(), + withdrawal.source_channel.clone(), + withdrawal.timeout_height, + withdrawal.timeout_time, + withdrawal.packet_data(), + ) + } +} + +impl Ics20Withdrawal { + pub fn value(&self) -> Value { + Value { + amount: self.amount, + asset_id: self.denom.id(), + } + } + + pub fn balance(&self) -> Balance { + -Balance::from(self.value()) + } + + pub fn packet_data(&self) -> Vec { + let ftpd: FungibleTokenPacketData = self.clone().into(); + + // In violation of the ICS20 spec, ibc-go encodes transfer packets as JSON. + serde_json::to_vec(&ftpd).expect("can serialize FungibleTokenPacketData as JSON") + } + + // stateless validation of an Ics20 withdrawal action. + pub fn validate(&self) -> anyhow::Result<()> { + if self.timeout_time == 0 { + anyhow::bail!("timeout time must be non-zero"); + } + + // NOTE: we could validate the destination chain address as bech32 to prevent mistyped + // addresses, but this would preclude sending to chains that don't use bech32 addresses. + + Ok(()) + } +} + +impl TypeUrl for Ics20Withdrawal { + const TYPE_URL: &'static str = "/penumbra.core.ibc.v1alpha1.Ics20Withdrawal"; +} + +impl DomainType for Ics20Withdrawal { + type Proto = pb::Ics20Withdrawal; +} + +impl From for pb::Ics20Withdrawal { + fn from(w: Ics20Withdrawal) -> Self { + pb::Ics20Withdrawal { + amount: Some(w.amount.into()), + denom: Some(w.denom.base_denom().into()), + destination_chain_address: w.destination_chain_address, + return_address: Some(w.return_address.into()), + timeout_height: Some(w.timeout_height.into()), + timeout_time: w.timeout_time, + source_channel: w.source_channel.to_string(), + } + } +} + +impl TryFrom for Ics20Withdrawal { + type Error = anyhow::Error; + fn try_from(s: pb::Ics20Withdrawal) -> Result { + Ok(Self { + amount: s + .amount + .ok_or_else(|| anyhow::anyhow!("missing amount"))? + .try_into()?, + denom: DenomMetadata::default_for( + &s.denom + .ok_or_else(|| anyhow::anyhow!("missing denom metadata"))? + .try_into()?, + ) + .ok_or_else(|| anyhow::anyhow!("could not generate default denom metadata"))?, + destination_chain_address: s.destination_chain_address, + return_address: s + .return_address + .ok_or_else(|| anyhow::anyhow!("missing sender"))? + .try_into()?, + timeout_height: s + .timeout_height + .ok_or_else(|| anyhow::anyhow!("missing timeout height"))? + .try_into()?, + timeout_time: s.timeout_time, + source_channel: ChannelId::from_str(&s.source_channel)?, + }) + } +} + +impl From for pb::FungibleTokenPacketData { + fn from(w: Ics20Withdrawal) -> Self { + pb::FungibleTokenPacketData { + amount: w.value().amount.to_string(), + denom: w.denom.to_string(), + receiver: w.destination_chain_address, + sender: w.return_address.to_string(), + } + } +} diff --git a/crates/core/component/shielded-pool/src/lib.rs b/crates/core/component/shielded-pool/src/lib.rs index 61d8ff5647..84bdaf8629 100644 --- a/crates/core/component/shielded-pool/src/lib.rs +++ b/crates/core/component/shielded-pool/src/lib.rs @@ -9,6 +9,9 @@ pub mod event; pub mod genesis; pub mod state_key; +pub mod ics20_withdrawal; +pub use ics20_withdrawal::Ics20Withdrawal; + pub mod note; mod note_payload; pub mod rseed; diff --git a/crates/core/transaction/src/action.rs b/crates/core/transaction/src/action.rs index 239b148ca7..42ea0ac6fb 100644 --- a/crates/core/transaction/src/action.rs +++ b/crates/core/transaction/src/action.rs @@ -31,7 +31,7 @@ pub enum Action { Undelegate(penumbra_stake::Undelegate), UndelegateClaim(penumbra_stake::UndelegateClaim), - Ics20Withdrawal(penumbra_ibc::Ics20Withdrawal), + Ics20Withdrawal(penumbra_shielded_pool::Ics20Withdrawal), DaoSpend(penumbra_dao::DaoSpend), DaoOutput(penumbra_dao::DaoOutput), diff --git a/crates/core/transaction/src/effect_hash.rs b/crates/core/transaction/src/effect_hash.rs index 0c69295592..82639c6528 100644 --- a/crates/core/transaction/src/effect_hash.rs +++ b/crates/core/transaction/src/effect_hash.rs @@ -11,7 +11,6 @@ use penumbra_governance::{ DelegatorVote, DelegatorVoteBody, Proposal, ProposalDepositClaim, ProposalSubmit, ProposalWithdraw, ValidatorVote, ValidatorVoteBody, Vote, }; -use penumbra_ibc::Ics20Withdrawal; use penumbra_keys::{FullViewingKey, PayloadKey}; use penumbra_proto::{ core::component::dex::v1alpha1 as pbd, core::component::fee::v1alpha1 as pbf, @@ -20,7 +19,7 @@ use penumbra_proto::{ core::transaction::v1alpha1 as pbt, crypto::decaf377_fmd::v1alpha1 as pb_fmd, Message, }; use penumbra_proto::{DomainType, TypeUrl}; -use penumbra_shielded_pool::{output, spend}; +use penumbra_shielded_pool::{output, spend, Ics20Withdrawal}; use penumbra_stake::{Delegate, Undelegate, UndelegateClaimBody}; use crate::{ diff --git a/crates/core/transaction/src/gas.rs b/crates/core/transaction/src/gas.rs index ef99bad521..5f4b289ebf 100644 --- a/crates/core/transaction/src/gas.rs +++ b/crates/core/transaction/src/gas.rs @@ -6,9 +6,9 @@ use penumbra_dex::{ SwapClaim, }; use penumbra_fee::Gas; -use penumbra_ibc::{IbcAction, Ics20Withdrawal}; +use penumbra_ibc::IbcAction; use penumbra_sct::Nullifier; -use penumbra_shielded_pool::{Output, Spend}; +use penumbra_shielded_pool::{Ics20Withdrawal, Output, Spend}; use penumbra_stake::{ validator::Definition as ValidatorDefinition, Delegate, Undelegate, UndelegateClaim, }; diff --git a/crates/core/transaction/src/is_action.rs b/crates/core/transaction/src/is_action.rs index 9bd137a51b..31a43ff9f9 100644 --- a/crates/core/transaction/src/is_action.rs +++ b/crates/core/transaction/src/is_action.rs @@ -14,8 +14,8 @@ use penumbra_governance::{ DelegatorVote, DelegatorVoteView, ProposalDepositClaim, ProposalSubmit, ProposalWithdraw, ValidatorVote, VotingReceiptToken, }; -use penumbra_ibc::{IbcAction, Ics20Withdrawal}; -use penumbra_shielded_pool::{Note, Output, OutputView, Spend, SpendView}; +use penumbra_ibc::IbcAction; +use penumbra_shielded_pool::{Ics20Withdrawal, Note, Output, OutputView, Spend, SpendView}; use penumbra_stake::{Delegate, Undelegate, UndelegateClaim}; use crate::{Action, ActionView, TransactionPerspective}; diff --git a/crates/core/transaction/src/plan.rs b/crates/core/transaction/src/plan.rs index 78a8b5e9eb..66f6162360 100644 --- a/crates/core/transaction/src/plan.rs +++ b/crates/core/transaction/src/plan.rs @@ -13,10 +13,10 @@ use penumbra_fee::Fee; use penumbra_governance::{ DelegatorVotePlan, ProposalDepositClaim, ProposalSubmit, ProposalWithdraw, ValidatorVote, }; -use penumbra_ibc::{IbcAction, Ics20Withdrawal}; +use penumbra_ibc::IbcAction; use penumbra_keys::Address; use penumbra_proto::{core::transaction::v1alpha1 as pb, DomainType, TypeUrl}; -use penumbra_shielded_pool::{OutputPlan, SpendPlan}; +use penumbra_shielded_pool::{Ics20Withdrawal, OutputPlan, SpendPlan}; use penumbra_stake::{Delegate, Undelegate, UndelegateClaimPlan}; use rand::{CryptoRng, Rng}; use serde::{Deserialize, Serialize}; diff --git a/crates/core/transaction/src/plan/action.rs b/crates/core/transaction/src/plan/action.rs index 5ba541f26f..c7c5bdf7c2 100644 --- a/crates/core/transaction/src/plan/action.rs +++ b/crates/core/transaction/src/plan/action.rs @@ -14,9 +14,9 @@ use penumbra_governance::{ ValidatorVote, }; -use penumbra_ibc::{IbcAction, Ics20Withdrawal}; +use penumbra_ibc::IbcAction; use penumbra_proto::{core::transaction::v1alpha1 as pb_t, DomainType, TypeUrl}; -use penumbra_shielded_pool::{OutputPlan, SpendPlan}; +use penumbra_shielded_pool::{Ics20Withdrawal, OutputPlan, SpendPlan}; use penumbra_stake::{Delegate, Undelegate, UndelegateClaimPlan}; use serde::{Deserialize, Serialize}; diff --git a/crates/core/transaction/src/view/action_view.rs b/crates/core/transaction/src/view/action_view.rs index 80630377d7..df536405bc 100644 --- a/crates/core/transaction/src/view/action_view.rs +++ b/crates/core/transaction/src/view/action_view.rs @@ -5,8 +5,9 @@ use penumbra_dex::{ swap_claim::SwapClaimView, }; use penumbra_governance::{ProposalDepositClaim, ProposalSubmit, ProposalWithdraw, ValidatorVote}; -use penumbra_ibc::{IbcAction, Ics20Withdrawal}; +use penumbra_ibc::IbcAction; use penumbra_proto::{core::transaction::v1alpha1 as pbt, DomainType, TypeUrl}; +use penumbra_shielded_pool::Ics20Withdrawal; use penumbra_stake::{Delegate, Undelegate, UndelegateClaim}; use serde::{Deserialize, Serialize}; diff --git a/crates/view/src/planner.rs b/crates/view/src/planner.rs index 12104795d7..73f2d26ad3 100644 --- a/crates/view/src/planner.rs +++ b/crates/view/src/planner.rs @@ -24,14 +24,14 @@ use penumbra_governance::{ proposal_state, DelegatorVotePlan, Proposal, ProposalDepositClaim, ProposalSubmit, ProposalWithdraw, ValidatorVote, Vote, }; -use penumbra_ibc::{IbcAction, Ics20Withdrawal}; +use penumbra_ibc::IbcAction; use penumbra_keys::{ keys::{AddressIndex, WalletId}, Address, }; use penumbra_num::Amount; use penumbra_proto::view::v1alpha1::{NotesForVotingRequest, NotesRequest}; -use penumbra_shielded_pool::{Note, OutputPlan, SpendPlan}; +use penumbra_shielded_pool::{Ics20Withdrawal, Note, OutputPlan, SpendPlan}; use penumbra_stake::{rate::RateData, validator}; use penumbra_stake::{IdentityKey, UndelegateClaimPlan}; use penumbra_tct as tct; diff --git a/crates/wasm/src/planner.rs b/crates/wasm/src/planner.rs index 5b4e7d33f6..0a99cd4366 100644 --- a/crates/wasm/src/planner.rs +++ b/crates/wasm/src/planner.rs @@ -24,11 +24,11 @@ use penumbra_governance::{ proposal_state::Outcome as ProposalOutcome, DelegatorVotePlan, Proposal, ProposalDepositClaim, ProposalSubmit, ProposalWithdraw, ValidatorVote, Vote, }; -use penumbra_ibc::{IbcAction, Ics20Withdrawal}; +use penumbra_ibc::IbcAction; use penumbra_keys::Address; use penumbra_num::Amount; use penumbra_proto::view::v1alpha1::{NotesForVotingRequest, NotesRequest}; -use penumbra_shielded_pool::{Note, OutputPlan, SpendPlan}; +use penumbra_shielded_pool::{Ics20Withdrawal, Note, OutputPlan, SpendPlan}; use penumbra_stake::{rate::RateData, validator}; use penumbra_stake::{IdentityKey, UndelegateClaimPlan}; use penumbra_tct as tct;