diff --git a/.changelog/unreleased/bug-fixes/1667-faucet-limit-fix.md b/.changelog/unreleased/bug-fixes/1667-faucet-limit-fix.md new file mode 100644 index 0000000000..49c3647b55 --- /dev/null +++ b/.changelog/unreleased/bug-fixes/1667-faucet-limit-fix.md @@ -0,0 +1,2 @@ +- Fix genesis `faucet_withdrawal_limit` parser to respect tokens' denomination. + ([\#1667](https://github.com/anoma/namada/pull/1667)) \ No newline at end of file diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index fcd57be745..1c659a056c 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -22,6 +22,7 @@ use namada::types::key::dkg_session_keys::DkgPublicKey; use namada::types::key::*; use namada::types::time::{DateTimeUtc, DurationSecs}; use namada::types::token::Denomination; +use namada::types::uint::Uint; use namada::types::{storage, token}; /// Genesis configuration file format @@ -45,6 +46,7 @@ pub mod genesis_config { use namada::types::key::*; use namada::types::time::Rfc3339String; use namada::types::token::Denomination; + use namada::types::uint::Uint; use namada::types::{storage, token}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -122,8 +124,9 @@ pub mod genesis_config { /// Testnet faucet PoW difficulty - defaults to `0` when not set pub faucet_pow_difficulty: Option, #[cfg(not(feature = "mainnet"))] - /// Testnet faucet withdrawal limit - defaults to 1000 NAM when not set - pub faucet_withdrawal_limit: Option, + /// Testnet faucet withdrawal limit - defaults to 1000 tokens when not + /// set + pub faucet_withdrawal_limit: Option, // Initial validator set pub validator: HashMap, // Token accounts present at genesis @@ -557,7 +560,6 @@ pub mod genesis_config { .expect("Missing native token address"), ) .expect("Invalid address"); - let validators: HashMap = validator .iter() .map(|(name, cfg)| (name.clone(), load_validator(cfg, &wasm))) @@ -732,7 +734,7 @@ pub struct Genesis { #[cfg(not(feature = "mainnet"))] pub faucet_pow_difficulty: Option, #[cfg(not(feature = "mainnet"))] - pub faucet_withdrawal_limit: Option, + pub faucet_withdrawal_limit: Option, pub validators: Vec, pub token_accounts: Vec, pub established_accounts: Vec, diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 91f48e3e05..7ce3e963f8 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -260,7 +260,7 @@ where fn initialize_established_accounts( &mut self, faucet_pow_difficulty: Option, - faucet_withdrawal_limit: Option, + faucet_withdrawal_limit: Option, accounts: Vec, implicit_vp_code_path: &str, ) -> Result<()> { @@ -311,8 +311,11 @@ where if vp_code_path == "vp_testnet_faucet.wasm" { let difficulty = faucet_pow_difficulty.unwrap_or_default(); // withdrawal limit defaults to 1000 NAM when not set - let withdrawal_limit = faucet_withdrawal_limit - .unwrap_or_else(|| token::Amount::native_whole(1_000)); + let withdrawal_limit = + faucet_withdrawal_limit.unwrap_or_else(|| { + token::Amount::native_whole(1_000).into() + }); + testnet_pow::init_faucet_storage( &mut self.wl_storage, &address, diff --git a/core/src/ledger/testnet_pow.rs b/core/src/ledger/testnet_pow.rs index aa1257a886..dcb7c9feb2 100644 --- a/core/src/ledger/testnet_pow.rs +++ b/core/src/ledger/testnet_pow.rs @@ -12,7 +12,7 @@ use crate::ledger::storage_api::collections::LazyMap; use crate::types::address::Address; use crate::types::hash::Hash; use crate::types::storage::{self, DbKeySeg, Key}; -use crate::types::token; +use crate::types::uint::Uint; /// Initialize faucet's storage. This must be called at genesis if faucet /// account is being used. @@ -20,7 +20,7 @@ pub fn init_faucet_storage( storage: &mut S, address: &Address, difficulty: Difficulty, - withdrawal_limit: token::Amount, + withdrawal_limit: Uint, ) -> storage_api::Result<()> where S: StorageWrite, @@ -457,7 +457,7 @@ where pub fn read_withdrawal_limit( storage: &S, address: &Address, -) -> storage_api::Result +) -> storage_api::Result where S: StorageRead, { @@ -471,7 +471,7 @@ where pub fn write_withdrawal_limit( storage: &mut S, address: &Address, - withdrawal_limit: token::Amount, + withdrawal_limit: Uint, ) -> Result<(), storage_api::Error> where S: StorageWrite, diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index 637694a29c..ea935c1cc1 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -8,7 +8,6 @@ use std::ops::{Add, AddAssign, BitAnd, Div, Mul, Neg, Rem, Sub, SubAssign}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use impl_num_traits::impl_uint_num_traits; use num_integer::Integer; -use serde::{Deserialize, Serialize}; use uint::construct_uint; use crate::types::token; @@ -31,8 +30,6 @@ construct_uint! { /// Namada native type to replace for unsigned 256 bit /// integers. #[derive( - Serialize, - Deserialize, BorshSerialize, BorshDeserialize, BorshSchema, @@ -41,6 +38,62 @@ construct_uint! { pub struct Uint(4); } +impl serde::Serialize for Uint { + fn serialize( + &self, + serializer: S, + ) -> std::result::Result + where + S: serde::Serializer, + { + let amount_string = self.to_string(); + serde::Serialize::serialize(&amount_string, serializer) + } +} + +impl<'de> serde::Deserialize<'de> for Uint { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as serdeError; + let amount_string: String = + serde::Deserialize::deserialize(deserializer)?; + + let digits = amount_string + .chars() + .filter_map(|c| { + if c.is_ascii_digit() { + c.to_digit(10).map(Uint::from) + } else { + None + } + }) + .rev() + .collect::>(); + if digits.len() != amount_string.len() { + return Err(D::Error::custom(AmountParseError::FromString)); + } + if digits.len() > 77 { + return Err(D::Error::custom(AmountParseError::ScaleTooLarge( + digits.len() as u32, + 77, + ))); + } + let mut value = Uint::default(); + let ten = Uint::from(10); + for (pow, digit) in digits.into_iter().enumerate() { + value = ten + .checked_pow(Uint::from(pow)) + .and_then(|scaling| scaling.checked_mul(digit)) + .and_then(|scaled| value.checked_add(scaled)) + .ok_or(AmountParseError::PrecisionOverflow) + .map_err(D::Error::custom)?; + } + Ok(value) + } +} + impl_uint_num_traits!(Uint, 4); impl Integer for Uint { @@ -624,4 +677,15 @@ mod test_uint { assert!(-that <= -this); assert!(-that <= this); } + + #[test] + fn test_serialization_roundtrip() { + let amount: Uint = serde_json::from_str(r#""1000000000""#).unwrap(); + assert_eq!(amount, Uint::from(1000000000)); + let serialized = serde_json::to_string(&amount).unwrap(); + assert_eq!(serialized, r#""1000000000""#); + + let amount: Result = serde_json::from_str(r#""1000000000.2""#); + assert!(amount.is_err()); + } } diff --git a/genesis/e2e-tests-single-node.toml b/genesis/e2e-tests-single-node.toml index beab8edc2f..c61c2c2bcd 100644 --- a/genesis/e2e-tests-single-node.toml +++ b/genesis/e2e-tests-single-node.toml @@ -5,7 +5,7 @@ genesis_time = "2021-09-30T10:00:00Z" native_token = "NAM" faucet_pow_difficulty = 1 -faucet_withdrawal_limit = "1000000000" +faucet_withdrawal_limit = "1000" [validator.validator-0] # Validator's staked NAM at genesis. diff --git a/tests/src/storage_api/testnet_pow.rs b/tests/src/storage_api/testnet_pow.rs index 5e54188c1b..ab45bc99c8 100644 --- a/tests/src/storage_api/testnet_pow.rs +++ b/tests/src/storage_api/testnet_pow.rs @@ -11,7 +11,7 @@ use crate::vp; fn test_challenge_and_solution() -> storage_api::Result<()> { let faucet_address = address::testing::established_address_1(); let difficulty = Difficulty::try_new(1).unwrap(); - let withdrawal_limit = token::Amount::native_whole(1_000); + let withdrawal_limit = token::Amount::native_whole(1_000).into(); let mut tx_env = TestTxEnv::default(); diff --git a/wasm/wasm_source/src/vp_testnet_faucet.rs b/wasm/wasm_source/src/vp_testnet_faucet.rs index 8e002663ee..3278f7a111 100644 --- a/wasm/wasm_source/src/vp_testnet_faucet.rs +++ b/wasm/wasm_source/src/vp_testnet_faucet.rs @@ -43,7 +43,7 @@ fn validate_tx( } for key in keys_changed.iter() { - let is_valid = if let Some([_, owner]) = + let is_valid = if let Some([token, owner]) = token::is_any_token_balance_key(key) { if owner == &addr { @@ -51,7 +51,17 @@ fn validate_tx( let post: token::Amount = ctx.read_post(key)?.unwrap_or_default(); let change = post.change() - pre.change(); - + let maybe_denom = + storage_api::token::read_denom(&ctx.pre(), token, None)?; + if maybe_denom.is_none() { + debug_log!( + "A denomination for token address {} does not exist \ + in storage", + token, + ); + return reject(); + } + let denom = maybe_denom.unwrap(); if !change.non_negative() { // Allow to withdraw without a sig if there's a valid PoW if ctx.has_valid_pow() { @@ -60,7 +70,10 @@ fn validate_tx( &ctx.pre(), &addr, )?; - change >= -max_free_debit.change() + + token::Amount::from_uint(change.abs(), 0).unwrap() + <= token::Amount::from_uint(max_free_debit, denom) + .unwrap() } else { debug_log!("No PoW solution, a signature is required"); // Debit without a solution has to signed @@ -304,7 +317,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let difficulty = testnet_pow::Difficulty::try_new(0).unwrap(); let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, 0).unwrap(); - testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit).unwrap(); + testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit.into()).unwrap(); let target = address::testing::established_address_2(); let token = address::nam(); @@ -349,7 +362,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let difficulty = testnet_pow::Difficulty::try_new(0).unwrap(); let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, 0).unwrap(); - testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit).unwrap(); + testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit.into()).unwrap(); let target = address::testing::established_address_2(); let target_key = key::testing::keypair_1(); @@ -414,7 +427,7 @@ mod tests { // Init the VP let difficulty = testnet_pow::Difficulty::try_new(0).unwrap(); let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, 0).unwrap(); - testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit).unwrap(); + testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit.into()).unwrap(); let keypair = key::testing::keypair_1(); let public_key = &keypair.ref_to();