diff --git a/.changelog/unreleased/features/2842-nam-transferable.md b/.changelog/unreleased/features/2842-nam-transferable.md new file mode 100644 index 0000000000..4f3561b107 --- /dev/null +++ b/.changelog/unreleased/features/2842-nam-transferable.md @@ -0,0 +1,2 @@ +- Add a parameter to enable/disable native token transfers + ([\#2842](https://github.com/anoma/namada/issues/2842)) \ No newline at end of file diff --git a/crates/apps/src/lib/config/genesis/chain.rs b/crates/apps/src/lib/config/genesis/chain.rs index c76926e4b3..31eae3acfb 100644 --- a/crates/apps/src/lib/config/genesis/chain.rs +++ b/crates/apps/src/lib/config/genesis/chain.rs @@ -295,6 +295,7 @@ impl Finalized { max_block_gas, minimum_gas_price, max_tx_bytes, + is_native_token_transferable, .. } = self.parameters.parameters.clone(); @@ -348,6 +349,7 @@ impl Finalized { ) }) .collect(), + is_native_token_transferable, } } diff --git a/crates/apps/src/lib/config/genesis/templates.rs b/crates/apps/src/lib/config/genesis/templates.rs index db1ded621e..9e8391de29 100644 --- a/crates/apps/src/lib/config/genesis/templates.rs +++ b/crates/apps/src/lib/config/genesis/templates.rs @@ -260,6 +260,8 @@ pub struct ChainParams { /// Name of the native token - this must one of the tokens from /// `tokens.toml` file pub native_token: Alias, + /// Enable the native token transfer if it is true + pub is_native_token_transferable: bool, /// Minimum number of blocks per epoch. // TODO: u64 only works with values up to i64::MAX with toml-rs! pub min_num_of_blocks: u64, @@ -311,6 +313,7 @@ impl ChainParams { let ChainParams { max_tx_bytes, native_token, + is_native_token_transferable, min_num_of_blocks, max_expected_time_per_block, max_proposal_bytes, @@ -356,6 +359,7 @@ impl ChainParams { Ok(ChainParams { max_tx_bytes, native_token, + is_native_token_transferable, min_num_of_blocks, max_expected_time_per_block, max_proposal_bytes, diff --git a/crates/apps/src/lib/node/ledger/storage/mod.rs b/crates/apps/src/lib/node/ledger/storage/mod.rs index ad61576598..30ae86f7f8 100644 --- a/crates/apps/src/lib/node/ledger/storage/mod.rs +++ b/crates/apps/src/lib/node/ledger/storage/mod.rs @@ -171,6 +171,7 @@ mod tests { fee_unshielding_gas_limit: 0, fee_unshielding_descriptions_limit: 0, minimum_gas_price: Default::default(), + is_native_token_transferable: true, }; parameters::init_storage(¶ms, &mut state).expect("Test failed"); // insert and commit diff --git a/crates/core/src/parameters.rs b/crates/core/src/parameters.rs index 38840e3fd3..56a77d68a8 100644 --- a/crates/core/src/parameters.rs +++ b/crates/core/src/parameters.rs @@ -59,6 +59,8 @@ pub struct Parameters { pub fee_unshielding_descriptions_limit: u64, /// Map of the cost per gas unit for every token allowed for fee payment pub minimum_gas_price: BTreeMap, + /// Enable the native token transfer if it is true + pub is_native_token_transferable: bool, } /// Epoch duration. A new epoch begins as soon as both the `min_num_of_blocks` diff --git a/crates/namada/Cargo.toml b/crates/namada/Cargo.toml index 0f5e6b2050..9665c8639b 100644 --- a/crates/namada/Cargo.toml +++ b/crates/namada/Cargo.toml @@ -50,6 +50,7 @@ http-client = ["tendermint-rpc/http-client"] testing = [ "namada_core/testing", "namada_ethereum_bridge/testing", + "namada_parameters/testing", "namada_proof_of_stake/testing", "namada_sdk/testing", "namada_state/testing", @@ -165,6 +166,9 @@ wasmtimer = "0.2.0" namada_core = { path = "../core", default-features = false, features = [ "testing", ] } +namada_parameters = { path = "../parameters", default-features = false, features = [ + "testing", +] } namada_ethereum_bridge = { path = "../ethereum_bridge", default-features = false, features = [ "testing", ] } diff --git a/crates/namada/src/ledger/native_vp/multitoken.rs b/crates/namada/src/ledger/native_vp/multitoken.rs index 6a1925553f..d5402bf854 100644 --- a/crates/namada/src/ledger/native_vp/multitoken.rs +++ b/crates/namada/src/ledger/native_vp/multitoken.rs @@ -4,13 +4,14 @@ use std::collections::BTreeSet; use namada_core::collections::HashMap; use namada_governance::is_proposal_accepted; +use namada_parameters::storage::is_native_token_transferable; use namada_state::StateRead; use namada_token::storage_key::is_any_token_parameter_key; use namada_tx::Tx; use namada_vp_env::VpEnv; use thiserror::Error; -use crate::address::{Address, InternalAddress}; +use crate::address::{Address, InternalAddress, GOV, POS}; use crate::ledger::native_vp::{self, Ctx, NativeVp}; use crate::storage::{Key, KeySeg}; use crate::token::storage_key::{ @@ -53,16 +54,39 @@ where keys_changed: &BTreeSet, verifiers: &BTreeSet
, ) -> Result { + let native_token = self.ctx.pre().ctx.get_native_token()?; + let is_native_token_transferable = + is_native_token_transferable(&self.ctx.pre())?; + // Native token can be transferred to `PoS` or `Gov` even if + // `is_native_token_transferable` is false + let is_allowed_inc = |token: &Address, target: &Address| -> bool { + *token != native_token + || is_native_token_transferable + || *target == POS + || *target == GOV + }; + let is_allowed_dec = |token: &Address, target: &Address| -> bool { + *token != native_token + || is_native_token_transferable + || (*target != POS && *target != GOV) + }; + let mut inc_changes: HashMap = HashMap::new(); let mut dec_changes: HashMap = HashMap::new(); let mut inc_mints: HashMap = HashMap::new(); let mut dec_mints: HashMap = HashMap::new(); for key in keys_changed { - if let Some([token, _]) = is_any_token_balance_key(key) { + if let Some([token, owner]) = is_any_token_balance_key(key) { let pre: Amount = self.ctx.read_pre(key)?.unwrap_or_default(); let post: Amount = self.ctx.read_post(key)?.unwrap_or_default(); match post.checked_sub(pre) { Some(diff) => { + if !is_allowed_inc(token, owner) { + tracing::debug!( + "Native token deposit isn't allowed" + ); + return Ok(false); + } let change = inc_changes.entry(token.clone()).or_default(); *change = @@ -75,6 +99,12 @@ where })?; } None => { + if !is_allowed_dec(token, owner) { + tracing::debug!( + "Native token withdraw isn't allowed" + ); + return Ok(false); + } let diff = pre .checked_sub(post) .expect("Underflow shouldn't happen here"); @@ -91,6 +121,13 @@ where } } } else if let Some(token) = is_any_minted_balance_key(key) { + if *token == native_token && !is_native_token_transferable { + tracing::debug!( + "Minting/Burning native token isn't allowed" + ); + return Ok(false); + } + let pre: Amount = self.ctx.read_pre(key)?.unwrap_or_default(); let post: Amount = self.ctx.read_post(key)?.unwrap_or_default(); match post.checked_sub(pre) { @@ -218,7 +255,9 @@ mod tests { use borsh_ext::BorshSerializeExt; use namada_core::validity_predicate::VpSentinel; use namada_gas::TxGasMeter; + use namada_parameters::storage::get_native_token_transferable_key; use namada_state::testing::TestState; + use namada_state::StorageWrite; use namada_tx::data::TxType; use namada_tx::{Authorization, Code, Data, Section}; @@ -235,6 +274,12 @@ mod tests { const ADDRESS: Address = Address::Internal(InternalAddress::Multitoken); + fn init_state() -> TestState { + let mut state = TestState::default(); + namada_parameters::init_test_storage(&mut state).unwrap(); + state + } + fn dummy_tx(state: &TestState) -> Tx { let tx_code = vec![]; let tx_data = vec![]; @@ -250,33 +295,44 @@ mod tests { tx } - #[test] - fn test_valid_transfer() { - let mut state = TestState::default(); + fn transfer( + state: &mut TestState, + src: &Address, + dest: &Address, + ) -> BTreeSet { let mut keys_changed = BTreeSet::new(); - let sender = established_address_1(); - let sender_key = balance_key(&nam(), &sender); + let src_key = balance_key(&nam(), src); let amount = Amount::native_whole(100); state - .db_write(&sender_key, amount.serialize_to_vec()) + .db_write(&src_key, amount.serialize_to_vec()) .expect("write failed"); // transfer 10 let amount = Amount::native_whole(90); state .write_log_mut() - .write(&sender_key, amount.serialize_to_vec()) + .write(&src_key, amount.serialize_to_vec()) .expect("write failed"); - keys_changed.insert(sender_key); - let receiver = established_address_2(); - let receiver_key = balance_key(&nam(), &receiver); + keys_changed.insert(src_key); + + let dest_key = balance_key(&nam(), dest); let amount = Amount::native_whole(10); state .write_log_mut() - .write(&receiver_key, amount.serialize_to_vec()) + .write(&dest_key, amount.serialize_to_vec()) .expect("write failed"); - keys_changed.insert(receiver_key); + keys_changed.insert(dest_key); + + keys_changed + } + + #[test] + fn test_valid_transfer() { + let mut state = init_state(); + let src = established_address_1(); + let dest = established_address_2(); + let keys_changed = transfer(&mut state, &src, &dest); let tx_index = TxIndex::default(); let tx = dummy_tx(&state); @@ -285,7 +341,7 @@ mod tests { )); let (vp_wasm_cache, _vp_cache_dir) = wasm_cache(); let mut verifiers = BTreeSet::new(); - verifiers.insert(sender); + verifiers.insert(src); let sentinel = RefCell::new(VpSentinel::default()); let ctx = Ctx::new( &ADDRESS, @@ -308,32 +364,18 @@ mod tests { #[test] fn test_invalid_transfer() { - let mut state = TestState::default(); - let mut keys_changed = BTreeSet::new(); - - let sender = established_address_1(); - let sender_key = balance_key(&nam(), &sender); - let amount = Amount::native_whole(100); - state - .db_write(&sender_key, amount.serialize_to_vec()) - .expect("write failed"); + let mut state = init_state(); + let src = established_address_1(); + let dest = established_address_2(); + let keys_changed = transfer(&mut state, &src, &dest); - // transfer 10 - let amount = Amount::native_whole(90); - state - .write_log_mut() - .write(&sender_key, amount.serialize_to_vec()) - .expect("write failed"); - keys_changed.insert(sender_key); - let receiver = established_address_2(); - let receiver_key = balance_key(&nam(), &receiver); // receive more than 10 + let dest_key = balance_key(&nam(), &dest); let amount = Amount::native_whole(100); state .write_log_mut() - .write(&receiver_key, amount.serialize_to_vec()) + .write(&dest_key, amount.serialize_to_vec()) .expect("write failed"); - keys_changed.insert(receiver_key); let tx_index = TxIndex::default(); let tx = dummy_tx(&state); @@ -364,7 +406,7 @@ mod tests { #[test] fn test_valid_mint() { - let mut state = TestState::default(); + let mut state = init_state(); let mut keys_changed = BTreeSet::new(); // IBC token @@ -427,7 +469,7 @@ mod tests { #[test] fn test_invalid_mint() { - let mut state = TestState::default(); + let mut state = init_state(); let mut keys_changed = BTreeSet::new(); // mint 100 @@ -488,7 +530,7 @@ mod tests { #[test] fn test_no_minter() { - let mut state = TestState::default(); + let mut state = init_state(); let mut keys_changed = BTreeSet::new(); // IBC token @@ -542,7 +584,7 @@ mod tests { #[test] fn test_invalid_minter() { - let mut state = TestState::default(); + let mut state = init_state(); let mut keys_changed = BTreeSet::new(); // IBC token @@ -605,7 +647,7 @@ mod tests { #[test] fn test_invalid_minter_update() { - let mut state = TestState::default(); + let mut state = init_state(); let mut keys_changed = BTreeSet::new(); let minter_key = minter_key(&nam()); @@ -648,7 +690,7 @@ mod tests { #[test] fn test_invalid_key_update() { - let mut state = TestState::default(); + let mut state = init_state(); let mut keys_changed = BTreeSet::new(); let key = Key::from( @@ -689,4 +731,121 @@ mod tests { .expect("validation failed") ); } + + #[test] + fn test_native_token_not_transferable() { + let mut state = init_state(); + let src = established_address_1(); + let dest = established_address_2(); + let keys_changed = transfer(&mut state, &src, &dest); + + // disable native token transfer + let key = get_native_token_transferable_key(); + state.write(&key, false).unwrap(); + + let tx_index = TxIndex::default(); + let tx = dummy_tx(&state); + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = wasm_cache(); + let mut verifiers = BTreeSet::new(); + verifiers.insert(src); + let sentinel = RefCell::new(VpSentinel::default()); + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let vp = MultitokenVp { ctx }; + assert!( + !vp.validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + } + + #[test] + fn test_native_token_transferable_to_pos() { + let mut state = init_state(); + let src = established_address_1(); + let dest = POS; + let keys_changed = transfer(&mut state, &src, &dest); + + // disable native token transfer + let key = get_native_token_transferable_key(); + state.write(&key, false).unwrap(); + + let tx_index = TxIndex::default(); + let tx = dummy_tx(&state); + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = wasm_cache(); + let mut verifiers = BTreeSet::new(); + verifiers.insert(src); + let sentinel = RefCell::new(VpSentinel::default()); + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let vp = MultitokenVp { ctx }; + assert!( + vp.validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + } + + #[test] + fn test_native_token_transferable_from_gov() { + let mut state = init_state(); + let src = GOV; + let dest = POS; + let keys_changed = transfer(&mut state, &src, &dest); + + // disable native token transfer + let key = get_native_token_transferable_key(); + state.write(&key, false).unwrap(); + + let tx_index = TxIndex::default(); + let tx = dummy_tx(&state); + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = wasm_cache(); + let mut verifiers = BTreeSet::new(); + verifiers.insert(src); + let sentinel = RefCell::new(VpSentinel::default()); + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let vp = MultitokenVp { ctx }; + assert!( + !vp.validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + } } diff --git a/crates/parameters/Cargo.toml b/crates/parameters/Cargo.toml index fc212110af..6be22eda9c 100644 --- a/crates/parameters/Cargo.toml +++ b/crates/parameters/Cargo.toml @@ -12,6 +12,13 @@ readme.workspace = true repository.workspace = true version.workspace = true +[features] +default = [] +testing = [ + "namada_core/testing", + "namada_storage/testing", +] + [dependencies] namada_core = { path = "../core" } namada_macros = { path = "../macros" } diff --git a/crates/parameters/src/lib.rs b/crates/parameters/src/lib.rs index b0c0a93410..5b51078d49 100644 --- a/crates/parameters/src/lib.rs +++ b/crates/parameters/src/lib.rs @@ -64,6 +64,7 @@ where minimum_gas_price, fee_unshielding_gas_limit, fee_unshielding_descriptions_limit, + is_native_token_transferable, } = parameters; // write max tx bytes parameter @@ -147,6 +148,11 @@ where let gas_cost_key = storage::get_gas_cost_key(); storage.write(&gas_cost_key, minimum_gas_price)?; + let native_token_transferable_key = + storage::get_native_token_transferable_key(); + storage + .write(&native_token_transferable_key, is_native_token_transferable)?; + Ok(()) } @@ -437,6 +443,13 @@ where .ok_or(ReadError::ParametersMissing) .into_storage_result()?; + let native_token_transferable_key = + storage::get_native_token_transferable_key(); + let value = storage.read(&native_token_transferable_key)?; + let is_native_token_transferable = value + .ok_or(ReadError::ParametersMissing) + .into_storage_result()?; + Ok(Parameters { max_tx_bytes, epoch_duration, @@ -453,6 +466,7 @@ where minimum_gas_price, fee_unshielding_gas_limit, fee_unshielding_descriptions_limit, + is_native_token_transferable, }) } @@ -474,3 +488,33 @@ where pub fn native_erc20_key() -> Key { storage::get_native_erc20_key_at_addr(ADDRESS) } + +/// Initialize parameters to the storage for testing +#[cfg(any(test, feature = "testing"))] +pub fn init_test_storage(storage: &mut S) -> namada_storage::Result<()> +where + S: StorageRead + StorageWrite, +{ + let params = Parameters { + max_tx_bytes: 1024 * 1024, + epoch_duration: EpochDuration { + min_num_of_blocks: 1, + min_duration: DurationSecs(3600), + }, + max_expected_time_per_block: DurationSecs(3600), + max_proposal_bytes: Default::default(), + max_block_gas: 100, + vp_allowlist: vec![], + tx_allowlist: vec![], + implicit_vp_code_hash: Default::default(), + epochs_per_year: 365, + max_signatures_per_transaction: 10, + staked_ratio: Default::default(), + pos_inflation_amount: Default::default(), + fee_unshielding_gas_limit: 0, + fee_unshielding_descriptions_limit: 0, + minimum_gas_price: Default::default(), + is_native_token_transferable: true, + }; + init_storage(¶ms, storage) +} diff --git a/crates/parameters/src/storage.rs b/crates/parameters/src/storage.rs index abf3fa743f..40aca06be5 100644 --- a/crates/parameters/src/storage.rs +++ b/crates/parameters/src/storage.rs @@ -45,6 +45,7 @@ struct Keys { fee_unshielding_gas_limit: &'static str, fee_unshielding_descriptions_limit: &'static str, max_signatures_per_transaction: &'static str, + native_token_transferable: &'static str, } /// Returns if the key is a parameter key. @@ -201,3 +202,20 @@ pub fn get_max_block_gas( ), ) } + +/// Storage key used for the flag to enable the native token transfer +pub fn get_native_token_transferable_key() -> Key { + get_native_token_transferable_key_at_addr(ADDRESS) +} + +/// Helper function to retrieve the `is_native_token_transferable` protocol +/// parameter from storage +pub fn is_native_token_transferable( + storage: &impl StorageRead, +) -> std::result::Result { + storage.read(&get_native_token_transferable_key())?.ok_or( + namada_storage::Error::SimpleMessage( + "Missing is_native_token_transferable parameter from storage", + ), + ) +} diff --git a/crates/proof_of_stake/src/lib.rs b/crates/proof_of_stake/src/lib.rs index 9e39310d8a..32efc6f5f5 100644 --- a/crates/proof_of_stake/src/lib.rs +++ b/crates/proof_of_stake/src/lib.rs @@ -2593,6 +2593,7 @@ pub mod test_utils { fee_unshielding_gas_limit: 10000, fee_unshielding_descriptions_limit: 15, minimum_gas_price: BTreeMap::new(), + is_native_token_transferable: true, }; init_storage(&chain_parameters, storage).unwrap(); init_genesis_helper(storage, ¶ms, validators, current_epoch)?; diff --git a/crates/shielded_token/Cargo.toml b/crates/shielded_token/Cargo.toml index 6b73901484..32390cde3b 100644 --- a/crates/shielded_token/Cargo.toml +++ b/crates/shielded_token/Cargo.toml @@ -31,6 +31,7 @@ tracing.workspace = true [dev-dependencies] namada_core = { path = "../core", features = ["testing"] } +namada_parameters = { path = "../parameters", features = ["testing"] } namada_storage = { path = "../storage", features = ["testing"] } proptest.workspace = true diff --git a/crates/shielded_token/src/conversion.rs b/crates/shielded_token/src/conversion.rs index a9dee1bba5..f066a8ae43 100644 --- a/crates/shielded_token/src/conversion.rs +++ b/crates/shielded_token/src/conversion.rs @@ -536,9 +536,7 @@ mod tests { use namada_core::address; use namada_core::collections::HashMap; use namada_core::dec::testing::arb_non_negative_dec; - use namada_core::time::DurationSecs; use namada_core::token::testing::arb_amount; - use namada_parameters::{EpochDuration, Parameters}; use namada_storage::testing::TestStorage; use namada_trans_token::write_denom; use proptest::prelude::*; @@ -569,31 +567,10 @@ mod tests { const ROUNDS: usize = 10; let mut s = TestStorage::default(); - let params = Parameters { - max_tx_bytes: 1024 * 1024, - epoch_duration: EpochDuration { - min_num_of_blocks: 1, - min_duration: DurationSecs(3600), - }, - max_expected_time_per_block: DurationSecs(3600), - max_proposal_bytes: Default::default(), - max_block_gas: 100, - vp_allowlist: vec![], - tx_allowlist: vec![], - implicit_vp_code_hash: Default::default(), - epochs_per_year: 365, - max_signatures_per_transaction: 10, - staked_ratio: Default::default(), - pos_inflation_amount: Default::default(), - fee_unshielding_gas_limit: 0, - fee_unshielding_descriptions_limit: 0, - minimum_gas_price: Default::default(), - }; - // Initialize the state { // Parameters - namada_parameters::init_storage(¶ms, &mut s).unwrap(); + namada_parameters::init_test_storage(&mut s).unwrap(); // Tokens let token_params = ShieldedParams { diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index fac1e1badb..15ca2de92d 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -738,6 +738,7 @@ mod tests { fee_unshielding_gas_limit: 20_000, fee_unshielding_descriptions_limit: 15, minimum_gas_price: BTreeMap::default(), + is_native_token_transferable: true, }; namada_parameters::init_storage(¶meters, &mut state).unwrap(); // Initialize pred_epochs to the current height diff --git a/crates/tests/src/vm_host_env/ibc.rs b/crates/tests/src/vm_host_env/ibc.rs index 595314038f..6dd4ce12c4 100644 --- a/crates/tests/src/vm_host_env/ibc.rs +++ b/crates/tests/src/vm_host_env/ibc.rs @@ -215,6 +215,7 @@ pub fn init_storage() -> (Address, Address) { let code_hash = Hash::sha256(&code); tx_host_env::with(|env| { + namada::parameters::init_test_storage(&mut env.state).unwrap(); ibc::init_genesis_storage(&mut env.state); let gov_params = GovernanceParameters::default(); gov_params.init_storage(&mut env.state).unwrap(); diff --git a/genesis/localnet/parameters.toml b/genesis/localnet/parameters.toml index 7843cc7e06..c6d063bbaa 100644 --- a/genesis/localnet/parameters.toml +++ b/genesis/localnet/parameters.toml @@ -1,6 +1,7 @@ # General protocol parameters. [parameters] native_token = "NAM" +is_native_token_transferable = true # Minimum number of blocks in an epoch. min_num_of_blocks = 4 # Maximum expected time per block (in seconds). diff --git a/genesis/starter/parameters.toml b/genesis/starter/parameters.toml index c0d19923ce..c1d076eab5 100644 --- a/genesis/starter/parameters.toml +++ b/genesis/starter/parameters.toml @@ -1,6 +1,7 @@ # General protocol parameters. [parameters] native_token = "NAM" +is_native_token_transferable = true # Minimum number of blocks in an epoch. min_num_of_blocks = 4 # Maximum expected time per block (in seconds).