diff --git a/engine/src/eth/retry_rpc.rs b/engine/src/eth/retry_rpc.rs index caffc91f53..a0bd094dbf 100644 --- a/engine/src/eth/retry_rpc.rs +++ b/engine/src/eth/retry_rpc.rs @@ -102,6 +102,8 @@ pub trait EthersRetryRpcApi: Clone { newest_block: BlockNumber, reward_percentiles: Vec, ) -> FeeHistory; + + async fn get_transaction(&self, tx_hash: H256) -> Transaction; } #[async_trait::async_trait] @@ -262,6 +264,18 @@ impl EthersRetryRpcApi for EthersRetryRpcClient { ) .await } + + async fn get_transaction(&self, tx_hash: H256) -> Transaction { + self.rpc_retry_client + .request( + Box::pin(move |client| { + #[allow(clippy::redundant_async_block)] + Box::pin(async move { client.get_transaction(tx_hash).await }) + }), + RequestLog::new("get_transaction".to_string(), Some(format!("{tx_hash:?}"))), + ) + .await + } } #[async_trait::async_trait] @@ -360,6 +374,8 @@ pub mod mocks { newest_block: BlockNumber, reward_percentiles: Vec, ) -> FeeHistory; + + async fn get_transaction(&self, tx_hash: H256) -> Transaction; } } } diff --git a/engine/src/eth/rpc.rs b/engine/src/eth/rpc.rs index 25c9d412e0..9ff941b67e 100644 --- a/engine/src/eth/rpc.rs +++ b/engine/src/eth/rpc.rs @@ -125,6 +125,8 @@ pub trait EthRpcApi: Send { newest_block: BlockNumber, reward_percentiles: &[f64], ) -> Result; + + async fn get_transaction(&self, tx_hash: H256) -> Result; } #[async_trait::async_trait] @@ -186,6 +188,13 @@ impl EthRpcApi for EthRpcClient { ) -> Result { Ok(self.signer.fee_history(block_count, last_block, reward_percentiles).await?) } + + async fn get_transaction(&self, tx_hash: H256) -> Result { + self.signer + .get_transaction(tx_hash) + .await? + .ok_or_else(|| anyhow!("Getting ETH transaction for tx hash {} returned None", tx_hash)) + } } /// On each subscription this will create a new WS connection. diff --git a/engine/src/witness/btc.rs b/engine/src/witness/btc.rs index 38b97a2db5..a79d15e5f0 100644 --- a/engine/src/witness/btc.rs +++ b/engine/src/witness/btc.rs @@ -55,6 +55,7 @@ pub async fn process_egress( tx_out_id: signature, signer_id: epoch.info.1, tx_fee, + tx_metadata: (), } .into(), epoch.index, diff --git a/engine/src/witness/eth/key_manager.rs b/engine/src/witness/eth/key_manager.rs index 8ba42e8a8a..804e761d2e 100644 --- a/engine/src/witness/eth/key_manager.rs +++ b/engine/src/witness/eth/key_manager.rs @@ -1,4 +1,6 @@ -use cf_chains::evm::{EvmCrypto, SchnorrVerificationComponents, TransactionFee}; +use cf_chains::evm::{ + EvmCrypto, EvmTransactionMetadata, SchnorrVerificationComponents, TransactionFee, +}; use cf_primitives::EpochIndex; use ethers::{ prelude::abigen, @@ -60,6 +62,7 @@ impl ChunkedByVaultBuilder { ChainCrypto = EvmCrypto, ChainAccount = H160, TransactionFee = TransactionFee, + TransactionMetadata = EvmTransactionMetadata, >, ProcessCall: Fn(state_chain_runtime::RuntimeCall, EpochIndex) -> ProcessingFut + Send @@ -107,8 +110,9 @@ impl ChunkedByVaultBuilder { sig_data, .. }) => { - let TransactionReceipt { gas_used, effective_gas_price, from, .. } = - eth_rpc.transaction_receipt(event.tx_hash).await; + let TransactionReceipt { + gas_used, effective_gas_price, from, to, .. + } = eth_rpc.transaction_receipt(event.tx_hash).await; let gas_used = gas_used .ok_or_else(|| { @@ -127,6 +131,14 @@ impl ChunkedByVaultBuilder { })? .try_into() .map_err(anyhow::Error::msg)?; + + let transaction = eth_rpc.get_transaction(event.tx_hash).await; + let tx_metadata = EvmTransactionMetadata { + contract: to.expect("To have a contract"), + max_fee_per_gas: transaction.max_fee_per_gas, + max_priority_fee_per_gas: transaction.max_priority_fee_per_gas, + gas_limit: Some(transaction.gas), + }; pallet_cf_broadcast::Call::< _, ::Instance, @@ -137,6 +149,7 @@ impl ChunkedByVaultBuilder { }, signer_id: from, tx_fee: TransactionFee { effective_gas_price, gas_used }, + tx_metadata, } .into() }, diff --git a/state-chain/chains/src/any.rs b/state-chain/chains/src/any.rs index ed191f5f91..d688c38420 100644 --- a/state-chain/chains/src/any.rs +++ b/state-chain/chains/src/any.rs @@ -19,6 +19,7 @@ impl Chain for AnyChain { type DepositChannelState = (); type DepositDetails = (); type Transaction = (); + type TransactionMetadata = (); type ReplayProtectionParams = (); type ReplayProtection = (); } diff --git a/state-chain/chains/src/benchmarking_value.rs b/state-chain/chains/src/benchmarking_value.rs index 7f4baaaa58..0265edc437 100644 --- a/state-chain/chains/src/benchmarking_value.rs +++ b/state-chain/chains/src/benchmarking_value.rs @@ -3,6 +3,7 @@ use cf_primitives::{ chains::assets::{btc, dot, eth}, Asset, }; +use ethereum_types::{H160, U256}; #[cfg(feature = "runtime-benchmarks")] use crate::address::EncodedAddress; @@ -10,6 +11,7 @@ use crate::address::EncodedAddress; use crate::address::ForeignChainAddress; #[cfg(feature = "runtime-benchmarks")] use crate::evm::EvmFetchId; +use crate::evm::EvmTransactionMetadata; /// Ensure type specifies a value to be used for benchmarking purposes. pub trait BenchmarkValue { @@ -120,6 +122,19 @@ impl BenchmarkValueExtended for () { Default::default() } } + +#[cfg(feature = "runtime-benchmarks")] +impl BenchmarkValue for EvmTransactionMetadata { + fn benchmark_value() -> Self { + Self { + contract: H160::zero(), + max_fee_per_gas: Some(U256::zero()), + max_priority_fee_per_gas: Some(U256::zero()), + gas_limit: None, + } + } +} + impl_default_benchmark_value!(()); impl_default_benchmark_value!(u32); impl_default_benchmark_value!(u64); diff --git a/state-chain/chains/src/btc.rs b/state-chain/chains/src/btc.rs index 815d441c54..ff5e12583d 100644 --- a/state-chain/chains/src/btc.rs +++ b/state-chain/chains/src/btc.rs @@ -187,6 +187,7 @@ impl Chain for Bitcoin { type DepositChannelState = DepositAddress; type DepositDetails = UtxoId; type Transaction = BitcoinTransactionData; + type TransactionMetadata = (); // There is no need for replay protection on Bitcoin since it is a UTXO chain. type ReplayProtectionParams = (); type ReplayProtection = (); diff --git a/state-chain/chains/src/dot.rs b/state-chain/chains/src/dot.rs index 7ddedda44d..29908783d8 100644 --- a/state-chain/chains/src/dot.rs +++ b/state-chain/chains/src/dot.rs @@ -267,6 +267,7 @@ impl Chain for Polkadot { type DepositChannelState = PolkadotChannelState; type DepositDetails = (); type Transaction = PolkadotTransactionData; + type TransactionMetadata = (); type ReplayProtectionParams = ResetProxyAccountNonce; type ReplayProtection = PolkadotReplayProtection; } diff --git a/state-chain/chains/src/eth.rs b/state-chain/chains/src/eth.rs index 163e52edce..c46a228feb 100644 --- a/state-chain/chains/src/eth.rs +++ b/state-chain/chains/src/eth.rs @@ -6,7 +6,7 @@ pub mod benchmarking; pub mod deposit_address; use crate::{ - evm::{DeploymentStatus, EvmFetchId, Transaction}, + evm::{DeploymentStatus, EvmFetchId, EvmTransactionMetadata, Transaction}, *, }; use cf_primitives::chains::assets; @@ -43,6 +43,7 @@ impl Chain for Ethereum { type DepositChannelState = DeploymentStatus; type DepositDetails = (); type Transaction = Transaction; + type TransactionMetadata = EvmTransactionMetadata; type ReplayProtectionParams = Self::ChainAccount; type ReplayProtection = EvmReplayProtection; } diff --git a/state-chain/chains/src/evm.rs b/state-chain/chains/src/evm.rs index 744213b406..17a879f7da 100644 --- a/state-chain/chains/src/evm.rs +++ b/state-chain/chains/src/evm.rs @@ -351,6 +351,40 @@ pub struct Transaction { pub data: Vec, } +#[derive( + Encode, Decode, TypeInfo, Clone, RuntimeDebug, Default, PartialEq, Eq, Serialize, Deserialize, +)] +pub struct EvmTransactionMetadata { + pub max_fee_per_gas: Option, + pub max_priority_fee_per_gas: Option, + pub contract: Address, + pub gas_limit: Option, +} + +impl> TransactionMetadata for EvmTransactionMetadata { + fn extract_metadata(transaction: &::Transaction) -> Self { + Self { + contract: transaction.contract, + max_fee_per_gas: transaction.max_fee_per_gas, + max_priority_fee_per_gas: transaction.max_priority_fee_per_gas, + gas_limit: transaction.gas_limit, + } + } + + fn verify_metadata(&self, expected_metadata: &Self) -> bool { + macro_rules! check_optional { + ($field:ident) => { + (expected_metadata.$field.is_none() || expected_metadata.$field == self.$field) + }; + } + + self.contract == expected_metadata.contract && + check_optional!(max_fee_per_gas) && + check_optional!(max_priority_fee_per_gas) && + check_optional!(gas_limit) + } +} + impl Transaction { fn check_contract( &self, @@ -700,3 +734,49 @@ mod verification_tests { ); } } + +#[test] +fn metadata_verification() { + let submitted_metadata = EvmTransactionMetadata { + max_fee_per_gas: None, + max_priority_fee_per_gas: Some(U256::one()), + contract: Default::default(), + gas_limit: None, + }; + + // Exact match. + assert!(>::verify_metadata( + &submitted_metadata, + &submitted_metadata + )); + + // If we don't expect a value, it's ok if it's set. + assert!(>::verify_metadata( + &submitted_metadata, + &EvmTransactionMetadata { max_priority_fee_per_gas: None, ..submitted_metadata } + )); + + // If we expect something else it fails. + assert!(!>::verify_metadata( + &submitted_metadata, + &EvmTransactionMetadata { + max_priority_fee_per_gas: Some(U256::zero()), + ..submitted_metadata + } + )); + + // If we witness `None` instead of `Some`, it fails. + assert!(!>::verify_metadata( + &submitted_metadata, + &EvmTransactionMetadata { max_fee_per_gas: Some(U256::zero()), ..submitted_metadata } + )); + + // Wrong contract address. + assert!(!>::verify_metadata( + &submitted_metadata, + &EvmTransactionMetadata { + contract: ethereum_types::H160::repeat_byte(1u8), + ..submitted_metadata + } + )); +} diff --git a/state-chain/chains/src/lib.rs b/state-chain/chains/src/lib.rs index 9b27f777f4..5d927b9a50 100644 --- a/state-chain/chains/src/lib.rs +++ b/state-chain/chains/src/lib.rs @@ -129,6 +129,12 @@ pub trait Chain: Member + Parameter { type DepositDetails: Member + Parameter + BenchmarkValue; type Transaction: Member + Parameter + BenchmarkValue + FeeRefundCalculator; + + type TransactionMetadata: Member + + Parameter + + TransactionMetadata + + BenchmarkValue + + Default; /// Passed in to construct the replay protection. type ReplayProtectionParams: Member + Parameter; type ReplayProtection: Member + Parameter; @@ -254,6 +260,20 @@ where } } +pub trait TransactionMetadata { + fn extract_metadata(transaction: &C::Transaction) -> Self; + fn verify_metadata(&self, expected_metadata: &Self) -> bool; +} + +impl TransactionMetadata for () { + fn extract_metadata(_transaction: &C::Transaction) -> Self { + Default::default() + } + fn verify_metadata(&self, _expected_metadata: &Self) -> bool { + true + } +} + /// Contains all the parameters required to fetch incoming transactions on an external chain. #[derive(RuntimeDebug, Copy, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] pub struct FetchAssetParams { diff --git a/state-chain/chains/src/mocks.rs b/state-chain/chains/src/mocks.rs index 3115637963..7438b3092b 100644 --- a/state-chain/chains/src/mocks.rs +++ b/state-chain/chains/src/mocks.rs @@ -18,6 +18,7 @@ thread_local! { static MOCK_KEY_HANDOVER_IS_REQUIRED: RefCell = RefCell::new(true); static MOCK_OPTIMISTIC_ACTIVATION: RefCell = RefCell::new(false); static MOCK_SIGN_WITH_SPECIFIC_KEY: RefCell = RefCell::new(false); + static MOCK_VALID_METADATA: RefCell = RefCell::new(true); } pub struct MockKeyHandoverIsRequired; @@ -62,6 +63,31 @@ impl Get for MockFixedKeySigningRequests { } } +#[derive(Debug, Clone, Default, PartialEq, Eq, Encode, Decode, TypeInfo)] +pub struct MockEthereumTransactionMetadata; + +impl TransactionMetadata for MockEthereumTransactionMetadata { + fn extract_metadata(_transaction: &::Transaction) -> Self { + Default::default() + } + + fn verify_metadata(&self, _expected_metadata: &Self) -> bool { + MOCK_VALID_METADATA.with(|cell| *cell.borrow()) + } +} + +impl BenchmarkValue for MockEthereumTransactionMetadata { + fn benchmark_value() -> Self { + Default::default() + } +} + +impl MockEthereumTransactionMetadata { + pub fn set_validity(valid: bool) { + MOCK_VALID_METADATA.with(|cell| *cell.borrow_mut() = valid); + } +} + // Chain implementation used for testing. impl Chain for MockEthereum { const NAME: &'static str = "MockEthereum"; @@ -78,6 +104,7 @@ impl Chain for MockEthereum { type DepositChannelState = MockLifecycleHooks; type DepositDetails = [u8; 4]; type Transaction = MockTransaction; + type TransactionMetadata = MockEthereumTransactionMetadata; type ReplayProtectionParams = (); type ReplayProtection = EvmReplayProtection; } @@ -255,6 +282,9 @@ pub const MOCK_TRANSACTION_OUT_ID: [u8; 4] = [0xbc; 4]; pub const ETH_TX_FEE: ::TransactionFee = TransactionFee { effective_gas_price: 200, gas_used: 100 }; +pub const MOCK_TX_METADATA: ::TransactionMetadata = + MockEthereumTransactionMetadata; + #[derive(Encode, Decode, TypeInfo, CloneNoBound, DebugNoBound, PartialEqNoBound, EqNoBound)] #[scale_info(skip_type_params(C))] pub struct MockApiCall { diff --git a/state-chain/chains/src/none.rs b/state-chain/chains/src/none.rs index c957a9affb..119a8b2ec8 100644 --- a/state-chain/chains/src/none.rs +++ b/state-chain/chains/src/none.rs @@ -19,6 +19,7 @@ impl Chain for NoneChain { type DepositChannelState = (); type DepositDetails = (); type Transaction = (); + type TransactionMetadata = (); type ReplayProtectionParams = (); type ReplayProtection = (); } diff --git a/state-chain/pallets/cf-broadcast/src/benchmarking.rs b/state-chain/pallets/cf-broadcast/src/benchmarking.rs index e9564119a3..42584f98c8 100644 --- a/state-chain/pallets/cf-broadcast/src/benchmarking.rs +++ b/state-chain/pallets/cf-broadcast/src/benchmarking.rs @@ -148,6 +148,7 @@ benchmarks_instance_pallet! { tx_out_id: TransactionOutIdFor::::benchmark_value(), signer_id, tx_fee: TransactionFeeFor::::benchmark_value(), + tx_metadata: TransactionMetadataFor::::benchmark_value(), }; let valid_key = AggKeyFor::::benchmark_value(); T::KeyProvider::set_key(valid_key); diff --git a/state-chain/pallets/cf-broadcast/src/lib.rs b/state-chain/pallets/cf-broadcast/src/lib.rs index eef21ca59b..7dab27ce22 100644 --- a/state-chain/pallets/cf-broadcast/src/lib.rs +++ b/state-chain/pallets/cf-broadcast/src/lib.rs @@ -14,7 +14,9 @@ pub use weights::WeightInfo; impl_pallet_safe_mode!(PalletSafeMode; retry_enabled); -use cf_chains::{ApiCall, Chain, ChainCrypto, FeeRefundCalculator, TransactionBuilder}; +use cf_chains::{ + ApiCall, Chain, ChainCrypto, FeeRefundCalculator, TransactionBuilder, TransactionMetadata as _, +}; use cf_traits::{ offence_reporting::OffenceReporter, BroadcastNomination, Broadcaster, Chainflip, EpochInfo, EpochKey, OnBroadcastReady, ThresholdSigner, @@ -94,6 +96,10 @@ pub mod pallet { pub type PayloadFor = <<>::TargetChain as Chain>::ChainCrypto as ChainCrypto>::Payload; + /// Type alias for the instance's configured transaction Metadata. + pub type TransactionMetadataFor = + <>::TargetChain as Chain>::TransactionMetadata; + pub type ChainBlockNumberFor = <>::TargetChain as cf_chains::Chain>::ChainBlockNumber; @@ -275,6 +281,11 @@ pub mod pallet { OptionQuery, >; + /// Stores metadata related to a transaction. + #[pallet::storage] + pub type TransactionMetadata, I: 'static = ()> = + StorageMap<_, Twox64Concat, BroadcastId, TransactionMetadataFor>; + /// Tracks how much a signer id is owed for paying transaction fees. #[pallet::storage] pub type TransactionFeeDeficit, I: 'static = ()> = @@ -307,6 +318,13 @@ pub mod pallet { /// A signature accepted event on the target chain has been witnessed and the callback was /// executed. BroadcastCallbackExecuted { broadcast_id: BroadcastId, result: DispatchResult }, + /// The fee paid for broadcasting a transaction has been recorded. + TransactionFeeDeficitRecorded { + beneficiary: SignerIdFor, + amount: ChainAmountFor, + }, + /// The fee paid for broadcasting a transaction has been refused. + TransactionFeeDeficitRefused { beneficiary: SignerIdFor }, } #[pallet::error] @@ -526,6 +544,7 @@ pub mod pallet { tx_out_id: TransactionOutIdFor, signer_id: SignerIdFor, tx_fee: TransactionFeeFor, + tx_metadata: TransactionMetadataFor, ) -> DispatchResultWithPostInfo { T::EnsureWitnessed::ensure_origin(origin.clone())?; @@ -533,18 +552,40 @@ pub mod pallet { TransactionOutIdToBroadcastId::::take(&tx_out_id) .ok_or(Error::::InvalidPayload)?; - let to_refund = AwaitingBroadcast::::get(BroadcastAttemptId { - broadcast_id, - attempt_count: BroadcastAttemptCount::::get(broadcast_id), - }) - .ok_or(Error::::InvalidBroadcastAttemptId)? - .broadcast_attempt - .transaction_payload - .return_fee_refund(tx_fee); - - TransactionFeeDeficit::::mutate(signer_id, |fee_deficit| { - *fee_deficit = fee_deficit.saturating_add(to_refund); - }); + if let Some(expected_tx_metadata) = TransactionMetadata::::take(broadcast_id) { + if tx_metadata.verify_metadata(&expected_tx_metadata) { + let to_refund = AwaitingBroadcast::::get(BroadcastAttemptId { + broadcast_id, + attempt_count: BroadcastAttemptCount::::get(broadcast_id), + }) + .ok_or(Error::::InvalidBroadcastAttemptId)? + .broadcast_attempt + .transaction_payload + .return_fee_refund(tx_fee); + + TransactionFeeDeficit::::mutate(signer_id.clone(), |fee_deficit| { + *fee_deficit = fee_deficit.saturating_add(to_refund); + }); + + Self::deposit_event(Event::::TransactionFeeDeficitRecorded { + beneficiary: signer_id, + amount: to_refund, + }); + } else { + Self::deposit_event(Event::::TransactionFeeDeficitRefused { + beneficiary: signer_id, + }); + log::warn!( + "Transaction metadata verification failed for broadcast {}. Deficit will not be recorded.", + broadcast_id + ); + } + } else { + log::error!( + "Transaction metadata not found for broadcast {}. Deficit will be ignored.", + broadcast_id + ); + } if let Some(callback) = RequestCallbacks::::get(broadcast_id) { Self::deposit_event(Event::::BroadcastCallbackExecuted { @@ -613,6 +654,7 @@ impl, I: 'static> Pallet { AwaitingBroadcast::::remove(BroadcastAttemptId { broadcast_id, attempt_count }); } + TransactionMetadata::::remove(broadcast_id); RequestCallbacks::::remove(broadcast_id); ThresholdSignatureData::::remove(broadcast_id); } @@ -755,6 +797,12 @@ impl, I: 'static> Pallet { fn start_broadcast_attempt(mut broadcast_attempt: BroadcastAttempt) { T::TransactionBuilder::refresh_unsigned_data(&mut broadcast_attempt.transaction_payload); + TransactionMetadata::::insert( + broadcast_attempt.broadcast_attempt_id.broadcast_id, + <::TransactionMetadata>::extract_metadata( + &broadcast_attempt.transaction_payload, + ), + ); let seed = (broadcast_attempt.broadcast_attempt_id, broadcast_attempt.transaction_payload.clone()) diff --git a/state-chain/pallets/cf-broadcast/src/mock.rs b/state-chain/pallets/cf-broadcast/src/mock.rs index a56c03096c..25d6383de8 100644 --- a/state-chain/pallets/cf-broadcast/src/mock.rs +++ b/state-chain/pallets/cf-broadcast/src/mock.rs @@ -86,6 +86,7 @@ pub const INVALID_AGG_KEY: MockAggKey = MockAggKey([1, 1, 1, 1]); thread_local! { pub static SIGNATURE_REQUESTS: RefCell::ChainCrypto as ChainCrypto>::Payload>> = RefCell::new(vec![]); pub static CALLBACK_CALLED: RefCell = RefCell::new(false); + pub static VALID_METADATA: RefCell = RefCell::new(true); } pub type EthMockThresholdSigner = MockThresholdSigner; diff --git a/state-chain/pallets/cf-broadcast/src/tests.rs b/state-chain/pallets/cf-broadcast/src/tests.rs index 5a1f8772da..99a24a539f 100644 --- a/state-chain/pallets/cf-broadcast/src/tests.rs +++ b/state-chain/pallets/cf-broadcast/src/tests.rs @@ -4,13 +4,14 @@ use crate::{ mock::*, AwaitingBroadcast, BroadcastAttemptCount, BroadcastAttemptId, BroadcastId, BroadcastRetryQueue, Error, Event as BroadcastEvent, FailedBroadcasters, Instance1, PalletOffence, RequestCallbacks, ThresholdSignatureData, Timeouts, TransactionFeeDeficit, - TransactionOutIdToBroadcastId, WeightInfo, + TransactionMetadata, TransactionOutIdToBroadcastId, WeightInfo, }; use cf_chains::{ evm::SchnorrVerificationComponents, mocks::{ - MockApiCall, MockEthereum, MockEthereumChainCrypto, MockThresholdSignature, - MockTransaction, MockTransactionBuilder, ETH_TX_FEE, + MockApiCall, MockEthereum, MockEthereumChainCrypto, MockEthereumTransactionMetadata, + MockThresholdSignature, MockTransaction, MockTransactionBuilder, ETH_TX_FEE, + MOCK_TX_METADATA, }, ChainCrypto, FeeRefundCalculator, }; @@ -117,6 +118,7 @@ fn assert_broadcast_storage_cleaned_up(broadcast_id: BroadcastId) { assert!(FailedBroadcasters::::get(broadcast_id).is_none()); assert_eq!(BroadcastAttemptCount::::get(broadcast_id), 0); assert!(ThresholdSignatureData::::get(broadcast_id).is_none()); + assert!(TransactionMetadata::::get(broadcast_id).is_none()); } fn start_mock_broadcast_tx_out_id( @@ -136,7 +138,6 @@ fn start_mock_broadcast() -> BroadcastAttemptId { start_mock_broadcast_tx_out_id(Default::default()) } -// The happy path :) #[test] fn transaction_succeeded_results_in_refund_for_signer() { new_test_ext().execute_with(|| { @@ -153,6 +154,7 @@ fn transaction_succeeded_results_in_refund_for_signer() { MOCK_TRANSACTION_OUT_ID, nominee, ETH_TX_FEE, + MOCK_TX_METADATA, )); let expected_refund = tx_sig_request @@ -164,6 +166,14 @@ fn transaction_succeeded_results_in_refund_for_signer() { assert_eq!(TransactionFeeDeficit::::get(nominee), expected_refund); + assert_eq!( + System::events().get(1).expect("an event").event, + RuntimeEvent::Broadcaster(crate::Event::TransactionFeeDeficitRecorded { + beneficiary: Default::default(), + amount: expected_refund + }) + ); + assert_broadcast_storage_cleaned_up(broadcast_attempt_id.broadcast_id); }); } @@ -285,6 +295,7 @@ fn test_sigdata_with_no_match_is_noop() { MOCK_TRANSACTION_OUT_ID, Default::default(), ETH_TX_FEE, + MOCK_TX_METADATA, ), Error::::InvalidPayload ); @@ -315,6 +326,7 @@ fn transaction_succeeded_after_timeout_reports_failed_nodes() { MOCK_TRANSACTION_OUT_ID, Default::default(), ETH_TX_FEE, + MOCK_TX_METADATA, )); MockOffenceReporter::assert_reported( @@ -495,6 +507,7 @@ fn threshold_sign_and_broadcast_with_callback() { MOCK_TRANSACTION_OUT_ID, Default::default(), ETH_TX_FEE, + MOCK_TX_METADATA, )); assert!(RequestCallbacks::::get(broadcast_id).is_none()); let mut events = System::events(); @@ -540,3 +553,29 @@ fn ensure_retries_are_skipped_during_safe_mode() { assert_eq!(BroadcastRetryQueue::::get().len(), 2); }); } + +#[test] +fn transaction_succeeded_results_in_refund_refuse_for_signer() { + new_test_ext().execute_with(|| { + MockEthereumTransactionMetadata::set_validity(false); + let _ = start_mock_broadcast_tx_out_id(MOCK_TRANSACTION_OUT_ID); + let nominee = MockNominator::get_last_nominee().unwrap(); + + assert_eq!(TransactionFeeDeficit::::get(nominee), 0); + + assert_ok!(Broadcaster::transaction_succeeded( + RuntimeOrigin::root(), + MOCK_TRANSACTION_OUT_ID, + nominee, + ETH_TX_FEE, + MOCK_TX_METADATA, + )); + + assert_eq!( + System::events().get(1).expect("an event").event, + RuntimeEvent::Broadcaster(crate::Event::TransactionFeeDeficitRefused { + beneficiary: Default::default(), + }) + ); + }); +}