diff --git a/crates/apps/src/lib/config/genesis/chain.rs b/crates/apps/src/lib/config/genesis/chain.rs index 563802e68b4..056c347d5a9 100644 --- a/crates/apps/src/lib/config/genesis/chain.rs +++ b/crates/apps/src/lib/config/genesis/chain.rs @@ -422,6 +422,17 @@ impl Finalized { } } + pub fn get_ibc_params(&self) -> namada::ibc::parameters::IbcParameters { + let templates::IbcParams { + default_mint_limit, + default_per_epoch_throughput_limit, + } = self.parameters.ibc_params.clone(); + namada::ibc::parameters::IbcParameters { + default_mint_limit, + default_per_epoch_throughput_limit, + } + } + pub fn get_token_address(&self, alias: &Alias) -> Option<&Address> { self.tokens.token.get(alias).map(|token| &token.address) } @@ -687,6 +698,7 @@ pub struct FinalizedParameters { pub gov_params: templates::GovernanceParams, pub pgf_params: namada::governance::pgf::parameters::PgfParameters, pub eth_bridge_params: Option, + pub ibc_params: templates::IbcParams, } impl FinalizedParameters { @@ -697,6 +709,7 @@ impl FinalizedParameters { gov_params, pgf_params, eth_bridge_params, + ibc_params, }: templates::Parameters, ) -> Self { use namada::governance::pgf::parameters::PgfParameters; @@ -711,6 +724,7 @@ impl FinalizedParameters { gov_params, pgf_params: finalized_pgf_params, eth_bridge_params, + ibc_params, } } } diff --git a/crates/apps/src/lib/config/genesis/templates.rs b/crates/apps/src/lib/config/genesis/templates.rs index 010d33fb160..9aabb1f90a4 100644 --- a/crates/apps/src/lib/config/genesis/templates.rs +++ b/crates/apps/src/lib/config/genesis/templates.rs @@ -228,6 +228,7 @@ pub struct Parameters { pub gov_params: GovernanceParams, pub pgf_params: PgfParams, pub eth_bridge_params: Option, + pub ibc_params: IbcParams, } #[derive( @@ -485,6 +486,23 @@ pub struct EthBridgeParams { pub contracts: Contracts, } +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct IbcParams { + /// Default supply limit of each token + pub default_mint_limit: token::Amount, + /// Default per-epoch throughput limit of each token + pub default_per_epoch_throughput_limit: token::Amount, +} + impl TokenBalances { pub fn get(&self, addr: &GenesisAddress) -> Option { self.0.get(addr).map(|amt| amt.amount()) @@ -856,6 +874,7 @@ pub fn validate_parameters( gov_params, pgf_params, eth_bridge_params, + ibc_params, } = parameters; match parameters.denominate(tokens) { Err(e) => { @@ -873,6 +892,7 @@ pub fn validate_parameters( valid: Default::default(), }, eth_bridge_params, + ibc_params, }), } } diff --git a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs index a65c2af8e7c..c6eb3be018a 100644 --- a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -6,6 +6,7 @@ use masp_primitives::sapling::Node; use namada::governance::pgf::inflation as pgf_inflation; use namada::ledger::events::EventType; use namada::ledger::gas::{GasMetering, TxGasMeter}; +use namada::ledger::ibc; use namada::ledger::pos::namada_proof_of_stake; use namada::ledger::protocol::{self, WrapperArgs}; use namada::proof_of_stake::storage::{ @@ -111,6 +112,8 @@ where &mut self.wl_storage, current_epoch, )?; + + ibc::clear_throughputs(&mut self.wl_storage)?; } // Get the actual votes from cometBFT in the preferred format diff --git a/crates/apps/src/lib/node/ledger/shell/init_chain.rs b/crates/apps/src/lib/node/ledger/shell/init_chain.rs index a881171b51f..7e29f68f4bd 100644 --- a/crates/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/crates/apps/src/lib/node/ledger/shell/init_chain.rs @@ -222,6 +222,10 @@ where .unwrap(); } + // Initialize IBC parameters + let ibc_params = genesis.get_ibc_params(); + ibc_params.init_storage(&mut self.wl_storage).unwrap(); + // Depends on parameters being initialized self.wl_storage .storage diff --git a/crates/ibc/src/context/common.rs b/crates/ibc/src/context/common.rs index 23b60692b74..e16c9c3d1d0 100644 --- a/crates/ibc/src/context/common.rs +++ b/crates/ibc/src/context/common.rs @@ -725,54 +725,21 @@ pub trait IbcCommonContext: IbcStorageContext { Ok(amount == Some(Amount::from_u64(1))) } - /// Read the mint limit of the given token - fn mint_limit(&self, token: &Address) -> Result { - let key = storage::mint_limit_key(token).map_err(|e| { - ChannelError::Other { - description: format!( - "Getting the deposit limit key failed: {e}" - ), - } - })?; - Ok(self.read::(&key)?.unwrap_or_default()) - } - - /// Read the per-epoch throughput limit of the given token - fn throughput_limit(&self, token: &Address) -> Result { - let key = storage::throughput_limit_key(token).map_err(|e| { - ChannelError::Other { - description: format!( - "Getting the throughput limit key failed: {e}" - ), - } - })?; - Ok(self.read::(&key)?.unwrap_or_default()) - } - /// Read the per-epoch deposit of the given token fn deposit(&self, token: &Address) -> Result { - let key = - storage::deposit_key(token).map_err(|e| ChannelError::Other { - description: format!("Getting the deposit key failed: {e}"), - })?; + let key = storage::deposit_key(token); Ok(self.read::(&key)?.unwrap_or_default()) } /// Write the per-epoch deposit of the given token fn store_deposit(&mut self, token: &Address, amount: Amount) -> Result<()> { - let key = - storage::deposit_key(token).map_err(|e| ChannelError::Other { - description: format!("Getting the deposit key failed: {e}"), - })?; + let key = storage::deposit_key(token); self.write(&key, amount).map_err(ContextError::from) } /// Read the per-epoch withdraw of the given token fn withdraw(&self, token: &Address) -> Result { - let key = - storage::withdraw_key(token).map_err(|e| ChannelError::Other { - description: format!("Getting the withdraw key failed: {e}"), - })?; + let key = storage::withdraw_key(token); Ok(self.read::(&key)?.unwrap_or_default()) } @@ -782,10 +749,7 @@ pub trait IbcCommonContext: IbcStorageContext { token: &Address, amount: Amount, ) -> Result<()> { - let key = - storage::withdraw_key(token).map_err(|e| ChannelError::Other { - description: format!("Getting the withdraw key failed: {e}"), - })?; + let key = storage::withdraw_key(token); self.write(&key, amount).map_err(ContextError::from) } } diff --git a/crates/ibc/src/lib.rs b/crates/ibc/src/lib.rs index b12379f6fa9..134e38de5e1 100644 --- a/crates/ibc/src/lib.rs +++ b/crates/ibc/src/lib.rs @@ -2,6 +2,7 @@ mod actions; pub mod context; +pub mod parameters; pub mod storage; use std::cell::RefCell; diff --git a/crates/ibc/src/parameters.rs b/crates/ibc/src/parameters.rs new file mode 100644 index 00000000000..b1c6dcbd61e --- /dev/null +++ b/crates/ibc/src/parameters.rs @@ -0,0 +1,34 @@ +//! IBC system parameters + +use namada_core::borsh::{BorshDeserialize, BorshSerialize}; +use namada_core::types::token::Amount; +use namada_state::{StorageRead, StorageResult, StorageWrite}; + +#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] +/// Governance parameter structure +pub struct IbcParameters { + /// Default supply limit of each token + pub default_mint_limit: Amount, + /// Default per-epoch throughput limit of each token + pub default_per_epoch_throughput_limit: Amount, +} + +impl Default for IbcParameters { + fn default() -> Self { + Self { + default_mint_limit: Amount::zero(), + default_per_epoch_throughput_limit: Amount::zero(), + } + } +} + +impl IbcParameters { + /// Initialize IBC parameters into storage + pub fn init_storage(&self, storage: &mut S) -> StorageResult<()> + where + S: StorageRead + StorageWrite, + { + let key = crate::storage::params_key(); + storage.write(&key, self) + } +} diff --git a/crates/ibc/src/storage.rs b/crates/ibc/src/storage.rs index 271327409f0..5e566792849 100644 --- a/crates/ibc/src/storage.rs +++ b/crates/ibc/src/storage.rs @@ -27,6 +27,7 @@ const COUNTER_SEG: &str = "counter"; const TRACE: &str = "ibc_trace"; const NFT_CLASS: &str = "nft_class"; const NFT_METADATA: &str = "nft_meta"; +const PARAMS: &str = "params"; const MINT_LIMIT: &str = "mint_limit"; const THROUGHPUT_LIMIT: &str = "throughput_limit"; const DEPOSIT: &str = "deposit"; @@ -41,8 +42,6 @@ pub enum Error { InvalidKey(String), #[error("Port capability error: {0}")] InvalidPortCapability(String), - #[error("Invalid address error: {0}")] - Address(String), } /// IBC storage functions result @@ -490,78 +489,59 @@ pub fn is_ibc_counter_key(key: &Key) -> bool { ) } +/// Returns a key of IBC parameters +pub fn params_key() -> Key { + Key::from(Address::Internal(InternalAddress::Ibc).to_db_key()) + .push(&PARAMS.to_string().to_db_key()) + .expect("Cannot obtain a storage key") +} + /// Returns a key of the deposit limit for the token -pub fn mint_limit_key(token: &Address) -> Result { - let hash = match token { - Address::Internal(InternalAddress::IbcToken(hash)) => hash, - _ => { - return Err(Error::Address(format!( - "token is not an IbcToken: {token}" - ))); - } - }; - Ok( - Key::from(Address::Internal(InternalAddress::Ibc).to_db_key()) - .push(&MINT_LIMIT.to_string().to_db_key()) - .expect("Cannot obtain a storage key") - .push(&hash.to_string().to_db_key()) - .expect("Cannot obtain a storage key"), - ) +pub fn mint_limit_key(token: &Address) -> Key { + Key::from(Address::Internal(InternalAddress::Ibc).to_db_key()) + .push(&MINT_LIMIT.to_string().to_db_key()) + .expect("Cannot obtain a storage key") + // Set as String to avoid checking the token address + .push(&token.to_string().to_db_key()) + .expect("Cannot obtain a storage key") } /// Returns a key of the per-epoch throughput limit for the token -pub fn throughput_limit_key(token: &Address) -> Result { - let hash = match token { - Address::Internal(InternalAddress::IbcToken(hash)) => hash, - _ => { - return Err(Error::Address(format!( - "token is not an IbcToken: {token}" - ))); - } - }; - Ok( - Key::from(Address::Internal(InternalAddress::Ibc).to_db_key()) - .push(&THROUGHPUT_LIMIT.to_string().to_db_key()) - .expect("Cannot obtain a storage key") - .push(&hash.to_string().to_db_key()) - .expect("Cannot obtain a storage key"), - ) +pub fn throughput_limit_key(token: &Address) -> Key { + Key::from(Address::Internal(InternalAddress::Ibc).to_db_key()) + .push(&THROUGHPUT_LIMIT.to_string().to_db_key()) + .expect("Cannot obtain a storage key") + // Set as String to avoid checking the token address + .push(&token.to_string().to_db_key()) + .expect("Cannot obtain a storage key") +} + +/// Returns a prefix of the per-epoch deposit +pub fn deposit_prefix() -> Key { + Key::from(Address::Internal(InternalAddress::Ibc).to_db_key()) + .push(&DEPOSIT.to_string().to_db_key()) + .expect("Cannot obtain a storage key") } /// Returns a key of the per-epoch deposit for the token -pub fn deposit_key(token: &Address) -> Result { - let hash = match token { - Address::Internal(InternalAddress::IbcToken(hash)) => hash, - _ => { - return Err(Error::Address(format!( - "token is not an IbcToken: {token}" - ))); - } - }; - Ok( - Key::from(Address::Internal(InternalAddress::Ibc).to_db_key()) - .push(&DEPOSIT.to_string().to_db_key()) - .expect("Cannot obtain a storage key") - .push(&hash.to_string().to_db_key()) - .expect("Cannot obtain a storage key"), - ) +pub fn deposit_key(token: &Address) -> Key { + deposit_prefix() + // Set as String to avoid checking the token address + .push(&token.to_string().to_db_key()) + .expect("Cannot obtain a storage key") +} + +/// Returns a prefix of the per-epoch withdraw +pub fn withdraw_prefix() -> Key { + Key::from(Address::Internal(InternalAddress::Ibc).to_db_key()) + .push(&WITHDRAW.to_string().to_db_key()) + .expect("Cannot obtain a storage key") } /// Returns a key of the per-epoch withdraw for the token -pub fn withdraw_key(token: &Address) -> Result { - let hash = match token { - Address::Internal(InternalAddress::IbcToken(hash)) => hash, - _ => { - return Err(Error::Address(format!( - "token is not an IbcToken: {token}" - ))); - } - }; - Ok( - Key::from(Address::Internal(InternalAddress::Ibc).to_db_key()) - .push(&WITHDRAW.to_string().to_db_key()) - .expect("Cannot obtain a storage key") - .push(&hash.to_string().to_db_key()) - .expect("Cannot obtain a storage key"), - ) +pub fn withdraw_key(token: &Address) -> Key { + withdraw_prefix() + // Set as String to avoid checking the token address + .push(&token.to_string().to_db_key()) + .expect("Cannot obtain a storage key") } diff --git a/crates/namada/src/ledger/ibc/mod.rs b/crates/namada/src/ledger/ibc/mod.rs index 45f987e7a88..3e520e87911 100644 --- a/crates/namada/src/ledger/ibc/mod.rs +++ b/crates/namada/src/ledger/ibc/mod.rs @@ -1,10 +1,14 @@ //! IBC integration -pub use namada_ibc::storage; +use namada_core::types::token::Amount; use namada_ibc::storage::{ channel_counter_key, client_counter_key, connection_counter_key, + deposit_prefix, withdraw_prefix, +}; +pub use namada_ibc::{parameters, storage}; +use namada_state::{ + Key, StorageError, StorageHasher, StorageRead, StorageWrite, WlStorage, }; -use namada_state::{StorageHasher, StorageWrite, WlStorage}; /// Initialize storage in the genesis block. pub fn init_genesis_storage(storage: &mut WlStorage) @@ -35,3 +39,26 @@ where .write(&key, init_value) .expect("Unable to write the initial channel counter"); } + +/// Clear the per-epoch throughputs (deposit and withdraw) +pub fn clear_throughputs( + storage: &mut WlStorage, +) -> Result<(), StorageError> +where + DB: namada_state::DB + for<'iter> namada_state::DBIter<'iter> + 'static, + H: StorageHasher + 'static, +{ + for prefix in [deposit_prefix(), withdraw_prefix()] { + let keys: Vec = storage + .iter_prefix(&prefix)? + .map(|(key, _, _)| { + Key::parse(key).expect("The key should be parsable") + }) + .collect(); + for key in keys { + storage.write(&key, Amount::from(0))?; + } + } + + Ok(()) +} diff --git a/crates/namada/src/ledger/native_vp/ibc/mod.rs b/crates/namada/src/ledger/native_vp/ibc/mod.rs index edff957971e..c23503ad758 100644 --- a/crates/namada/src/ledger/native_vp/ibc/mod.rs +++ b/crates/namada/src/ledger/native_vp/ibc/mod.rs @@ -10,6 +10,7 @@ use std::time::Duration; use context::{PseudoExecutionContext, VpValidationContext}; use namada_core::types::storage::Key; use namada_gas::{IBC_ACTION_EXECUTE_GAS, IBC_ACTION_VALIDATE_GAS}; +use namada_ibc::parameters::IbcParameters; use namada_ibc::{ Error as ActionError, IbcActions, NftTransferModule, TransferModule, ValidationParams, @@ -24,12 +25,12 @@ use thiserror::Error; use crate::ibc::core::host::types::identifiers::ChainId as IbcChainId; use crate::ledger::ibc::storage::{ calc_hash, deposit_key, is_ibc_key, is_ibc_trace_key, mint_limit_key, - throughput_limit_key, withdraw_key, + params_key, throughput_limit_key, withdraw_key, }; use crate::ledger::native_vp::{self, Ctx, NativeVp}; use crate::ledger::parameters::read_epoch_duration_parameter; use crate::token::storage_key::{is_any_token_balance_key, minted_balance_key}; -use crate::types::address::{Address, InternalAddress}; +use crate::types::address::Address; use crate::types::token::Amount; use crate::vm::WasmCacheAccess; @@ -48,6 +49,8 @@ pub enum Error { StateChange(String), #[error("IBC event error: {0}")] IbcEvent(String), + #[error("IBC rate limit: {0}")] + RateLimit(String), } /// IBC functions result @@ -224,20 +227,43 @@ where } fn check_limits(&self, keys_changed: &BTreeSet) -> VpResult { - for token in keys_changed.iter().filter_map(|k| { - is_any_token_balance_key(k).and_then(|[token, _]| match token { - Address::Internal(InternalAddress::IbcToken(_)) => Some(token), - _ => None, - }) - }) { - // Check the supply - let mint_limit_key = - mint_limit_key(token).expect("The token should be an IbcToken"); - let mint_limit: Amount = self + let mut tokens: Vec<&Address> = keys_changed + .iter() + .filter_map(|k| is_any_token_balance_key(k).map(|[key, _]| key)) + .collect(); + tokens.sort(); + tokens.dedup(); + for token in tokens { + // Limits + let mint_limit_key = mint_limit_key(token); + let mint_limit: Option = self .ctx .read_pre(&mint_limit_key) - .map_err(Error::NativeVpError)? - .unwrap_or_default(); + .map_err(Error::NativeVpError)?; + let throughput_limit_key = throughput_limit_key(token); + let throughput_limit: Option = self + .ctx + .read_pre(&throughput_limit_key) + .map_err(Error::NativeVpError)?; + let (mint_limit, throughput_limit) = + match (mint_limit, throughput_limit) { + (Some(ml), Some(tl)) => (ml, tl), + _ => { + let params: IbcParameters = self + .ctx + .read_pre(¶ms_key()) + .map_err(Error::NativeVpError)? + .expect("Parameters should be stored"); + ( + mint_limit.unwrap_or(params.default_mint_limit), + throughput_limit.unwrap_or( + params.default_per_epoch_throughput_limit, + ), + ) + } + }; + + // Check the supply let minted_balance_key = minted_balance_key(token); let minted: Amount = self .ctx @@ -245,30 +271,20 @@ where .map_err(Error::NativeVpError)? .unwrap_or_default(); if mint_limit < minted { - tracing::debug!( + return Err(Error::RateLimit(format!( "Transfer exceeding the mint limit is not allowed: Mint \ limit {mint_limit}, minted amount {minted}" - ); - return Ok(false); + ))); } - // Check the per-epoch throughput - let throughput_limit_key = throughput_limit_key(token) - .expect("The token should be an IbcToken"); - let throughput_limit: Amount = self - .ctx - .read_pre(&throughput_limit_key) - .map_err(Error::NativeVpError)? - .unwrap_or_default(); - let deposit_key = - deposit_key(token).expect("The token should be an IbcToken"); + // Check the rate limit + let deposit_key = deposit_key(token); let deposit: Amount = self .ctx .read_post(&deposit_key) .map_err(Error::NativeVpError)? .unwrap_or_default(); - let withdraw_key = - withdraw_key(token).expect("The token should be an IbcToken"); + let withdraw_key = withdraw_key(token); let withdraw: Amount = self .ctx .read_post(&withdraw_key) @@ -280,16 +296,15 @@ where .expect("withdraw should be bigger than deposit") } else { deposit - .checked_sub(deposit) + .checked_sub(withdraw) .expect("deposit should be bigger than withdraw") }; if throughput_limit < diff { - tracing::debug!( + return Err(Error::RateLimit(format!( "Transfer exceeding the per-epoch throughput limit is not \ allowed: Per-epoch throughput limit {throughput_limit}, \ actual throughput {diff}" - ); - return Ok(false); + ))); } } Ok(true) @@ -405,6 +420,7 @@ mod tests { use namada_governance::parameters::GovernanceParameters; use namada_state::testing::TestWlStorage; use namada_state::StorageRead; + use namada_token::NATIVE_MAX_DECIMAL_PLACES; use namada_tx::data::TxType; use namada_tx::{Code, Data, Section, Signature, Tx}; use prost::Message; @@ -489,7 +505,7 @@ mod tests { ack_key, calc_hash, channel_counter_key, channel_key, client_connections_key, client_counter_key, client_state_key, client_update_height_key, client_update_timestamp_key, commitment_key, - connection_counter_key, connection_key, consensus_state_key, + connection_counter_key, connection_key, consensus_state_key, ibc_token, ibc_trace_key, next_sequence_ack_key, next_sequence_recv_key, next_sequence_send_key, nft_class_key, nft_metadata_key, receipt_key, }; @@ -524,6 +540,11 @@ mod tests { ibc::init_genesis_storage(&mut wl_storage); let gov_params = GovernanceParameters::default(); gov_params.init_storage(&mut wl_storage).unwrap(); + let ibc_params = IbcParameters { + default_mint_limit: Amount::native_whole(100), + default_per_epoch_throughput_limit: Amount::native_whole(100), + }; + ibc_params.init_storage(&mut wl_storage).unwrap(); pos::test_utils::test_init_genesis( &mut wl_storage, namada_proof_of_stake::OwnedPosParams::default(), @@ -2183,7 +2204,7 @@ mod tests { packet_data: PacketData { token: PrefixedCoin { denom: nam().to_string().parse().unwrap(), - amount: 100u64.into(), + amount: 100.into(), }, sender: sender.to_string().into(), receiver: "receiver".to_string().into(), @@ -2213,6 +2234,19 @@ mod tests { .write(&commitment_key, bytes) .expect("write failed"); keys_changed.insert(commitment_key); + // withdraw + let withdraw_key = withdraw_key(&nam()); + let bytes = Amount::from_str( + msg.packet_data.token.amount.to_string(), + NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap() + .serialize_to_vec(); + wl_storage + .write_log + .write(&withdraw_key, bytes) + .expect("write failed"); + keys_changed.insert(withdraw_key); // event let transfer_event = TransferEvent { sender: msg.packet_data.sender.clone(), @@ -2364,12 +2398,24 @@ mod tests { .write(&ack_key, bytes) .expect("write failed"); keys_changed.insert(ack_key); - // denom + let mut coin = transfer_msg.packet_data.token; coin.denom.add_trace_prefix(TracePrefix::new( packet.port_id_on_b.clone(), packet.chan_id_on_b.clone(), )); + // deposit + let ibc_token = ibc_token(coin.denom.to_string()); + let deposit_key = deposit_key(&ibc_token); + let bytes = Amount::from_str(coin.amount.to_string(), 0) + .unwrap() + .serialize_to_vec(); + wl_storage + .write_log + .write(&deposit_key, bytes) + .expect("write failed"); + keys_changed.insert(deposit_key); + // denom let trace_hash = calc_hash(coin.denom.to_string()); let trace_key = ibc_trace_key(receiver.to_string(), &trace_hash); let bytes = coin.denom.to_string().serialize_to_vec(); @@ -2705,9 +2751,22 @@ mod tests { .delete(&commitment_key) .expect("delete failed"); keys_changed.insert(commitment_key); - // event + // deposit let data = serde_json::from_slice::(&packet.data) .expect("decoding packet data failed"); + let deposit_key = deposit_key(&nam()); + let bytes = Amount::from_str( + data.token.amount.to_string(), + NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap() + .serialize_to_vec(); + wl_storage + .write_log + .write(&deposit_key, bytes) + .expect("write failed"); + keys_changed.insert(deposit_key); + // event let timeout_event = TimeoutEvent { refund_receiver: data.sender, refund_denom: data.token.denom, @@ -2857,9 +2916,22 @@ mod tests { .delete(&commitment_key) .expect("delete failed"); keys_changed.insert(commitment_key); - // event + // deposit let data = serde_json::from_slice::(&packet.data) .expect("decoding packet data failed"); + let deposit_key = deposit_key(&nam()); + let bytes = Amount::from_str( + data.token.amount.to_string(), + NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap() + .serialize_to_vec(); + wl_storage + .write_log + .write(&deposit_key, bytes) + .expect("write failed"); + keys_changed.insert(deposit_key); + // event let timeout_event = TimeoutEvent { refund_receiver: data.sender, refund_denom: data.token.denom, @@ -3020,6 +3092,14 @@ mod tests { .write(&commitment_key, bytes) .expect("write failed"); keys_changed.insert(commitment_key); + // withdraw + let withdraw_key = withdraw_key(&ibc_token); + let bytes = Amount::from_u64(1).serialize_to_vec(); + wl_storage + .write_log + .write(&withdraw_key, bytes) + .expect("write failed"); + keys_changed.insert(withdraw_key); // event let transfer_event = NftTransferEvent { sender: msg.packet_data.sender.clone(), @@ -3219,6 +3299,15 @@ mod tests { .write(&metadata_key, bytes) .expect("write failed"); keys_changed.insert(metadata_key); + // deposit + let ibc_token = ibc_token(&ibc_trace); + let deposit_key = deposit_key(&ibc_token); + let bytes = Amount::from_u64(1).serialize_to_vec(); + wl_storage + .write_log + .write(&deposit_key, bytes) + .expect("write failed"); + keys_changed.insert(deposit_key); // event let recv_event = NftRecvEvent { sender: sender.to_string().into(), diff --git a/crates/tests/src/e2e/ibc_tests.rs b/crates/tests/src/e2e/ibc_tests.rs index 9f8c2dd0927..01c63eb17e1 100644 --- a/crates/tests/src/e2e/ibc_tests.rs +++ b/crates/tests/src/e2e/ibc_tests.rs @@ -109,6 +109,11 @@ fn run_ledger_ibc() -> Result<()> { let update_genesis = |mut genesis: templates::All, base_dir: &_| { genesis.parameters.parameters.epochs_per_year = 31536; + genesis.parameters.ibc_params.default_mint_limit = Amount::max(); + genesis + .parameters + .ibc_params + .default_per_epoch_throughput_limit = Amount::max(); setup::set_validators(1, genesis, base_dir, |_| 0) }; let (ledger_a, ledger_b, test_a, test_b) = run_two_nets(update_genesis)?; @@ -205,6 +210,11 @@ fn run_ledger_ibc_with_hermes() -> Result<()> { let update_genesis = |mut genesis: templates::All, base_dir: &_| { genesis.parameters.parameters.epochs_per_year = 31536; + genesis.parameters.ibc_params.default_mint_limit = Amount::max(); + genesis + .parameters + .ibc_params + .default_per_epoch_throughput_limit = Amount::max(); setup::set_validators(1, genesis, base_dir, |_| 0) }; let (ledger_a, ledger_b, test_a, test_b) = run_two_nets(update_genesis)?; @@ -382,6 +392,11 @@ fn pgf_over_ibc_with_hermes() -> Result<()> { ALBERT_KEY, base_dir, &genesis, ) .unwrap()]); + genesis.parameters.ibc_params.default_mint_limit = Amount::max(); + genesis + .parameters + .ibc_params + .default_per_epoch_throughput_limit = Amount::max(); setup::set_validators(1, genesis, base_dir, |_| 0) }; let (ledger_a, ledger_b, test_a, test_b) = run_two_nets(update_genesis)?; @@ -443,6 +458,127 @@ fn pgf_over_ibc_with_hermes() -> Result<()> { Ok(()) } +#[test] +fn ibc_rate_limit() -> Result<()> { + // Mint limit 2 NAM, per-epoch throughput limit 1 NAM + let update_genesis = + |mut genesis: templates::All, base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = + epochs_per_year_from_min_duration(10); + // for the trusting period of IBC client + genesis.parameters.pos_params.pipeline_len = 10; + genesis.parameters.ibc_params.default_mint_limit = + Amount::from_u64(2_000_000); + genesis + .parameters + .ibc_params + .default_per_epoch_throughput_limit = + Amount::from_u64(1_000_000); + setup::set_validators(1, genesis, base_dir, |_| 0) + }; + let (ledger_a, ledger_b, test_a, test_b) = run_two_nets(update_genesis)?; + let _bg_ledger_a = ledger_a.background(); + let _bg_ledger_b = ledger_b.background(); + + setup_hermes(&test_a, &test_b)?; + let port_id_a = "transfer".parse().unwrap(); + let (channel_id_a, _channel_id_b) = + create_channel_with_hermes(&test_a, &test_b)?; + + // Start relaying + let hermes = run_hermes(&test_a)?; + let _bg_hermes = hermes.background(); + + // Transfer 1 NAM from Chain A to Chain B + std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); + let receiver = find_address(&test_b, BERTHA)?; + transfer( + &test_a, + ALBERT, + receiver.to_string(), + NAM, + "1", + ALBERT_KEY, + &port_id_a, + &channel_id_a, + None, + None, + false, + )?; + // This transfer should succeed + wait_for_packet_relay(&port_id_a, &channel_id_a, &test_a)?; + + // Transfer 1 NAM from Chain A to Chain B again will fail + transfer( + &test_a, + ALBERT, + receiver.to_string(), + NAM, + "1", + ALBERT_KEY, + &port_id_a, + &channel_id_a, + None, + // expect an error of the throughput limit + Some(&format!("throughput limit")), + false, + )?; + + // wait for the next epoch + let rpc_a = get_actor_rpc(&test_a, Who::Validator(0)); + let mut epoch = get_epoch(&test_a, &rpc_a).unwrap(); + let next_epoch = epoch.next(); + while epoch <= next_epoch { + sleep(5); + epoch = get_epoch(&test_a, &rpc_a).unwrap(); + } + + // Transfer 1 NAM from Chain A to Chain B will succeed + transfer( + &test_a, + ALBERT, + receiver.to_string(), + NAM, + "1", + ALBERT_KEY, + &port_id_a, + &channel_id_a, + None, + None, + false, + )?; + // This transfer should succeed + wait_for_packet_relay(&port_id_a, &channel_id_a, &test_a)?; + + // wait for the next epoch + let mut epoch = get_epoch(&test_a, &rpc_a).unwrap(); + let next_epoch = epoch.next(); + while epoch <= next_epoch { + sleep(5); + epoch = get_epoch(&test_a, &rpc_a).unwrap(); + } + + // Transfer 1 NAM from Chain A to Chain B will fail + transfer( + &test_a, + ALBERT, + receiver.to_string(), + NAM, + "1", + ALBERT_KEY, + &port_id_a, + &channel_id_a, + None, + // expect an error of the mint limit + Some(&format!("mint limit")), + false, + )?; + // This transfer should succeed + wait_for_packet_relay(&port_id_a, &channel_id_a, &test_a)?; + + Ok(()) +} + fn run_two_nets( update_genesis: impl FnMut( templates::All, diff --git a/crates/tests/src/vm_host_env/ibc.rs b/crates/tests/src/vm_host_env/ibc.rs index e11fa33e227..7909a2f37a2 100644 --- a/crates/tests/src/vm_host_env/ibc.rs +++ b/crates/tests/src/vm_host_env/ibc.rs @@ -49,6 +49,7 @@ pub use namada::ibc::core::host::types::identifiers::{ use namada::ibc::primitives::proto::{Any, Protobuf}; use namada::ibc::primitives::Timestamp; use namada::ledger::gas::VpGasMeter; +use namada::ledger::ibc::parameters::IbcParameters; pub use namada::ledger::ibc::storage::{ ack_key, channel_counter_key, channel_key, client_counter_key, client_state_key, client_update_height_key, client_update_timestamp_key, @@ -212,6 +213,11 @@ pub fn init_storage() -> (Address, Address) { ibc::init_genesis_storage(&mut env.wl_storage); let gov_params = GovernanceParameters::default(); gov_params.init_storage(&mut env.wl_storage).unwrap(); + let ibc_params = IbcParameters { + default_mint_limit: Amount::native_whole(100), + default_per_epoch_throughput_limit: Amount::native_whole(100), + }; + ibc_params.init_storage(&mut env.wl_storage).unwrap(); pos::test_utils::test_init_genesis( &mut env.wl_storage, OwnedPosParams::default(), diff --git a/genesis/localnet/parameters.toml b/genesis/localnet/parameters.toml index f99f9e0a50b..7843cc7e06e 100644 --- a/genesis/localnet/parameters.toml +++ b/genesis/localnet/parameters.toml @@ -101,3 +101,10 @@ stewards = [ pgf_inflation_rate = "0.1" # The pgf stewards inflation rate stewards_inflation_rate = "0.01" + +# IBC parameters +[ibc_params] +# default mint limit of each token +default_mint_limit = "0" +# default per-epoch throughput limit of each token +default_per_epoch_throughput_limit = "0" diff --git a/genesis/starter/parameters.toml b/genesis/starter/parameters.toml index ccbf9c95bb0..c0d19923ce4 100644 --- a/genesis/starter/parameters.toml +++ b/genesis/starter/parameters.toml @@ -96,3 +96,10 @@ stewards = [] pgf_inflation_rate = "0.1" # The pgf stewards inflation rate stewards_inflation_rate = "0.01" + +# IBC parameters +[ibc_params] +# default mint limit of each token +default_mint_limit = "0" +# default per-epoch throughput limit of each token +default_per_epoch_throughput_limit = "0"