diff --git a/.changelog/unreleased/improvements/3141-balance-change-events.md b/.changelog/unreleased/improvements/3141-balance-change-events.md new file mode 100644 index 0000000000..0fc29ce8ab --- /dev/null +++ b/.changelog/unreleased/improvements/3141-balance-change-events.md @@ -0,0 +1,2 @@ +- Emit balance change events for various protocol actions. + ([\#3141](https://github.com/anoma/namada/pull/3141)) \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 41ecf4d679..0c0031fed8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4945,6 +4945,7 @@ dependencies = [ "data-encoding", "derivative", "itertools 0.10.5", + "konst", "linkme", "namada_account", "namada_controller", @@ -5196,8 +5197,10 @@ dependencies = [ name = "namada_trans_token" version = "0.34.0" dependencies = [ + "konst", "linkme", "namada_core", + "namada_events", "namada_storage", ] diff --git a/crates/apps/src/lib/node/ledger/shell/governance.rs b/crates/apps/src/lib/node/ledger/shell/governance.rs index 23936a4433..94a35b2f1d 100644 --- a/crates/apps/src/lib/node/ledger/shell/governance.rs +++ b/crates/apps/src/lib/node/ledger/shell/governance.rs @@ -23,8 +23,10 @@ use namada::proof_of_stake::storage::{ read_total_active_stake, validator_state_handle, }; use namada::proof_of_stake::types::{BondId, ValidatorState}; -use namada::sdk::events::EmitEvents; +use namada::sdk::events::{EmitEvents, EventLevel}; use namada::state::StorageWrite; +use namada::token::event::{TokenEvent, TokenOperation, UserAccount}; +use namada::token::read_balance; use namada::tx::{Code, Data}; use namada_sdk::proof_of_stake::storage::read_validator_stake; @@ -171,6 +173,7 @@ where let native_token = &shell.state.get_native_token()?; let _result = execute_pgf_funding_proposal( &mut shell.state, + events, native_token, payments, id, @@ -189,11 +192,17 @@ where // Take events that could have been emitted by PGF // over IBC, governance proposal execution, etc - for event in shell.state.write_log_mut().take_events() { - events.emit(event.with(Height( - shell.state.in_mem().get_last_block_height() + 1, - ))); - } + let current_height = + shell.state.in_mem().get_last_block_height() + 1; + + events.emit_many( + shell + .state + .write_log_mut() + .take_events() + .into_iter() + .map(|event| event.with(Height(current_height))), + ); gov_api::get_proposal_author(&shell.state, id)? } @@ -240,6 +249,26 @@ where &address, funds, )?; + + const DESCRIPTOR: &str = "governance-locked-funds-refund"; + + let final_gov_balance = + read_balance(&shell.state, &native_token, &gov_address)?.into(); + let final_target_balance = + read_balance(&shell.state, &native_token, &address)?.into(); + + events.emit(TokenEvent { + descriptor: DESCRIPTOR.into(), + level: EventLevel::Block, + token: native_token.clone(), + operation: TokenOperation::Transfer { + amount: funds.into(), + source: UserAccount::Internal(gov_address), + target: UserAccount::Internal(address), + source_post_balance: final_gov_balance, + target_post_balance: Some(final_target_balance), + }, + }); } else { token::burn_tokens( &mut shell.state, @@ -247,6 +276,22 @@ where &gov_address, funds, )?; + + const DESCRIPTOR: &str = "governance-locked-funds-burn"; + + let final_gov_balance = + read_balance(&shell.state, &native_token, &gov_address)?.into(); + + events.emit(TokenEvent { + descriptor: DESCRIPTOR.into(), + level: EventLevel::Block, + token: native_token.clone(), + operation: TokenOperation::Burn { + amount: funds.into(), + target_account: UserAccount::Internal(gov_address), + post_balance: final_gov_balance, + }, + }); } } @@ -414,6 +459,7 @@ where fn execute_pgf_funding_proposal( state: &mut WlState, + events: &mut impl EmitEvents, token: &Address, fundings: BTreeSet, proposal_id: u64, @@ -452,25 +498,68 @@ where } }, PGFAction::Retro(target) => { - let result = match &target { - PGFTarget::Internal(target) => token::transfer( - state, - token, - &ADDRESS, - &target.target, - target.amount, + let (result, event) = match &target { + PGFTarget::Internal(target) => ( + token::transfer( + state, + token, + &ADDRESS, + &target.target, + target.amount, + ), + TokenEvent { + descriptor: "pgf-payments".into(), + level: EventLevel::Block, + token: token.clone(), + operation: TokenOperation::Transfer { + amount: target.amount.into(), + source: UserAccount::Internal(ADDRESS), + target: UserAccount::Internal( + target.target.clone(), + ), + source_post_balance: read_balance( + state, token, &ADDRESS, + )? + .into(), + target_post_balance: Some( + read_balance(state, token, &target.target)? + .into(), + ), + }, + }, + ), + PGFTarget::Ibc(target) => ( + ibc::transfer_over_ibc(state, token, &ADDRESS, target), + TokenEvent { + descriptor: "pgf-payments-over-ibc".into(), + level: EventLevel::Block, + token: token.clone(), + operation: TokenOperation::Transfer { + amount: target.amount.into(), + source: UserAccount::Internal(ADDRESS), + target: UserAccount::External( + target.target.clone(), + ), + source_post_balance: read_balance( + state, token, &ADDRESS, + )? + .into(), + target_post_balance: None, + }, + }, ), - PGFTarget::Ibc(target) => { - ibc::transfer_over_ibc(state, token, &ADDRESS, target) - } }; match result { - Ok(()) => tracing::info!( - "Execute RetroPgf from proposal id {}: sent {} to {}.", - proposal_id, - target.amount().to_string_native(), - target.target() - ), + Ok(()) => { + tracing::info!( + "Execute RetroPgf from proposal id {}: sent {} to \ + {}.", + proposal_id, + target.amount().to_string_native(), + target.target() + ); + events.emit(event); + } Err(e) => tracing::warn!( "Error in RetroPgf transfer from proposal id {}, \ amount {} to {}: {}", 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 3fd22151f8..e691be1abb 100644 --- a/crates/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/crates/apps/src/lib/node/ledger/shell/init_chain.rs @@ -1193,7 +1193,7 @@ mod test { token::Amount::from_uint(1, 6).unwrap(), 6.into(), ), - "Insufficient source balance".to_string(), + format!("{albert_address_str} has insufficient balance"), )]; assert_eq!(expected, initializer.warnings); initializer.warnings.clear(); diff --git a/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs b/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs index 07477c4271..82e9d39245 100644 --- a/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs +++ b/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs @@ -6,6 +6,7 @@ use masp_primitives::transaction::Transaction; use namada::core::address::Address; use namada::core::key::tm_raw_hash_to_string; use namada::gas::TxGasMeter; +use namada::hash::Hash; use namada::ledger::protocol::{self, ShellParams}; use namada::proof_of_stake::storage::find_validator_by_raw_hash; use namada::state::{DBIter, StorageHasher, TempWlState, DB}; @@ -295,6 +296,7 @@ where // Check fees and extract the gas limit of this transaction match prepare_proposal_fee_check( &wrapper, + tx.header_hash(), protocol::get_fee_unshielding_transaction(&tx, &wrapper), block_proposer, proposer_local_config, @@ -313,8 +315,10 @@ where } } +#[allow(clippy::too_many_arguments)] fn prepare_proposal_fee_check( wrapper: &WrapperTx, + wrapper_tx_hash: Hash, masp_transaction: Option, proposer: &Address, proposer_local_config: Option<&ValidatorLocalConfig>, @@ -357,8 +361,13 @@ where shell_params, )?; - protocol::transfer_fee(shell_params.state, proposer, wrapper) - .map_err(Error::TxApply) + protocol::transfer_fee( + shell_params.state, + proposer, + wrapper, + wrapper_tx_hash, + ) + .map_err(Error::TxApply) } #[cfg(test)] diff --git a/crates/apps/src/lib/node/ledger/shell/process_proposal.rs b/crates/apps/src/lib/node/ledger/shell/process_proposal.rs index 4643d32a3b..15a1552c42 100644 --- a/crates/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/crates/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -2,6 +2,7 @@ //! and [`RevertProposal`] ABCI++ methods for the Shell use data_encoding::HEXUPPER; +use namada::hash::Hash; use namada::ledger::pos::PosQueries; use namada::proof_of_stake::storage::find_validator_by_raw_hash; use namada::tx::data::protocol::ProtocolTxType; @@ -465,6 +466,7 @@ where // Check that the fee payer has sufficient balance. match process_proposal_fee_check( &wrapper, + tx.header_hash(), get_fee_unshielding_transaction(&tx, &wrapper), block_proposer, &mut ShellParams::new( @@ -498,6 +500,7 @@ where fn process_proposal_fee_check( wrapper: &WrapperTx, + wrapper_tx_hash: Hash, masp_transaction: Option, proposer: &Address, shell_params: &mut ShellParams<'_, TempWlState, D, H, CA>, @@ -524,8 +527,13 @@ where shell_params, )?; - protocol::transfer_fee(shell_params.state, proposer, wrapper) - .map_err(Error::TxApply) + protocol::transfer_fee( + shell_params.state, + proposer, + wrapper, + wrapper_tx_hash, + ) + .map_err(Error::TxApply) } /// We test the failure cases of [`process_proposal`]. The happy flows diff --git a/crates/core/src/token.rs b/crates/core/src/token.rs index 42ff1a9747..28ad030fff 100644 --- a/crates/core/src/token.rs +++ b/crates/core/src/token.rs @@ -211,7 +211,7 @@ impl Amount { let denom = denom.into(); let uint = uint.into(); if denom == 0 { - return Ok(Self { raw: uint }); + return Ok(uint.into()); } match Uint::from(10) .checked_pow(Uint::from(denom)) @@ -907,6 +907,12 @@ impl From for Uint { } } +impl From for Amount { + fn from(raw: Uint) -> Self { + Self { raw } + } +} + /// The four possible u64 words in a [`Uint`]. /// Used for converting to MASP amounts. #[derive( diff --git a/crates/core/src/uint.rs b/crates/core/src/uint.rs index 60cfd14952..284018296e 100644 --- a/crates/core/src/uint.rs +++ b/crates/core/src/uint.rs @@ -4,6 +4,7 @@ use std::cmp::Ordering; use std::fmt; use std::ops::{Add, AddAssign, BitAnd, Div, Mul, Neg, Rem, Sub, SubAssign}; +use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use impl_num_traits::impl_uint_num_traits; @@ -507,7 +508,26 @@ impl fmt::Display for I256 { } } +impl FromStr for I256 { + type Err = Box; + + fn from_str(num: &str) -> Result { + if let Some(("", neg_num)) = num.split_once('-') { + let uint = neg_num.parse::()?.negate(); + Ok(I256(uint)) + } else { + let uint = num.parse::()?; + Ok(I256(uint)) + } + } +} + impl I256 { + /// Compute the two's complement of a number. + pub fn negate(&self) -> Self { + Self(self.0.negate()) + } + /// Check if the amount is not negative (greater /// than or equal to zero) pub fn non_negative(&self) -> bool { @@ -1064,4 +1084,14 @@ mod test_uint { assert_eq!(e.checked_mul_div(c, b), Some((Uint::zero(), c))); assert_eq!(d.checked_mul_div(a, e), None); } + + #[test] + fn test_i256_str_roundtrip() { + let minus_one = I256::one().negate(); + let minus_one_str = minus_one.to_string(); + assert_eq!(minus_one_str, "-1"); + + let parsed: I256 = minus_one_str.parse().unwrap(); + assert_eq!(minus_one, parsed); + } } diff --git a/crates/events/Cargo.toml b/crates/events/Cargo.toml index d427cf3c03..0e6972db4e 100644 --- a/crates/events/Cargo.toml +++ b/crates/events/Cargo.toml @@ -19,6 +19,7 @@ migrations = [ "namada_migrations", "linkme", ] +testing = [] [dependencies] namada_core = {path = "../core"} diff --git a/crates/events/src/extend.rs b/crates/events/src/extend.rs index 313086102f..61c6dd0004 100644 --- a/crates/events/src/extend.rs +++ b/crates/events/src/extend.rs @@ -616,6 +616,20 @@ where } } +/// Extend an [`Event`] with the given closure. +pub struct Closure(pub F); + +impl ExtendEvent for Closure +where + F: FnOnce(&mut Event), +{ + #[inline] + fn extend_event(self, event: &mut Event) { + let Self(closure) = self; + closure(event); + } +} + #[cfg(test)] mod event_composition_tests { use super::*; diff --git a/crates/events/src/lib.rs b/crates/events/src/lib.rs index 83de56705e..f0f5ce819e 100644 --- a/crates/events/src/lib.rs +++ b/crates/events/src/lib.rs @@ -1,6 +1,8 @@ //! Events emitted by the Namada ledger. pub mod extend; +#[cfg(any(test, feature = "testing"))] +pub mod testing; use std::borrow::Cow; use std::collections::BTreeMap; diff --git a/crates/events/src/testing.rs b/crates/events/src/testing.rs new file mode 100644 index 0000000000..e02a566b18 --- /dev/null +++ b/crates/events/src/testing.rs @@ -0,0 +1,21 @@ +//! Events testing utilities. + +use super::{EmitEvents, Event}; + +/// Event sink that drops any emitted events. +pub struct VoidEventSink; + +impl EmitEvents for VoidEventSink { + fn emit(&mut self, _: E) + where + E: Into, + { + } + + fn emit_many(&mut self, _: B) + where + B: IntoIterator, + E: Into, + { + } +} diff --git a/crates/ibc/src/actions.rs b/crates/ibc/src/actions.rs index 720853eb78..d2529b7626 100644 --- a/crates/ibc/src/actions.rs +++ b/crates/ibc/src/actions.rs @@ -4,7 +4,7 @@ use std::cell::RefCell; use std::collections::BTreeSet; use std::rc::Rc; -use namada_core::address::{Address, InternalAddress}; +use namada_core::address::Address; use namada_core::borsh::BorshSerializeExt; use namada_core::ibc::apps::transfer::types::msgs::transfer::MsgTransfer as IbcMsgTransfer; use namada_core::ibc::apps::transfer::types::packet::PacketData; @@ -13,7 +13,7 @@ use namada_core::ibc::core::channel::types::timeout::TimeoutHeight; use namada_core::ibc::MsgTransfer; use namada_core::tendermint::Time as TmTime; use namada_core::token::Amount; -use namada_events::EventTypeBuilder; +use namada_events::{EmitEvents, EventTypeBuilder}; use namada_governance::storage::proposal::PGFIbcTarget; use namada_parameters::read_epoch_duration_parameter; use namada_state::{ @@ -24,14 +24,13 @@ use namada_token as token; use token::DenominatedAmount; use crate::event::IbcEvent; -use crate::{IbcActions, IbcCommonContext, IbcStorageContext}; +use crate::{ + storage as ibc_storage, IbcActions, IbcCommonContext, IbcStorageContext, +}; /// IBC protocol context #[derive(Debug)] -pub struct IbcProtocolContext<'a, S> -where - S: State, -{ +pub struct IbcProtocolContext<'a, S> { state: &'a mut S, } @@ -165,9 +164,7 @@ where token: &Address, amount: Amount, ) -> Result<(), StorageError> { - token::credit_tokens(self, token, target, amount)?; - let minter_key = token::storage_key::minter_key(token); - self.write(&minter_key, Address::Internal(InternalAddress::Ibc)) + ibc_storage::mint_tokens(self, target, token, amount) } fn burn_token( @@ -176,7 +173,7 @@ where token: &Address, amount: Amount, ) -> Result<(), StorageError> { - token::burn_tokens(self, token, target, amount) + ibc_storage::burn_tokens(self, target, token, amount) } fn log_string(&self, message: String) { @@ -193,7 +190,7 @@ where impl IbcStorageContext for IbcProtocolContext<'_, S> where - S: State, + S: State + EmitEvents, { fn emit_ibc_event(&mut self, event: IbcEvent) -> Result<(), StorageError> { self.state.write_log_mut().emit_event(event); @@ -243,10 +240,7 @@ where token: &Address, amount: Amount, ) -> Result<(), StorageError> { - token::credit_tokens(self.state, token, target, amount)?; - let minter_key = token::storage_key::minter_key(token); - self.state - .write(&minter_key, Address::Internal(InternalAddress::Ibc)) + ibc_storage::mint_tokens(self.state, target, token, amount) } /// Burn token @@ -256,7 +250,7 @@ where token: &Address, amount: Amount, ) -> Result<(), StorageError> { - token::burn_tokens(self.state, token, target, amount) + ibc_storage::burn_tokens(self.state, target, token, amount) } fn log_string(&self, message: String) { @@ -264,7 +258,10 @@ where } } -impl IbcCommonContext for IbcProtocolContext<'_, S> where S: State {} +impl IbcCommonContext for IbcProtocolContext<'_, S> where + S: State + EmitEvents +{ +} /// Transfer tokens over IBC pub fn transfer_over_ibc( diff --git a/crates/ibc/src/event.rs b/crates/ibc/src/event.rs index aec5b1961a..98e514de6a 100644 --- a/crates/ibc/src/event.rs +++ b/crates/ibc/src/event.rs @@ -30,6 +30,9 @@ use namada_macros::BorshDeserializer; use namada_migrations::*; use serde::{Deserialize, Serialize}; +/// Describes a token event within IBC. +pub const TOKEN_EVENT_DESCRIPTOR: &str = IbcEvent::DOMAIN; + pub mod types { //! IBC event types. diff --git a/crates/ibc/src/storage.rs b/crates/ibc/src/storage.rs index e506cfbb0e..c9fa19839a 100644 --- a/crates/ibc/src/storage.rs +++ b/crates/ibc/src/storage.rs @@ -16,10 +16,14 @@ use namada_core::ibc::core::host::types::path::{ use namada_core::ibc::IbcTokenHash; use namada_core::storage::{DbKeySeg, Key, KeySeg}; use namada_core::token::Amount; -use namada_state::{StorageRead, StorageResult}; +use namada_events::{EmitEvents, EventLevel}; +use namada_state::{StorageRead, StorageResult, StorageWrite}; +use namada_token as token; +use namada_token::event::{TokenEvent, TokenOperation, UserAccount}; use sha2::{Digest, Sha256}; use thiserror::Error; +use crate::event::TOKEN_EVENT_DESCRIPTOR; use crate::parameters::IbcParameters; const CLIENTS_COUNTER_PREFIX: &str = "clients"; @@ -50,6 +54,64 @@ pub enum Error { /// IBC storage functions result pub type Result = std::result::Result; +/// Mint tokens, and emit an IBC token mint event. +pub fn mint_tokens( + state: &mut S, + target: &Address, + token: &Address, + amount: Amount, +) -> StorageResult<()> +where + S: StorageRead + StorageWrite + EmitEvents, +{ + token::mint_tokens( + state, + &Address::Internal(InternalAddress::Ibc), + token, + target, + amount, + )?; + + state.emit(TokenEvent { + descriptor: TOKEN_EVENT_DESCRIPTOR.into(), + level: EventLevel::Tx, + token: token.clone(), + operation: TokenOperation::Mint { + amount: amount.into(), + post_balance: token::read_balance(state, token, target)?.into(), + target_account: UserAccount::Internal(target.clone()), + }, + }); + + Ok(()) +} + +/// Burn tokens, and emit an IBC token burn event. +pub fn burn_tokens( + state: &mut S, + target: &Address, + token: &Address, + amount: Amount, +) -> StorageResult<()> +where + S: StorageRead + StorageWrite + EmitEvents, +{ + token::burn_tokens(state, token, target, amount)?; + + state.emit(TokenEvent { + descriptor: TOKEN_EVENT_DESCRIPTOR.into(), + level: EventLevel::Tx, + token: token.clone(), + operation: TokenOperation::Burn { + amount: amount.into(), + post_balance: token::read_balance(state, token, target)?.into(), + target_account: UserAccount::Internal(target.clone()), + }, + }); + + Ok(()) +} + /// Returns a key of the IBC-related data pub fn ibc_key(path: impl AsRef) -> Result { let path = Key::parse(path).map_err(Error::StorageKey)?; diff --git a/crates/namada/src/ledger/native_vp/multitoken.rs b/crates/namada/src/ledger/native_vp/multitoken.rs index 08676ca949..bd454a1c4b 100644 --- a/crates/namada/src/ledger/native_vp/multitoken.rs +++ b/crates/namada/src/ledger/native_vp/multitoken.rs @@ -272,9 +272,9 @@ where .into()), } } - _ => Err(native_vp::Error::new_const( - "Only IBC tokens can be minted by a user transaction", - ) + _ => Err(native_vp::Error::new_alloc(format!( + "Attempted to mint non-IBC token {token}" + )) .into()), } } diff --git a/crates/namada/src/ledger/protocol/mod.rs b/crates/namada/src/ledger/protocol/mod.rs index e4dd86a46f..97ad0cc7e5 100644 --- a/crates/namada/src/ledger/protocol/mod.rs +++ b/crates/namada/src/ledger/protocol/mod.rs @@ -9,9 +9,14 @@ use masp_primitives::transaction::Transaction; use namada_core::booleans::BoolResultUnitExt; use namada_core::hash::Hash; use namada_core::storage::Key; +use namada_events::extend::{ + ComposeEvent, Height as HeightAttr, TxHash as TxHashAttr, +}; +use namada_events::EventLevel; use namada_gas::TxGasMeter; use namada_sdk::tx::TX_TRANSFER_WASM; use namada_state::StorageWrite; +use namada_token::event::{TokenEvent, TokenOperation, UserAccount}; use namada_tx::data::protocol::ProtocolTxType; use namada_tx::data::{ GasLimit, TxResult, TxType, VpStatusFlags, VpsResult, WrapperTx, @@ -82,7 +87,7 @@ pub enum Error { PosNativeVpRuntime, #[error("Parameters native VP: {0}")] ParametersNativeVpError(parameters::Error), - #[error("IBC Token native VP: {0}")] + #[error("Multitoken native VP: {0}")] MultitokenNativeVpError(crate::ledger::native_vp::multitoken::Error), #[error("Governance native VP error: {0}")] GovernanceNativeVpError(crate::ledger::governance::Error), @@ -272,16 +277,19 @@ where { let mut changed_keys = BTreeSet::default(); + let wrapper_tx_hash = tx.header_hash(); + // Write wrapper tx hash to storage shell_params .state .write_log_mut() - .write_tx_hash(tx.header_hash()) + .write_tx_hash(wrapper_tx_hash) .expect("Error while writing tx hash to storage"); // Charge fee before performing any fallible operations charge_fee( wrapper, + wrapper_tx_hash, fee_unshield_transaction, &mut shell_params, &mut changed_keys, @@ -325,6 +333,7 @@ pub fn get_fee_unshielding_transaction( /// - The accumulated fee amount to be credited to the block proposer overflows fn charge_fee( wrapper: &WrapperTx, + wrapper_tx_hash: Hash, masp_transaction: Option, shell_params: &mut ShellParams<'_, S, D, H, CA>, changed_keys: &mut BTreeSet, @@ -351,7 +360,12 @@ where Some(WrapperArgs { block_proposer, is_committed_fee_unshield: _, - }) => transfer_fee(shell_params.state, block_proposer, wrapper)?, + }) => transfer_fee( + shell_params.state, + block_proposer, + wrapper, + wrapper_tx_hash, + )?, None => check_fees(shell_params.state, wrapper)?, } @@ -482,6 +496,7 @@ pub fn transfer_fee( state: &mut S, block_proposer: &Address, wrapper: &WrapperTx, + wrapper_tx_hash: Hash, ) -> Result<()> where S: State + StorageRead + StorageWrite, @@ -493,12 +508,19 @@ where ) .unwrap(); + const FEE_PAYMENT_DESCRIPTOR: std::borrow::Cow<'static, str> = + std::borrow::Cow::Borrowed("wrapper-fee-payment"); + match wrapper.get_tx_fee() { Ok(fees) => { let fees = crate::token::denom_to_amount(fees, &wrapper.fee.token, state) .map_err(|e| Error::FeeError(e.to_string()))?; - if balance.checked_sub(fees).is_some() { + + let current_block_height = + state.in_mem().get_last_block_height() + 1; + + if let Some(post_bal) = balance.checked_sub(fees) { token_transfer( state, &wrapper.fee.token, @@ -506,7 +528,38 @@ where block_proposer, fees, ) - .map_err(|e| Error::FeeError(e.to_string())) + .map_err(|e| Error::FeeError(e.to_string()))?; + + let target_post_balance = Some( + namada_token::read_balance( + state, + &wrapper.fee.token, + block_proposer, + ) + .map_err(Error::StorageError)? + .into(), + ); + + state.write_log_mut().emit_event( + TokenEvent { + descriptor: FEE_PAYMENT_DESCRIPTOR, + level: EventLevel::Tx, + token: wrapper.fee.token.clone(), + operation: TokenOperation::Transfer { + amount: fees.into(), + source: UserAccount::Internal(wrapper.fee_payer()), + target: UserAccount::Internal( + block_proposer.clone(), + ), + source_post_balance: post_bal.into(), + target_post_balance, + }, + } + .with(HeightAttr(current_block_height)) + .with(TxHashAttr(wrapper_tx_hash)), + ); + + Ok(()) } else { // Balance was insufficient for fee payment, move all the // available funds in the transparent balance of @@ -527,6 +580,35 @@ where ) .map_err(|e| Error::FeeError(e.to_string()))?; + let target_post_balance = Some( + namada_token::read_balance( + state, + &wrapper.fee.token, + block_proposer, + ) + .map_err(Error::StorageError)? + .into(), + ); + + state.write_log_mut().emit_event( + TokenEvent { + descriptor: FEE_PAYMENT_DESCRIPTOR, + level: EventLevel::Tx, + token: wrapper.fee.token.clone(), + operation: TokenOperation::Transfer { + amount: balance.into(), + source: UserAccount::Internal(wrapper.fee_payer()), + target: UserAccount::Internal( + block_proposer.clone(), + ), + source_post_balance: namada_core::uint::ZERO, + target_post_balance, + }, + } + .with(HeightAttr(current_block_height)) + .with(TxHashAttr(wrapper_tx_hash)), + ); + Err(Error::FeeError( "Transparent balance of wrapper's signer was insufficient \ to pay fee. All the available transparent funds have \ diff --git a/crates/proof_of_stake/Cargo.toml b/crates/proof_of_stake/Cargo.toml index 20ab734cd6..e8ac302a28 100644 --- a/crates/proof_of_stake/Cargo.toml +++ b/crates/proof_of_stake/Cargo.toml @@ -36,6 +36,7 @@ namada_trans_token = { path = "../trans_token" } borsh.workspace = true data-encoding.workspace = true derivative.workspace = true +konst.workspace = true linkme = {workspace = true, optional = true} num-traits.workspace = true once_cell.workspace = true @@ -47,6 +48,7 @@ tracing.workspace = true [dev-dependencies] namada_core = { path = "../core", features = ["testing"] } +namada_events = { path = "../events", features = ["testing"] } namada_state = { path = "../state", features = ["testing"] } assert_matches.workspace = true diff --git a/crates/proof_of_stake/src/event.rs b/crates/proof_of_stake/src/event.rs new file mode 100644 index 0000000000..2b9407f2dc --- /dev/null +++ b/crates/proof_of_stake/src/event.rs @@ -0,0 +1,75 @@ +//! Proof of Stake events. + +use namada_core::address::Address; +use namada_core::token; +use namada_core::uint::Uint; +use namada_events::extend::{ComposeEvent, EventAttributeEntry}; +use namada_events::{Event, EventLevel, EventToEmit}; + +pub mod types { + //! Proof of Stake event types. + + use namada_events::{event_type, EventType}; + + use super::PosEvent; + + /// Slash event. + pub const SLASH: EventType = event_type!(PosEvent, "slash"); +} + +/// Proof of Stake event. +#[derive(Debug)] +pub enum PosEvent { + /// Slashing event. + Slash { + /// The address of the slashed validator. + validator: Address, + /// Amount of tokens that have been slashed. + amount: token::Amount, + }, +} + +impl EventToEmit for PosEvent { + const DOMAIN: &'static str = "proof-of-stake"; +} + +impl From for Event { + fn from(pos_event: PosEvent) -> Self { + match pos_event { + PosEvent::Slash { validator, amount } => { + Event::new(types::SLASH, EventLevel::Block) + .with(SlashedValidator(validator)) + .with(SlashedAmount(&amount.into())) + .into() + } + } + } +} + +/// Extend an [`Event`] with slashed validator data. +pub struct SlashedValidator(pub Address); + +impl EventAttributeEntry<'static> for SlashedValidator { + type Value = Address; + type ValueOwned = Self::Value; + + const KEY: &'static str = "slashed-validator"; + + fn into_value(self) -> Self::Value { + self.0 + } +} + +/// Extend an [`Event`] with slashed amount data. +pub struct SlashedAmount<'amt>(pub &'amt Uint); + +impl<'amt> EventAttributeEntry<'amt> for SlashedAmount<'amt> { + type Value = &'amt Uint; + type ValueOwned = Uint; + + const KEY: &'static str = "slashed-amount"; + + fn into_value(self) -> Self::Value { + self.0 + } +} diff --git a/crates/proof_of_stake/src/lib.rs b/crates/proof_of_stake/src/lib.rs index b364d09d89..26cd0aad97 100644 --- a/crates/proof_of_stake/src/lib.rs +++ b/crates/proof_of_stake/src/lib.rs @@ -7,6 +7,7 @@ #![deny(rustdoc::private_intra_doc_links)] pub mod epoched; +pub mod event; pub mod parameters; pub mod pos_queries; pub mod queries; @@ -2869,7 +2870,7 @@ where /// Apply PoS updates for a block pub fn finalize_block( storage: &mut S, - _events: &mut impl EmitEvents, + events: &mut impl EmitEvents, is_new_epoch: bool, validator_set_update_epoch: Epoch, votes: Vec, @@ -2928,7 +2929,9 @@ where // Process and apply slashes that have already been recorded for the // current epoch - if let Err(err) = slashing::process_slashes(storage, current_epoch) { + if let Err(err) = + slashing::process_slashes(storage, events, current_epoch) + { tracing::error!( "Error while processing slashes queued for epoch {}: {}", current_epoch, diff --git a/crates/proof_of_stake/src/slashing.rs b/crates/proof_of_stake/src/slashing.rs index 3965ff6014..6b2a662bb1 100644 --- a/crates/proof_of_stake/src/slashing.rs +++ b/crates/proof_of_stake/src/slashing.rs @@ -11,12 +11,14 @@ use namada_core::key::tm_raw_hash_to_string; use namada_core::storage::{BlockHeight, Epoch}; use namada_core::tendermint::abci::types::{Misbehavior, MisbehaviorKind}; use namada_core::token; +use namada_events::EmitEvents; use namada_storage::collections::lazy_map::{ Collectable, NestedMap, NestedSubKey, SubKey, }; use namada_storage::collections::LazyMap; use namada_storage::{StorageRead, StorageWrite}; +use crate::event::PosEvent; use crate::storage::{ enqueued_slashes_handle, read_pos_params, read_validator_last_slash_epoch, read_validator_stake, total_bonded_handle, total_unbonded_handle, @@ -201,6 +203,7 @@ where /// validators. pub fn process_slashes( storage: &mut S, + events: &mut impl EmitEvents, current_epoch: Epoch, ) -> namada_storage::Result<()> where @@ -318,6 +321,11 @@ where epoch, Some(0), )?; + + events.emit(PosEvent::Slash { + validator: validator.clone(), + amount: slash_amount, + }); } } // Then update validator and total deltas diff --git a/crates/proof_of_stake/src/tests/state_machine.rs b/crates/proof_of_stake/src/tests/state_machine.rs index 0ad85e9448..3529323149 100644 --- a/crates/proof_of_stake/src/tests/state_machine.rs +++ b/crates/proof_of_stake/src/tests/state_machine.rs @@ -268,8 +268,12 @@ impl StateMachineTest for ConcretePosState { // Need to apply some slashing let current_epoch = state.s.in_mem().block.epoch; - crate::slashing::process_slashes(&mut state.s, current_epoch) - .unwrap(); + crate::slashing::process_slashes( + &mut state.s, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); let params = read_pos_params(&state.s).unwrap(); state.check_next_epoch_post_conditions(¶ms); diff --git a/crates/proof_of_stake/src/tests/state_machine_v2.rs b/crates/proof_of_stake/src/tests/state_machine_v2.rs index 5c89bc6d98..1b8dbbd642 100644 --- a/crates/proof_of_stake/src/tests/state_machine_v2.rs +++ b/crates/proof_of_stake/src/tests/state_machine_v2.rs @@ -1986,8 +1986,12 @@ impl StateMachineTest for ConcretePosState { // Need to apply some slashing let current_epoch = state.s.in_mem().block.epoch; - crate::slashing::process_slashes(&mut state.s, current_epoch) - .unwrap(); + crate::slashing::process_slashes( + &mut state.s, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); let params = read_pos_params(&state.s).unwrap(); state.check_next_epoch_post_conditions(¶ms); diff --git a/crates/proof_of_stake/src/tests/test_pos.rs b/crates/proof_of_stake/src/tests/test_pos.rs index 0150cba4d5..a26764788b 100644 --- a/crates/proof_of_stake/src/tests/test_pos.rs +++ b/crates/proof_of_stake/src/tests/test_pos.rs @@ -854,7 +854,12 @@ fn test_unjail_validator_aux( s.commit_block().unwrap(); current_epoch = advance_epoch(&mut s, ¶ms); - process_slashes(&mut s, current_epoch).unwrap(); + process_slashes( + &mut s, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); // Discover first slash let slash_0_evidence_epoch = current_epoch; @@ -914,7 +919,12 @@ fn test_unjail_validator_aux( slash_0_evidence_epoch + params.slash_processing_epoch_offset(); while current_epoch < unfreeze_epoch + 4u64 { current_epoch = advance_epoch(&mut s, ¶ms); - process_slashes(&mut s, current_epoch).unwrap(); + process_slashes( + &mut s, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); } // Unjail the validator @@ -960,7 +970,12 @@ fn test_unjail_validator_aux( // Advance another epoch current_epoch = advance_epoch(&mut s, ¶ms); - process_slashes(&mut s, current_epoch).unwrap(); + process_slashes( + &mut s, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); let second_att = unjail_validator(&mut s, val_addr, current_epoch); assert!(second_att.is_err()); @@ -1040,7 +1055,12 @@ fn test_unslashed_bond_amount_aux(validators: Vec) { // Advance an epoch current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); // Bond to validator 1 bond_tokens( @@ -1088,7 +1108,12 @@ fn test_unslashed_bond_amount_aux(validators: Vec) { // Advance an epoch current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); // Bond to validator 1 bond_tokens( @@ -1630,7 +1655,12 @@ fn test_is_delegator_aux(mut validators: Vec) { // Advance to epoch 1 current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); // Delegate in epoch 1 to validator1 let del1_epoch = current_epoch; @@ -1646,7 +1676,12 @@ fn test_is_delegator_aux(mut validators: Vec) { // Advance to epoch 2 current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); // Delegate in epoch 2 to validator2 let del2_epoch = current_epoch; diff --git a/crates/proof_of_stake/src/tests/test_slash_and_redel.rs b/crates/proof_of_stake/src/tests/test_slash_and_redel.rs index 9df89eeef4..d866220cc0 100644 --- a/crates/proof_of_stake/src/tests/test_slash_and_redel.rs +++ b/crates/proof_of_stake/src/tests/test_slash_and_redel.rs @@ -117,7 +117,12 @@ fn test_simple_redelegation_aux( for _ in 0..5 { current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); } let init_epoch = current_epoch; @@ -135,11 +140,26 @@ fn test_simple_redelegation_aux( // Advance three epochs current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); // Redelegate in epoch 3 redelegate_tokens( @@ -183,11 +203,26 @@ fn test_simple_redelegation_aux( // Advance three epochs current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); // Unbond in epoch 5 from dest_validator let _ = unbond_tokens( @@ -239,7 +274,12 @@ fn test_simple_redelegation_aux( // Advance to withdrawal epoch loop { current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); if current_epoch == unbond_end { break; } @@ -324,7 +364,12 @@ fn test_slashes_with_unbonding_aux( s.commit_block().unwrap(); current_epoch = advance_epoch(&mut s, ¶ms); - process_slashes(&mut s, current_epoch).unwrap(); + process_slashes( + &mut s, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); // Discover first slash let slash_0_evidence_epoch = current_epoch; @@ -349,7 +394,12 @@ fn test_slashes_with_unbonding_aux( slash_0_evidence_epoch + params.slash_processing_epoch_offset(); while current_epoch < unfreeze_epoch { current_epoch = advance_epoch(&mut s, ¶ms); - process_slashes(&mut s, current_epoch).unwrap(); + process_slashes( + &mut s, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); } // Advance more epochs randomly from the generated delay @@ -386,7 +436,12 @@ fn test_slashes_with_unbonding_aux( let withdraw_epoch = unbond_epoch + params.withdrawable_epoch_offset(); while current_epoch < withdraw_epoch { current_epoch = advance_epoch(&mut s, ¶ms); - process_slashes(&mut s, current_epoch).unwrap(); + process_slashes( + &mut s, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); } let token = staking_token_address(&s); let val_balance_pre = read_balance(&s, &token, val_addr).unwrap(); @@ -496,7 +551,12 @@ fn test_redelegation_with_slashing_aux( for _ in 0..5 { current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); } let init_epoch = current_epoch; @@ -514,11 +574,26 @@ fn test_redelegation_with_slashing_aux( // Advance three epochs current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); // Redelegate in epoch 8 redelegate_tokens( @@ -558,11 +633,26 @@ fn test_redelegation_with_slashing_aux( // Advance three epochs current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); // Unbond in epoch 11 from dest_validator let _ = unbond_tokens( @@ -577,7 +667,12 @@ fn test_redelegation_with_slashing_aux( // Advance one epoch current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); // Discover evidence slash( @@ -631,7 +726,12 @@ fn test_redelegation_with_slashing_aux( // Advance to withdrawal epoch loop { current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); if current_epoch == unbond_end { break; } @@ -729,7 +829,12 @@ fn test_chain_redelegations_aux(mut validators: Vec) { // Advance one epoch current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); // Redelegate in epoch 1 to dest_validator let redel_amount_1: token::Amount = 58.into(); @@ -842,9 +947,19 @@ fn test_chain_redelegations_aux(mut validators: Vec) { // Attempt to redelegate in epoch 3 to dest_validator current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); let redel_amount_2: token::Amount = 23.into(); let redel_att = redelegate_tokens( @@ -863,7 +978,12 @@ fn test_chain_redelegations_aux(mut validators: Vec) { redel_end.prev() + params.slash_processing_epoch_offset(); loop { current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); if current_epoch == epoch_can_redel.prev() { break; } @@ -882,7 +1002,12 @@ fn test_chain_redelegations_aux(mut validators: Vec) { // Advance one more epoch current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); // Redelegate from dest_validator to dest_validator_2 now redelegate_tokens( @@ -1154,7 +1279,12 @@ fn test_overslashing_aux(mut validators: Vec) { // Advance to processing epoch 1 loop { current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); if current_epoch == processing_epoch_1 { break; } @@ -1190,7 +1320,12 @@ fn test_overslashing_aux(mut validators: Vec) { // Advance to processing epoch 2 loop { current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); if current_epoch == processing_epoch_2 { break; } @@ -1333,7 +1468,12 @@ fn test_slashed_bond_amount_aux(validators: Vec) { // Advance an epoch to 1 current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); // Bond to validator 1 bond_tokens( @@ -1381,7 +1521,12 @@ fn test_slashed_bond_amount_aux(validators: Vec) { // Advance an epoch to ep 2 current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); // Bond to validator 1 bond_tokens( @@ -1419,7 +1564,12 @@ fn test_slashed_bond_amount_aux(validators: Vec) { // Advance two epochs to ep 4 for _ in 0..2 { current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); } // Find some slashes committed in various epochs @@ -1471,7 +1621,12 @@ fn test_slashed_bond_amount_aux(validators: Vec) { // Advance such that these slashes are all processed for _ in 0..params.slash_processing_epoch_offset() { current_epoch = advance_epoch(&mut storage, ¶ms); - process_slashes(&mut storage, current_epoch).unwrap(); + process_slashes( + &mut storage, + &mut namada_events::testing::VoidEventSink, + current_epoch, + ) + .unwrap(); } let pipeline_epoch = current_epoch + params.pipeline_len; diff --git a/crates/state/src/host_env.rs b/crates/state/src/host_env.rs index 5a6804b5ba..d270c11d19 100644 --- a/crates/state/src/host_env.rs +++ b/crates/state/src/host_env.rs @@ -1,5 +1,6 @@ use std::cell::RefCell; +use namada_events::{EmitEvents, EventToEmit}; use namada_gas::{GasMetering, TxGasMeter, VpGasMeter}; use namada_tx::data::TxSentinel; @@ -91,6 +92,30 @@ where } } +impl EmitEvents for TxHostEnvState<'_, D, H> +where + D: 'static + DB + for<'iter> DBIter<'iter>, + H: 'static + StorageHasher, +{ + #[inline] + fn emit(&mut self, event: E) + where + E: EventToEmit, + { + self.write_log_mut().emit_event(event); + } + + fn emit_many(&mut self, event_batch: B) + where + B: IntoIterator, + E: EventToEmit, + { + for event in event_batch { + self.emit(event.into()); + } + } +} + impl StateRead for VpHostEnvState<'_, D, H> where D: 'static + DB + for<'iter> DBIter<'iter>, diff --git a/crates/state/src/wl_state.rs b/crates/state/src/wl_state.rs index d40481ffcf..05af157f38 100644 --- a/crates/state/src/wl_state.rs +++ b/crates/state/src/wl_state.rs @@ -6,6 +6,7 @@ use namada_core::borsh::BorshSerializeExt; use namada_core::chain::ChainId; use namada_core::storage; use namada_core::time::DateTimeUtc; +use namada_events::{EmitEvents, EventToEmit}; use namada_parameters::EpochDuration; use namada_replay_protection as replay_protection; use namada_storage::conversion_state::{ConversionState, WithConversionState}; @@ -1087,6 +1088,30 @@ where } } +impl EmitEvents for FullAccessState +where + D: 'static + DB + for<'iter> DBIter<'iter>, + H: 'static + StorageHasher, +{ + #[inline] + fn emit(&mut self, event: E) + where + E: EventToEmit, + { + self.write_log_mut().emit_event(event); + } + + fn emit_many(&mut self, event_batch: B) + where + B: IntoIterator, + E: EventToEmit, + { + for event in event_batch { + self.emit(event.into()); + } + } +} + impl WithConversionState for FullAccessState where D: 'static + DB + for<'iter> DBIter<'iter>, @@ -1142,6 +1167,30 @@ where } } +impl EmitEvents for WlState +where + D: 'static + DB + for<'iter> DBIter<'iter>, + H: 'static + StorageHasher, +{ + #[inline] + fn emit(&mut self, event: E) + where + E: EventToEmit, + { + self.write_log_mut().emit_event(event); + } + + fn emit_many(&mut self, event_batch: B) + where + B: IntoIterator, + E: EventToEmit, + { + for event in event_batch { + self.emit(event.into()); + } + } +} + impl StateRead for TempWlState<'_, D, H> where D: 'static + DB + for<'iter> DBIter<'iter>, diff --git a/crates/state/src/wl_storage.rs b/crates/state/src/wl_storage.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/token/src/lib.rs b/crates/token/src/lib.rs index d525c68acc..f613b47106 100644 --- a/crates/token/src/lib.rs +++ b/crates/token/src/lib.rs @@ -9,6 +9,10 @@ pub mod storage_key { pub use namada_trans_token::storage_key::*; } +pub mod event { + pub use namada_trans_token::event::*; +} + use namada_core::address::Address; use namada_events::EmitEvents; use namada_storage::{Result, StorageRead, StorageWrite}; diff --git a/crates/trans_token/Cargo.toml b/crates/trans_token/Cargo.toml index a3302c4de8..a2a6f38a98 100644 --- a/crates/trans_token/Cargo.toml +++ b/crates/trans_token/Cargo.toml @@ -20,8 +20,10 @@ migrations = [ [dependencies] namada_core = { path = "../core" } +namada_events = { path = "../events", default-features = false } namada_storage = { path = "../storage" } +konst.workspace = true linkme = {workspace = true, optional = true} [dev-dependencies] diff --git a/crates/trans_token/src/event.rs b/crates/trans_token/src/event.rs new file mode 100644 index 0000000000..2f4435d8f8 --- /dev/null +++ b/crates/trans_token/src/event.rs @@ -0,0 +1,326 @@ +//! Token transaction events. + +use std::borrow::Cow; +use std::fmt; +use std::str::FromStr; + +use namada_core::address::Address; +use namada_core::uint::Uint; +use namada_events::extend::{Closure, ComposeEvent, EventAttributeEntry}; +use namada_events::{Event, EventLevel, EventToEmit, EventType}; + +pub mod types { + //! Token event types. + + use namada_events::{event_type, EventType}; + + use super::TokenEvent; + + /// Mint token event. + pub const MINT: EventType = event_type!(TokenEvent, "mint"); + + /// Burn token event. + pub const BURN: EventType = event_type!(TokenEvent, "burn"); + + /// Transfer token event. + pub const TRANSFER: EventType = event_type!(TokenEvent, "transfer"); +} + +/// A user account. +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub enum UserAccount { + /// Internal chain address in Namada. + Internal(Address), + /// External chain address. + External(String), +} + +impl fmt::Display for UserAccount { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Internal(addr) => write!(f, "internal-address/{addr}"), + Self::External(addr) => write!(f, "external-address/{addr}"), + } + } +} + +impl FromStr for UserAccount { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.split_once('/') { + Some(("internal-address", addr)) => { + Ok(Self::Internal(Address::decode(addr).map_err(|err| { + format!( + "Unknown internal address balance change target \ + {s:?}: {err}" + ) + })?)) + } + Some(("external-address", addr)) => { + Ok(Self::External(addr.to_owned())) + } + _ => Err(format!("Unknown balance change target {s:?}")), + } + } +} + +/// Token event kind. +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub enum TokenEventKind { + /// Token mint operation. + Mint, + /// Token burn operation. + Burn, + /// Token transfer operation. + Transfer, +} + +impl From<&TokenEventKind> for EventType { + fn from(token_event_kind: &TokenEventKind) -> Self { + match token_event_kind { + TokenEventKind::Mint => types::MINT, + TokenEventKind::Burn => types::BURN, + TokenEventKind::Transfer => types::TRANSFER, + } + } +} + +impl From for EventType { + fn from(token_event_kind: TokenEventKind) -> Self { + (&token_event_kind).into() + } +} + +/// Namada token event. +#[derive(Debug)] +pub struct TokenEvent { + /// The event level. + pub level: EventLevel, + /// The affected token address. + pub token: Address, + /// The operation that took place. + pub operation: TokenOperation, + /// Additional description of the token event. + pub descriptor: Cow<'static, str>, +} + +/// Namada token operation. +#[derive(Debug)] +pub enum TokenOperation { + /// Token mint event. + Mint { + /// The target account whose balance was changed. + target_account: UserAccount, + /// The amount of minted tokens. + amount: Uint, + /// The balance that `target_account` ended up with. + post_balance: Uint, + }, + /// Token burn event. + Burn { + /// The target account whose balance was changed. + target_account: UserAccount, + /// The amount of minted tokens. + amount: Uint, + /// The balance that `target_account` ended up with. + post_balance: Uint, + }, + /// Token transfer event. + Transfer { + /// The source of the token transfer. + source: UserAccount, + /// The target of the token transfer. + target: UserAccount, + /// The transferred amount. + amount: Uint, + /// The balance that `source` ended up with. + source_post_balance: Uint, + /// The balance that `target` ended up with, + /// if it is known. + target_post_balance: Option, + }, +} + +impl TokenOperation { + /// The token event kind associated with this operation. + pub fn kind(&self) -> TokenEventKind { + match self { + Self::Mint { .. } => TokenEventKind::Mint, + Self::Burn { .. } => TokenEventKind::Burn, + Self::Transfer { .. } => TokenEventKind::Transfer, + } + } +} + +impl EventToEmit for TokenEvent { + const DOMAIN: &'static str = "token"; +} + +impl From for Event { + fn from(token_event: TokenEvent) -> Self { + let event = + Self::new(token_event.operation.kind().into(), token_event.level) + .with(TokenAddress(token_event.token)) + .with(Descriptor(&token_event.descriptor)); + + match token_event.operation { + TokenOperation::Mint { + target_account, + amount, + post_balance, + } + | TokenOperation::Burn { + target_account, + amount, + post_balance, + } => event + .with(TargetAccount(target_account)) + .with(Amount(&amount)) + .with(TargetPostBalance(&post_balance)) + .into(), + TokenOperation::Transfer { + source, + target, + amount, + source_post_balance, + target_post_balance, + } => event + .with(SourceAccount(source)) + .with(TargetAccount(target)) + .with(Amount(&amount)) + .with(SourcePostBalance(&source_post_balance)) + .with(Closure(|event: &mut Event| { + if let Some(post_balance) = target_post_balance { + event.extend(TargetPostBalance(&post_balance)); + } + })) + .into(), + } + } +} + +/// Extend an [`Event`] with token event descriptor data. +pub struct Descriptor<'k>(pub &'k str); + +impl<'k> EventAttributeEntry<'k> for Descriptor<'k> { + type Value = &'k str; + type ValueOwned = String; + + const KEY: &'static str = "token-event-descriptor"; + + fn into_value(self) -> Self::Value { + self.0 + } +} + +/// Extend an [`Event`] with token address data. +pub struct TokenAddress(pub Address); + +impl EventAttributeEntry<'static> for TokenAddress { + type Value = Address; + type ValueOwned = Self::Value; + + const KEY: &'static str = "token-address"; + + fn into_value(self) -> Self::Value { + self.0 + } +} + +/// Extend an [`Event`] with source account data. +pub struct SourceAccount(pub UserAccount); + +impl EventAttributeEntry<'static> for SourceAccount { + type Value = UserAccount; + type ValueOwned = Self::Value; + + const KEY: &'static str = "source-account"; + + fn into_value(self) -> Self::Value { + self.0 + } +} + +/// Extend an [`Event`] with target account data. +pub struct TargetAccount(pub UserAccount); + +impl EventAttributeEntry<'static> for TargetAccount { + type Value = UserAccount; + type ValueOwned = Self::Value; + + const KEY: &'static str = "target-account"; + + fn into_value(self) -> Self::Value { + self.0 + } +} + +/// Extend an [`Event`] with amount data. +pub struct Amount<'amt>(pub &'amt Uint); + +impl<'amt> EventAttributeEntry<'amt> for Amount<'amt> { + type Value = &'amt Uint; + type ValueOwned = Uint; + + const KEY: &'static str = "amount"; + + fn into_value(self) -> Self::Value { + self.0 + } +} + +/// Extend an [`Event`] with source post balance data. +pub struct SourcePostBalance<'bal>(pub &'bal Uint); + +impl<'bal> EventAttributeEntry<'bal> for SourcePostBalance<'bal> { + type Value = &'bal Uint; + type ValueOwned = Uint; + + const KEY: &'static str = "source-post-balance"; + + fn into_value(self) -> Self::Value { + self.0 + } +} + +/// Extend an [`Event`] with target post balance data. +pub struct TargetPostBalance<'bal>(pub &'bal Uint); + +impl<'bal> EventAttributeEntry<'bal> for TargetPostBalance<'bal> { + type Value = &'bal Uint; + type ValueOwned = Uint; + + const KEY: &'static str = "target-post-balance"; + + fn into_value(self) -> Self::Value { + self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn user_account_str_roundtrip() { + let targets = [ + UserAccount::External( + "cosmos1hkgjfuznl4af5ayzn6gzl6kwwkcu28urxmqejg".to_owned(), + ), + UserAccount::Internal( + Address::decode( + "tnam1q82t25z5f9gmnv5sztyr8ht9tqhrw4u875qjhy56", + ) + .unwrap(), + ), + ]; + + for target in targets { + let as_str = target.to_string(); + let decoded: UserAccount = as_str.parse().unwrap(); + + assert_eq!(decoded, target); + } + } +} diff --git a/crates/trans_token/src/lib.rs b/crates/trans_token/src/lib.rs index 6644f73d7a..04b80b152c 100644 --- a/crates/trans_token/src/lib.rs +++ b/crates/trans_token/src/lib.rs @@ -1,5 +1,6 @@ //! Transparent token types, storage functions, and validation. +pub mod event; mod storage; pub mod storage_key; diff --git a/crates/trans_token/src/storage.rs b/crates/trans_token/src/storage.rs index 04048b466c..a6ad50926c 100644 --- a/crates/trans_token/src/storage.rs +++ b/crates/trans_token/src/storage.rs @@ -247,15 +247,32 @@ where storage.write(&src_key, new_src_balance)?; storage.write(&dest_key, new_dest_balance) } - None => Err(storage::Error::new_const( - "The transfer would overflow destination balance", - )), + None => Err(storage::Error::new_alloc(format!( + "The transfer would overflow balance of {dest}" + ))), } } - None => Err(storage::Error::new_const("Insufficient source balance")), + None => Err(storage::Error::new_alloc(format!( + "{src} has insufficient balance" + ))), } } +/// Mint `amount` of `token` as `minter` to `dest`. +pub fn mint_tokens( + storage: &mut S, + minter: &Address, + token: &Address, + dest: &Address, + amount: token::Amount, +) -> storage::Result<()> +where + S: StorageRead + StorageWrite, +{ + credit_tokens(storage, token, dest, amount)?; + storage.write(&minter_key(token), minter.clone()) +} + /// Credit tokens to an account, to be used only by protocol. In transactions, /// this would get rejected by the default `vp_token`. pub fn credit_tokens( diff --git a/crates/tx_prelude/src/ibc.rs b/crates/tx_prelude/src/ibc.rs index 4c2c7a9f3d..61f470bae6 100644 --- a/crates/tx_prelude/src/ibc.rs +++ b/crates/tx_prelude/src/ibc.rs @@ -4,20 +4,20 @@ use std::cell::RefCell; use std::collections::BTreeSet; use std::rc::Rc; -use namada_core::address::{Address, InternalAddress}; +use namada_core::address::Address; use namada_core::token::Amount; use namada_events::EventTypeBuilder; pub use namada_ibc::event::{IbcEvent, IbcEventType}; -pub use namada_ibc::storage::{ibc_token, is_ibc_key}; +pub use namada_ibc::storage::{ + burn_tokens, ibc_token, is_ibc_key, mint_tokens, +}; pub use namada_ibc::{ IbcActions, IbcCommonContext, IbcStorageContext, NftTransferModule, ProofSpec, TransferModule, }; -use namada_storage::StorageWrite; -use namada_token::storage_key::minter_key; use namada_tx_env::TxEnv; -use crate::token::{burn, mint, transfer}; +use crate::token::transfer; use crate::{Ctx, Error}; /// IBC actions to handle an IBC message. The `verifiers` inserted into the set @@ -82,10 +82,7 @@ impl IbcStorageContext for Ctx { token: &Address, amount: Amount, ) -> Result<(), Error> { - mint(self, target, token, amount)?; - - let minter_key = minter_key(token); - self.write(&minter_key, &Address::Internal(InternalAddress::Ibc)) + mint_tokens(self, target, token, amount) } fn burn_token( @@ -94,7 +91,7 @@ impl IbcStorageContext for Ctx { token: &Address, amount: Amount, ) -> Result<(), Error> { - burn(self, target, token, amount) + burn_tokens(self, target, token, amount) } fn log_string(&self, message: String) { diff --git a/crates/tx_prelude/src/lib.rs b/crates/tx_prelude/src/lib.rs index bba65283b1..3948342af8 100644 --- a/crates/tx_prelude/src/lib.rs +++ b/crates/tx_prelude/src/lib.rs @@ -31,7 +31,7 @@ pub use namada_core::storage::{ self, BlockHash, BlockHeight, Epoch, Header, BLOCK_HASH_LENGTH, }; pub use namada_core::{encode, eth_bridge_pool, *}; -use namada_events::{Event, EventToEmit, EventType}; +use namada_events::{EmitEvents, Event, EventToEmit, EventType}; pub use namada_governance::storage as gov_storage; pub use namada_macros::transaction; pub use namada_parameters::storage as parameters_storage; @@ -249,6 +249,26 @@ impl StorageWrite for Ctx { } } +impl EmitEvents for Ctx { + #[inline] + fn emit(&mut self, event: E) + where + E: EventToEmit, + { + _ = self.emit_event(event); + } + + fn emit_many(&mut self, event_batch: B) + where + B: IntoIterator, + E: EventToEmit, + { + for event in event_batch { + self.emit(event.into()); + } + } +} + impl TxEnv for Ctx { fn read_bytes_temp( &self, diff --git a/crates/tx_prelude/src/token.rs b/crates/tx_prelude/src/token.rs index f62ec8d5a5..7eaa1129a7 100644 --- a/crates/tx_prelude/src/token.rs +++ b/crates/tx_prelude/src/token.rs @@ -1,10 +1,11 @@ use namada_core::address::Address; -use namada_proof_of_stake::token::storage_key::balance_key; -use namada_storage::{Error as StorageError, ResultExt}; -pub use namada_token::*; +use namada_events::{EmitEvents, EventLevel}; +pub use namada_token::{ + storage_key, utils, Amount, DenominatedAmount, Transfer, +}; use namada_tx_env::TxEnv; -use crate::{Ctx, StorageRead, StorageWrite, TxResult}; +use crate::{Ctx, TxResult}; /// A token transfer that can be used in a transaction. pub fn transfer( @@ -14,6 +15,8 @@ pub fn transfer( token: &Address, amount: Amount, ) -> TxResult { + use namada_token::event::{TokenEvent, TokenOperation, UserAccount}; + // The tx must be authorized by the source address ctx.insert_verifier(src)?; if token.is_internal() { @@ -23,47 +26,23 @@ pub fn transfer( ctx.insert_verifier(token)?; } - if amount == Amount::zero() { - return Ok(()); - } - - let src_key = balance_key(token, src); - let dest_key = balance_key(token, dest); - let src_bal: Option = ctx.read(&src_key)?; - let mut src_bal = src_bal - .ok_or_else(|| StorageError::new_const("the source has no balance"))?; - - if !src_bal.can_spend(&amount) { - return Err(StorageError::new_const( - "the source has no enough balance", - )); - } - - src_bal.spend(&amount).into_storage_result()?; - let mut dest_bal: Amount = ctx.read(&dest_key)?.unwrap_or_default(); - dest_bal.receive(&amount).into_storage_result()?; - ctx.write(&src_key, src_bal)?; - ctx.write(&dest_key, dest_bal)?; + namada_token::transfer(ctx, token, src, dest, amount)?; + + ctx.emit(TokenEvent { + descriptor: "transfer-from-wasm".into(), + level: EventLevel::Tx, + token: token.clone(), + operation: TokenOperation::Transfer { + amount: amount.into(), + source: UserAccount::Internal(src.clone()), + target: UserAccount::Internal(dest.clone()), + source_post_balance: namada_token::read_balance(ctx, token, src)? + .into(), + target_post_balance: Some( + namada_token::read_balance(ctx, token, dest)?.into(), + ), + }, + }); Ok(()) } - -/// Mint that can be used in a transaction. -pub fn mint( - ctx: &mut Ctx, - target: &Address, - token: &Address, - amount: Amount, -) -> TxResult { - credit_tokens(ctx, token, target, amount) -} - -/// Burn that can be used in a transaction. -pub fn burn( - ctx: &mut Ctx, - target: &Address, - token: &Address, - amount: Amount, -) -> TxResult { - burn_tokens(ctx, token, target, amount) -} diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 9cc812238b..a63f32ff43 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -3977,6 +3977,7 @@ dependencies = [ "borsh 1.4.0", "data-encoding", "derivative", + "konst", "linkme", "namada_account", "namada_controller", @@ -4192,7 +4193,9 @@ dependencies = [ name = "namada_trans_token" version = "0.34.0" dependencies = [ + "konst", "namada_core", + "namada_events", "namada_storage", ] diff --git a/wasm_for_tests/Cargo.lock b/wasm_for_tests/Cargo.lock index b9005775b6..f8bcde2d2e 100644 --- a/wasm_for_tests/Cargo.lock +++ b/wasm_for_tests/Cargo.lock @@ -3931,6 +3931,7 @@ dependencies = [ "borsh 1.2.1", "data-encoding", "derivative", + "konst", "namada_account", "namada_controller", "namada_core", @@ -4138,7 +4139,9 @@ dependencies = [ name = "namada_trans_token" version = "0.34.0" dependencies = [ + "konst", "namada_core", + "namada_events", "namada_storage", ]