diff --git a/.github/workflows/scripts/e2e.json b/.github/workflows/scripts/e2e.json index d9b43599d96..3769cc34456 100644 --- a/.github/workflows/scripts/e2e.json +++ b/.github/workflows/scripts/e2e.json @@ -3,6 +3,7 @@ "e2e::ibc_tests::run_ledger_ibc": 155, "e2e::ibc_tests::run_ledger_ibc_with_hermes": 130, "e2e::ibc_tests::pgf_over_ibc_with_hermes": 240, + "e2e::ibc_tests::ibc_rate_limit": 500, "e2e::eth_bridge_tests::test_add_to_bridge_pool": 10, "e2e::ledger_tests::double_signing_gets_slashed": 12, "e2e::ledger_tests::invalid_transactions": 13, @@ -36,4 +37,4 @@ "e2e::wallet_tests::wallet_encrypted_key_cmds": 1, "e2e::wallet_tests::wallet_encrypted_key_cmds_env_var": 1, "e2e::wallet_tests::wallet_unencrypted_key_cmds": 1 -} \ No newline at end of file +} diff --git a/crates/ibc/src/context/common.rs b/crates/ibc/src/context/common.rs index e16c9c3d1d0..7e10cfd6969 100644 --- a/crates/ibc/src/context/common.rs +++ b/crates/ibc/src/context/common.rs @@ -725,6 +725,22 @@ pub trait IbcCommonContext: IbcStorageContext { Ok(amount == Some(Amount::from_u64(1))) } + /// Read the mint amount of the given token + fn mint_amount(&self, token: &Address) -> Result { + let key = storage::mint_amount_key(token); + Ok(self.read::(&key)?.unwrap_or_default()) + } + + /// Write the mint amount of the given token + fn store_mint_amount( + &mut self, + token: &Address, + amount: Amount, + ) -> Result<()> { + let key = storage::mint_amount_key(token); + self.write(&key, amount).map_err(ContextError::from) + } + /// Read the per-epoch deposit of the given token fn deposit(&self, token: &Address) -> Result { let key = storage::deposit_key(token); diff --git a/crates/ibc/src/context/nft_transfer.rs b/crates/ibc/src/context/nft_transfer.rs index 5a6a56c3c82..c0878d20b71 100644 --- a/crates/ibc/src/context/nft_transfer.rs +++ b/crates/ibc/src/context/nft_transfer.rs @@ -38,6 +38,32 @@ where Self { inner } } + /// Update the mint amount of the token + fn update_mint_amount( + &self, + token: &Address, + is_minted: bool, + ) -> Result<(), NftTransferError> { + let mint = self.inner.borrow().mint_amount(token)?; + let updated_mint = if is_minted { + mint.checked_add(Amount::from_u64(1)).ok_or_else(|| { + NftTransferError::Other( + "The mint amount overflowed".to_string(), + ) + })? + } else { + mint.checked_sub(Amount::from_u64(1)).ok_or_else(|| { + NftTransferError::Other( + "The mint amount underflowed".to_string(), + ) + })? + }; + self.inner + .borrow_mut() + .store_mint_amount(token, updated_mint) + .map_err(NftTransferError::from) + } + /// Add the amount to the per-epoch withdraw of the token fn add_deposit(&self, token: &Address) -> Result<(), NftTransferError> { let deposit = self.inner.borrow().deposit(token)?; @@ -317,6 +343,7 @@ where }; self.inner.borrow_mut().store_nft_metadata(metadata)?; + self.update_mint_amount(&ibc_token, true)?; self.add_deposit(&ibc_token)?; self.inner @@ -334,6 +361,7 @@ where ) -> Result<(), NftTransferError> { let ibc_token = storage::ibc_token_for_nft(class_id, token_id); + self.update_mint_amount(&ibc_token, false)?; self.add_withdraw(&ibc_token)?; self.inner diff --git a/crates/ibc/src/context/token_transfer.rs b/crates/ibc/src/context/token_transfer.rs index 59bd8652206..4d527496525 100644 --- a/crates/ibc/src/context/token_transfer.rs +++ b/crates/ibc/src/context/token_transfer.rs @@ -69,6 +69,33 @@ where Ok((token, amount)) } + /// Update the mint amount of the token + fn update_mint_amount( + &self, + token: &Address, + amount: Amount, + is_minted: bool, + ) -> Result<(), TokenTransferError> { + let mint = self.inner.borrow().mint_amount(token)?; + let updated_mint = if is_minted { + mint.checked_add(amount).ok_or_else(|| { + TokenTransferError::Other( + "The mint amount overflowed".to_string(), + ) + })? + } else { + mint.checked_sub(amount).ok_or_else(|| { + TokenTransferError::Other( + "The mint amount underflowed".to_string(), + ) + })? + }; + self.inner + .borrow_mut() + .store_mint_amount(token, updated_mint) + .map_err(TokenTransferError::from) + } + /// Add the amount to the per-epoch withdraw of the token fn add_deposit( &self, @@ -223,6 +250,7 @@ where // The trace path of the denom is already updated if receiving the token let (ibc_token, amount) = self.get_token_amount(coin)?; + self.update_mint_amount(&ibc_token, amount, true)?; self.add_deposit(&ibc_token, amount)?; self.inner @@ -239,6 +267,7 @@ where ) -> Result<(), TokenTransferError> { let (ibc_token, amount) = self.get_token_amount(coin)?; + self.update_mint_amount(&ibc_token, amount, false)?; self.add_withdraw(&ibc_token, amount)?; // The burn is "unminting" from the minted balance diff --git a/crates/ibc/src/storage.rs b/crates/ibc/src/storage.rs index 5e566792849..82e17144c23 100644 --- a/crates/ibc/src/storage.rs +++ b/crates/ibc/src/storage.rs @@ -29,6 +29,7 @@ const NFT_CLASS: &str = "nft_class"; const NFT_METADATA: &str = "nft_meta"; const PARAMS: &str = "params"; const MINT_LIMIT: &str = "mint_limit"; +const MINT: &str = "mint"; const THROUGHPUT_LIMIT: &str = "throughput_limit"; const DEPOSIT: &str = "deposit"; const WITHDRAW: &str = "withdraw"; @@ -496,7 +497,7 @@ pub fn params_key() -> Key { .expect("Cannot obtain a storage key") } -/// Returns a key of the deposit limit for the token +/// Returns a key of the mint limit for the token 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()) @@ -506,6 +507,16 @@ pub fn mint_limit_key(token: &Address) -> Key { .expect("Cannot obtain a storage key") } +/// Returns a key of the IBC mint amount for the token +pub fn mint_amount_key(token: &Address) -> Key { + Key::from(Address::Internal(InternalAddress::Ibc).to_db_key()) + .push(&MINT.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) -> Key { Key::from(Address::Internal(InternalAddress::Ibc).to_db_key()) diff --git a/crates/namada/src/ledger/native_vp/ibc/mod.rs b/crates/namada/src/ledger/native_vp/ibc/mod.rs index c23503ad758..20c9af57163 100644 --- a/crates/namada/src/ledger/native_vp/ibc/mod.rs +++ b/crates/namada/src/ledger/native_vp/ibc/mod.rs @@ -24,12 +24,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, - params_key, throughput_limit_key, withdraw_key, + calc_hash, deposit_key, is_ibc_key, is_ibc_trace_key, mint_amount_key, + mint_limit_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::token::storage_key::is_any_token_balance_key; use crate::types::address::Address; use crate::types::token::Amount; use crate::vm::WasmCacheAccess; @@ -264,10 +264,10 @@ where }; // Check the supply - let minted_balance_key = minted_balance_key(token); + let mint_amount_key = mint_amount_key(token); let minted: Amount = self .ctx - .read_post(&minted_balance_key) + .read_post(&mint_amount_key) .map_err(Error::NativeVpError)? .unwrap_or_default(); if mint_limit < minted { diff --git a/crates/tests/src/e2e/ibc_tests.rs b/crates/tests/src/e2e/ibc_tests.rs index 01c63eb17e1..2a9002082f5 100644 --- a/crates/tests/src/e2e/ibc_tests.rs +++ b/crates/tests/src/e2e/ibc_tests.rs @@ -460,35 +460,44 @@ fn pgf_over_ibc_with_hermes() -> Result<()> { #[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) - }; + // Mint limit 2 transfer/channel-0/nam, per-epoch throughput limit 1 NAM + let update_genesis = |mut genesis: templates::All< + templates::Unvalidated, + >, + base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = + epochs_per_year_from_min_duration(50); + genesis.parameters.ibc_params.default_mint_limit = Amount::from_u64(2); + 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) = + let port_id_b: PortId = "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(); + // wait for the next epoch + std::env::set_var(ENV_VAR_CHAIN_ID, test_a.net.chain_id.to_string()); + 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 std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); let receiver = find_address(&test_b, BERTHA)?; @@ -505,8 +514,6 @@ fn ibc_rate_limit() -> Result<()> { 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( @@ -520,12 +527,11 @@ fn ibc_rate_limit() -> Result<()> { &channel_id_a, None, // expect an error of the throughput limit - Some(&format!("throughput limit")), + Some("Transaction was rejected by VPs"), 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 { @@ -533,7 +539,7 @@ fn ibc_rate_limit() -> Result<()> { epoch = get_epoch(&test_a, &rpc_a).unwrap(); } - // Transfer 1 NAM from Chain A to Chain B will succeed + // Transfer 1 NAM from Chain A to Chain B will succeed in the new epoch transfer( &test_a, ALBERT, @@ -547,8 +553,6 @@ fn ibc_rate_limit() -> Result<()> { 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(); @@ -558,7 +562,8 @@ fn ibc_rate_limit() -> Result<()> { epoch = get_epoch(&test_a, &rpc_a).unwrap(); } - // Transfer 1 NAM from Chain A to Chain B will fail + // Transfer 1 NAM from Chain A to Chain B will succeed, but Chain B can't + // receive due to the mint limit and the packet will be timed out transfer( &test_a, ALBERT, @@ -568,14 +573,24 @@ fn ibc_rate_limit() -> Result<()> { ALBERT_KEY, &port_id_a, &channel_id_a, + Some(Duration::new(20, 0)), 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)?; + // Check the balance on Chain B + let ibc_denom = format!("{port_id_b}/{channel_id_b}/nam"); + std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); + let rpc_b = get_actor_rpc(&test_b, Who::Validator(0)); + let query_args = vec![ + "balance", "--owner", BERTHA, "--token", &ibc_denom, "--node", &rpc_b, + ]; + let expected = format!("{ibc_denom}: 2"); + let mut client = run!(test_b, Bin::Client, query_args, Some(40))?; + client.exp_string(&expected)?; + client.assert_success(); + Ok(()) }