diff --git a/.changelog/unreleased/improvements/2382-fee-refactor.md b/.changelog/unreleased/improvements/2382-fee-refactor.md new file mode 100644 index 0000000000..6fee457ef0 --- /dev/null +++ b/.changelog/unreleased/improvements/2382-fee-refactor.md @@ -0,0 +1,2 @@ +- Refactored the fee validation process. + ([\#2382](https://github.com/anoma/namada/pull/2382)) \ No newline at end of file diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index e8d5da8b83..15c251cfa1 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -901,44 +901,6 @@ where } } - /// Checks that neither the wrapper nor the inner transaction have already - /// been applied. Requires a [`TempWlStorage`] to perform the check during - /// block construction and validation - pub fn replay_protection_checks( - &self, - wrapper: &Tx, - temp_wl_storage: &mut TempWlStorage, - ) -> Result<()> { - let inner_tx_hash = wrapper.raw_header_hash(); - // Check the inner tx hash only against the storage, skip the write - // log - if temp_wl_storage - .has_committed_replay_protection_entry(&inner_tx_hash) - .expect("Error while checking inner tx hash key in storage") - { - return Err(Error::ReplayAttempt(format!( - "Inner transaction hash {} already in storage", - &inner_tx_hash, - ))); - } - - let wrapper_hash = wrapper.header_hash(); - if temp_wl_storage - .has_replay_protection_entry(&wrapper_hash) - .expect("Error while checking wrapper tx hash key in storage") - { - return Err(Error::ReplayAttempt(format!( - "Wrapper transaction hash {} already in storage", - wrapper_hash - ))); - } - - // Write wrapper hash to WAL - temp_wl_storage - .write_tx_hash(wrapper_hash) - .map_err(|e| Error::ReplayAttempt(e.to_string())) - } - /// If a handle to an Ethereum oracle was provided to the [`Shell`], attempt /// to send it an updated configuration, using a configuration /// based on Ethereum bridge parameters in blockchain storage. @@ -1287,14 +1249,12 @@ where } // Validate wrapper fees - if let Err(e) = self.wrapper_fee_check( + if let Err(e) = mempool_fee_check( &wrapper, get_fee_unshielding_transaction(&tx, &wrapper), &mut TempWlStorage::new(&self.wl_storage.storage), &mut self.vp_wasm_cache.clone(), &mut self.tx_wasm_cache.clone(), - None, - false, ) { response.code = ResultCode::FeeError.into(); response.log = format!("{INVALID_MSG}: {e}"); @@ -1322,170 +1282,6 @@ where response } - /// Check that the Wrapper's signer has enough funds to pay fees. If a block - /// proposer is provided, updates the balance of the fee payer - #[allow(clippy::too_many_arguments)] - pub fn wrapper_fee_check( - &self, - wrapper: &WrapperTx, - masp_transaction: Option, - temp_wl_storage: &mut TempWlStorage, - vp_wasm_cache: &mut VpCache, - tx_wasm_cache: &mut TxCache, - block_proposer: Option<&Address>, - is_prepare_proposal: bool, - ) -> Result<()> - where - CA: 'static + WasmCacheAccess + Sync, - { - // Check that fee token is an allowed one - let minimum_gas_price = { - let proposer_local_config = if is_prepare_proposal { - if let ShellMode::Validator { - ref local_config, .. - } = self.mode - { - local_config.as_ref() - } else { - None - } - } else { - None - }; - - match proposer_local_config { - Some(config) => config - .accepted_gas_tokens - .get(&wrapper.fee.token) - .ok_or(Error::TxApply(protocol::Error::FeeError(format!( - "The provided {} token is not accepted by the block \ - proposer for fee payment", - wrapper.fee.token - ))))? - .to_owned(), - None => namada::ledger::parameters::read_gas_cost( - &self.wl_storage, - &wrapper.fee.token, - ) - .expect("Must be able to read gas cost parameter") - .ok_or(Error::TxApply( - protocol::Error::FeeError(format!( - "The provided {} token is not allowed for fee payment", - wrapper.fee.token - )), - ))?, - } - }; - - match wrapper - .fee - .amount_per_gas_unit - .to_amount(&wrapper.fee.token, &self.wl_storage) - { - Ok(amount_per_gas_unit) - if amount_per_gas_unit < minimum_gas_price => - { - // The fees do not match the minimum required - return Err(Error::TxApply(protocol::Error::FeeError( - format!( - "Fee amount {:?} do not match the minimum required \ - amount {:?} for token {}", - wrapper.fee.amount_per_gas_unit, - minimum_gas_price, - wrapper.fee.token - ), - ))); - } - Ok(_) => {} - Err(err) => { - return Err(Error::TxApply(protocol::Error::FeeError( - format!( - "The precision of the fee amount {:?} is higher than \ - the denomination for token {}: {}", - wrapper.fee.amount_per_gas_unit, wrapper.fee.token, err, - ), - ))); - } - } - - if let Some(transaction) = masp_transaction { - // Validation of the commitment to this section is done when - // checking the aggregated signature of the wrapper, no need for - // further validation - - // Validate data and generate unshielding tx - let transfer_code_hash = - get_transfer_hash_from_storage(temp_wl_storage); - - let descriptions_limit = self.wl_storage.read(¶meters::storage::get_fee_unshielding_descriptions_limit_key()).expect("Error reading the storage").expect("Missing fee unshielding descriptions limit param in storage"); - - let unshield = wrapper - .check_and_generate_fee_unshielding( - transfer_code_hash, - Some(namada_sdk::tx::TX_TRANSFER_WASM.to_string()), - descriptions_limit, - transaction, - ) - .map_err(|e| { - Error::TxApply(protocol::Error::FeeUnshieldingError(e)) - })?; - - let fee_unshielding_gas_limit = temp_wl_storage - .read(¶meters::storage::get_fee_unshielding_gas_limit_key()) - .expect("Error reading from storage") - .expect("Missing fee unshielding gas limit in storage"); - - // Runtime check - // NOTE: A clean tx write log must be provided to this call for a - // correct vp validation. Block write log, instead, should contain - // any prior changes (if any). This is to simulate the - // unshielding tx (to prevent the already written keys - // from being passed/triggering VPs) but we cannot - // commit the tx write log yet cause the tx could still - // be invalid. - temp_wl_storage.write_log.precommit_tx(); - - match apply_wasm_tx( - unshield, - &TxIndex::default(), - ShellParams::new( - &mut TxGasMeter::new(fee_unshielding_gas_limit), - temp_wl_storage, - vp_wasm_cache, - tx_wasm_cache, - ), - ) { - Ok(result) => { - if !result.is_accepted() { - return Err(Error::TxApply( - protocol::Error::FeeUnshieldingError(namada::types::transaction::WrapperTxErr::InvalidUnshield(format!( - "Some VPs rejected fee unshielding: {:#?}", - result.vps_result.rejected_vps - ))), - )); - } - } - Err(e) => { - return Err(Error::TxApply( - protocol::Error::FeeUnshieldingError(namada::types::transaction::WrapperTxErr::InvalidUnshield(format!( - "Wasm run failed: {}", - e - ))), - )); - } - } - } - - let result = match block_proposer { - Some(proposer) => { - protocol::transfer_fee(temp_wl_storage, proposer, wrapper) - } - None => protocol::check_fees(temp_wl_storage, wrapper), - }; - - result.map_err(Error::TxApply) - } - fn get_abci_validator_updates( &self, is_genesis: bool, @@ -1540,6 +1336,216 @@ where } } +/// Checks that neither the wrapper nor the inner transaction have already +/// been applied. Requires a [`TempWlStorage`] to perform the check during +/// block construction and validation +pub fn replay_protection_checks( + wrapper: &Tx, + temp_wl_storage: &mut TempWlStorage, +) -> Result<()> +where + D: DB + for<'iter> DBIter<'iter> + Sync + 'static, + H: StorageHasher + Sync + 'static, +{ + let inner_tx_hash = wrapper.raw_header_hash(); + // Check the inner tx hash only against the storage, skip the write + // log + if temp_wl_storage + .has_committed_replay_protection_entry(&inner_tx_hash) + .expect("Error while checking inner tx hash key in storage") + { + return Err(Error::ReplayAttempt(format!( + "Inner transaction hash {} already in storage", + &inner_tx_hash, + ))); + } + + let wrapper_hash = wrapper.header_hash(); + if temp_wl_storage + .has_replay_protection_entry(&wrapper_hash) + .expect("Error while checking wrapper tx hash key in storage") + { + return Err(Error::ReplayAttempt(format!( + "Wrapper transaction hash {} already in storage", + wrapper_hash + ))); + } + + // Write wrapper hash to WAL + temp_wl_storage + .write_tx_hash(wrapper_hash) + .map_err(|e| Error::ReplayAttempt(e.to_string())) +} + +// Perform the fee check in mempool +fn mempool_fee_check( + wrapper: &WrapperTx, + masp_transaction: Option, + temp_wl_storage: &mut TempWlStorage, + vp_wasm_cache: &mut VpCache, + tx_wasm_cache: &mut TxCache, +) -> Result<()> +where + D: DB + for<'iter> DBIter<'iter> + Sync + 'static, + H: StorageHasher + Sync + 'static, + CA: 'static + WasmCacheAccess + Sync, +{ + let minimum_gas_price = namada::ledger::parameters::read_gas_cost( + temp_wl_storage, + &wrapper.fee.token, + ) + .expect("Must be able to read gas cost parameter") + .ok_or(Error::TxApply(protocol::Error::FeeError(format!( + "The provided {} token is not allowed for fee payment", + wrapper.fee.token + ))))?; + + wrapper_fee_check( + wrapper, + masp_transaction, + minimum_gas_price, + temp_wl_storage, + vp_wasm_cache, + tx_wasm_cache, + )?; + protocol::check_fees(temp_wl_storage, wrapper).map_err(Error::TxApply) +} + +/// Check the validity of the fee payment, including the minimum amounts +/// required and the optional unshield +pub fn wrapper_fee_check( + wrapper: &WrapperTx, + masp_transaction: Option, + minimum_gas_price: token::Amount, + temp_wl_storage: &mut TempWlStorage, + vp_wasm_cache: &mut VpCache, + tx_wasm_cache: &mut TxCache, +) -> Result<()> +where + D: DB + for<'iter> DBIter<'iter> + Sync + 'static, + H: StorageHasher + Sync + 'static, + CA: 'static + WasmCacheAccess + Sync, +{ + match wrapper + .fee + .amount_per_gas_unit + .to_amount(&wrapper.fee.token, temp_wl_storage) + { + Ok(amount_per_gas_unit) if amount_per_gas_unit < minimum_gas_price => { + // The fees do not match the minimum required + return Err(Error::TxApply(protocol::Error::FeeError(format!( + "Fee amount {:?} do not match the minimum required amount \ + {:?} for token {}", + wrapper.fee.amount_per_gas_unit, + minimum_gas_price, + wrapper.fee.token + )))); + } + Ok(_) => {} + Err(err) => { + return Err(Error::TxApply(protocol::Error::FeeError(format!( + "The precision of the fee amount {:?} is higher than the \ + denomination for token {}: {}", + wrapper.fee.amount_per_gas_unit, wrapper.fee.token, err, + )))); + } + } + + if let Some(transaction) = masp_transaction { + fee_unshielding_validation( + wrapper, + transaction, + temp_wl_storage, + vp_wasm_cache, + tx_wasm_cache, + )?; + } + + Ok(()) +} + +// Verifies the correctness of the masp transaction for fee payment +fn fee_unshielding_validation( + wrapper: &WrapperTx, + masp_transaction: Transaction, + temp_wl_storage: &mut TempWlStorage, + vp_wasm_cache: &mut VpCache, + tx_wasm_cache: &mut TxCache, +) -> Result<()> +where + D: DB + for<'iter> DBIter<'iter> + Sync + 'static, + H: StorageHasher + Sync + 'static, + CA: 'static + WasmCacheAccess + Sync, +{ + // Validation of the commitment to this section is done when + // checking the aggregated signature of the wrapper, no need for + // further validation + + // Validate data and generate unshielding tx + let transfer_code_hash = get_transfer_hash_from_storage(temp_wl_storage); + + let descriptions_limit = temp_wl_storage + .read( + ¶meters::storage::get_fee_unshielding_descriptions_limit_key(), + ) + .expect("Error reading the storage") + .expect("Missing fee unshielding descriptions limit param in storage"); + + let unshield = wrapper + .check_and_generate_fee_unshielding( + transfer_code_hash, + Some(namada_sdk::tx::TX_TRANSFER_WASM.to_string()), + descriptions_limit, + masp_transaction, + ) + .map_err(|e| Error::TxApply(protocol::Error::FeeUnshieldingError(e)))?; + + let fee_unshielding_gas_limit = temp_wl_storage + .read(¶meters::storage::get_fee_unshielding_gas_limit_key()) + .expect("Error reading from storage") + .expect("Missing fee unshielding gas limit in storage"); + + // Runtime check + // NOTE: A clean tx write log must be provided to this call for a + // correct vp validation. Block write log, instead, should contain + // any prior changes (if any). This is to simulate the + // unshielding tx (to prevent the already written keys + // from being passed/triggering VPs) but we cannot + // commit the tx write log yet cause the tx could still + // be invalid. + temp_wl_storage.write_log.precommit_tx(); + + let result = apply_wasm_tx( + unshield, + &TxIndex::default(), + ShellParams::new( + &mut TxGasMeter::new(fee_unshielding_gas_limit), + temp_wl_storage, + vp_wasm_cache, + tx_wasm_cache, + ), + ) + .map_err(|e| { + Error::TxApply(protocol::Error::FeeUnshieldingError( + namada::types::transaction::WrapperTxErr::InvalidUnshield(format!( + "Wasm run failed: {}", + e + )), + )) + })?; + + if result.is_accepted() { + Ok(()) + } else { + Err(Error::TxApply(protocol::Error::FeeUnshieldingError( + namada::types::transaction::WrapperTxErr::InvalidUnshield(format!( + "Some VPs rejected fee unshielding: {:#?}", + result.vps_result.rejected_vps + )), + ))) + } +} + /// for the shell #[cfg(test)] mod test_utils { diff --git a/apps/src/lib/node/ledger/shell/prepare_proposal.rs b/apps/src/lib/node/ledger/shell/prepare_proposal.rs index 1161401180..667f7c4f28 100644 --- a/apps/src/lib/node/ledger/shell/prepare_proposal.rs +++ b/apps/src/lib/node/ledger/shell/prepare_proposal.rs @@ -1,9 +1,10 @@ //! Implementation of the [`RequestPrepareProposal`] ABCI++ method for the Shell +use masp_primitives::transaction::Transaction; use namada::core::hints; use namada::core::ledger::gas::TxGasMeter; use namada::ledger::pos::PosQueries; -use namada::ledger::protocol::get_fee_unshielding_transaction; +use namada::ledger::protocol; use namada::ledger::storage::{DBIter, StorageHasher, TempWlStorage, DB}; use namada::proof_of_stake::storage::find_validator_by_raw_hash; use namada::proto::Tx; @@ -11,7 +12,7 @@ use namada::types::address::Address; use namada::types::internal::TxInQueue; use namada::types::key::tm_raw_hash_to_string; use namada::types::time::DateTimeUtc; -use namada::types::transaction::{DecryptedTx, TxType}; +use namada::types::transaction::{DecryptedTx, TxType, WrapperTx}; use namada::vm::wasm::{TxCache, VpCache}; use namada::vm::WasmCacheAccess; @@ -21,6 +22,7 @@ use super::block_alloc::states::{ EncryptedTxBatchAllocator, NextState, TryAlloc, }; use super::block_alloc::{AllocFailure, BlockAllocator, BlockResources}; +use crate::config::ValidatorLocalConfig; use crate::facade::tendermint_proto::google::protobuf::Timestamp; use crate::facade::tendermint_proto::v0_37::abci::RequestPrepareProposal; use crate::node::ledger::shell::ShellMode; @@ -43,7 +45,10 @@ where &self, req: RequestPrepareProposal, ) -> response::PrepareProposal { - let txs = if let ShellMode::Validator { .. } = self.mode { + let txs = if let ShellMode::Validator { + ref local_config, .. + } = self.mode + { // start counting allotted space for txs let alloc = self.get_encrypted_txs_allocator(); @@ -64,6 +69,7 @@ where &req.txs, req.time, &block_proposer, + local_config.as_ref(), ); let mut txs = encrypted_txs; // decrypt the wrapper txs included in the previous block @@ -131,6 +137,7 @@ where txs: &[TxBytes], block_time: Option, block_proposer: &Address, + proposer_local_config: Option<&ValidatorLocalConfig>, ) -> (Vec, BlockAllocator) { let pos_queries = self.wl_storage.pos_queries(); let block_time = block_time.and_then(|block_time| { @@ -145,7 +152,7 @@ where let txs = txs .iter() .filter_map(|tx_bytes| { - match self.validate_wrapper_bytes(tx_bytes, block_time, &mut temp_wl_storage, &mut vp_wasm_cache, &mut tx_wasm_cache, block_proposer) { + match validate_wrapper_bytes(tx_bytes, block_time, block_proposer, proposer_local_config, &mut temp_wl_storage, &mut vp_wasm_cache, &mut tx_wasm_cache, ) { Ok(gas) => { temp_wl_storage.write_log.commit_tx(); Some((tx_bytes.to_owned(), gas)) @@ -193,60 +200,6 @@ where (txs, alloc) } - /// Validity checks on a wrapper tx - #[allow(clippy::too_many_arguments)] - fn validate_wrapper_bytes( - &self, - tx_bytes: &[u8], - block_time: Option, - temp_wl_storage: &mut TempWlStorage, - vp_wasm_cache: &mut VpCache, - tx_wasm_cache: &mut TxCache, - block_proposer: &Address, - ) -> Result - where - CA: 'static + WasmCacheAccess + Sync, - { - let tx = Tx::try_from(tx_bytes).map_err(|_| ())?; - - // If tx doesn't have an expiration it is valid. If time cannot be - // retrieved from block default to last block datetime which has - // already been checked by mempool_validate, so it's valid - if let (Some(block_time), Some(exp)) = - (block_time.as_ref(), &tx.header().expiration) - { - if block_time > exp { - return Err(()); - } - } - - tx.validate_tx().map_err(|_| ())?; - if let TxType::Wrapper(wrapper) = tx.header().tx_type { - // Check tx gas limit for tx size - let mut tx_gas_meter = TxGasMeter::new(wrapper.gas_limit); - tx_gas_meter.add_wrapper_gas(tx_bytes).map_err(|_| ())?; - - self.replay_protection_checks(&tx, temp_wl_storage) - .map_err(|_| ())?; - - // Check fees - match self.wrapper_fee_check( - &wrapper, - get_fee_unshielding_transaction(&tx, &wrapper), - temp_wl_storage, - vp_wasm_cache, - tx_wasm_cache, - Some(block_proposer), - true, - ) { - Ok(()) => Ok(u64::from(wrapper.gas_limit)), - Err(_) => Err(()), - } - } else { - Err(()) - } - } - /// Builds a batch of DKG decrypted transactions. // NOTE: we won't have frontrunning protection until V2 of the // Anoma protocol; Namada runs V1, therefore this method is @@ -369,6 +322,114 @@ where } } +// Validity checks on a wrapper tx +#[allow(clippy::too_many_arguments)] +fn validate_wrapper_bytes( + tx_bytes: &[u8], + block_time: Option, + block_proposer: &Address, + proposer_local_config: Option<&ValidatorLocalConfig>, + temp_wl_storage: &mut TempWlStorage, + vp_wasm_cache: &mut VpCache, + tx_wasm_cache: &mut TxCache, +) -> Result +where + D: DB + for<'iter> DBIter<'iter> + Sync + 'static, + H: StorageHasher + Sync + 'static, + CA: 'static + WasmCacheAccess + Sync, +{ + let tx = Tx::try_from(tx_bytes).map_err(|_| ())?; + + // If tx doesn't have an expiration it is valid. If time cannot be + // retrieved from block default to last block datetime which has + // already been checked by mempool_validate, so it's valid + if let (Some(block_time), Some(exp)) = + (block_time.as_ref(), &tx.header().expiration) + { + if block_time > exp { + return Err(()); + } + } + + tx.validate_tx().map_err(|_| ())?; + if let TxType::Wrapper(wrapper) = tx.header().tx_type { + // Check tx gas limit for tx size + let mut tx_gas_meter = TxGasMeter::new(wrapper.gas_limit); + tx_gas_meter.add_wrapper_gas(tx_bytes).map_err(|_| ())?; + + super::replay_protection_checks(&tx, temp_wl_storage) + .map_err(|_| ())?; + + // Check fees and extract the gas limit of this transaction + match prepare_proposal_fee_check( + &wrapper, + protocol::get_fee_unshielding_transaction(&tx, &wrapper), + block_proposer, + proposer_local_config, + temp_wl_storage, + vp_wasm_cache, + tx_wasm_cache, + ) { + Ok(()) => Ok(u64::from(wrapper.gas_limit)), + Err(_) => Err(()), + } + } else { + Err(()) + } +} + +fn prepare_proposal_fee_check( + wrapper: &WrapperTx, + masp_transaction: Option, + proposer: &Address, + proposer_local_config: Option<&ValidatorLocalConfig>, + temp_wl_storage: &mut TempWlStorage, + vp_wasm_cache: &mut VpCache, + tx_wasm_cache: &mut TxCache, +) -> Result<(), Error> +where + D: DB + for<'iter> DBIter<'iter> + Sync + 'static, + H: StorageHasher + Sync + 'static, + CA: 'static + WasmCacheAccess + Sync, +{ + let minimum_gas_price = { + // A local config of the validator overrides the consensus param + // when creating a block + match proposer_local_config { + Some(config) => config + .accepted_gas_tokens + .get(&wrapper.fee.token) + .ok_or(Error::TxApply(protocol::Error::FeeError(format!( + "The provided {} token is not accepted by the block \ + proposer for fee payment", + wrapper.fee.token + ))))? + .to_owned(), + None => namada::ledger::parameters::read_gas_cost( + temp_wl_storage, + &wrapper.fee.token, + ) + .expect("Must be able to read gas cost parameter") + .ok_or(Error::TxApply(protocol::Error::FeeError(format!( + "The provided {} token is not allowed for fee payment", + wrapper.fee.token + ))))?, + } + }; + + super::wrapper_fee_check( + wrapper, + masp_transaction, + minimum_gas_price, + temp_wl_storage, + vp_wasm_cache, + tx_wasm_cache, + )?; + + protocol::transfer_fee(temp_wl_storage, proposer, wrapper) + .map_err(Error::TxApply) +} + #[cfg(test)] // TODO: write tests for validator set update vote extensions in // prepare proposals diff --git a/apps/src/lib/node/ledger/shell/process_proposal.rs b/apps/src/lib/node/ledger/shell/process_proposal.rs index 5677e67fc0..d44742d676 100644 --- a/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -652,7 +652,7 @@ where // Replay protection checks if let Err(e) = - self.replay_protection_checks(&tx, temp_wl_storage) + super::replay_protection_checks(&tx, temp_wl_storage) { return TxResult { code: ResultCode::ReplayTx.into(), @@ -661,14 +661,13 @@ where } // Check that the fee payer has sufficient balance. - match self.wrapper_fee_check( + match process_proposal_fee_check( &wrapper, get_fee_unshielding_transaction(&tx, &wrapper), + block_proposer, temp_wl_storage, vp_wasm_cache, tx_wasm_cache, - Some(block_proposer), - false, ) { Ok(()) => TxResult { code: ResultCode::Ok.into(), @@ -701,6 +700,42 @@ where } } +fn process_proposal_fee_check( + wrapper: &WrapperTx, + masp_transaction: Option, + proposer: &Address, + temp_wl_storage: &mut TempWlStorage, + vp_wasm_cache: &mut VpCache, + tx_wasm_cache: &mut TxCache, +) -> Result<()> +where + D: DB + for<'iter> DBIter<'iter> + Sync + 'static, + H: StorageHasher + Sync + 'static, + CA: 'static + WasmCacheAccess + Sync, +{ + let minimum_gas_price = namada::ledger::parameters::read_gas_cost( + temp_wl_storage, + &wrapper.fee.token, + ) + .expect("Must be able to read gas cost parameter") + .ok_or(Error::TxApply(protocol::Error::FeeError(format!( + "The provided {} token is not allowed for fee payment", + wrapper.fee.token + ))))?; + + wrapper_fee_check( + wrapper, + masp_transaction, + minimum_gas_price, + temp_wl_storage, + vp_wasm_cache, + tx_wasm_cache, + )?; + + protocol::transfer_fee(temp_wl_storage, proposer, wrapper) + .map_err(Error::TxApply) +} + /// We test the failure cases of [`process_proposal`]. The happy flows /// are covered by the e2e tests. #[cfg(test)]