From 4f3741ee3a2efc9d87466859f2a7fb1a503a4d32 Mon Sep 17 00:00:00 2001 From: Maxim Shishmarev Date: Tue, 19 Nov 2024 17:39:07 +1100 Subject: [PATCH] feat: boost support for vault swaps --- engine/src/witness/arb.rs | 47 +- engine/src/witness/btc/deposits.rs | 24 +- engine/src/witness/btc/vault_swaps.rs | 42 +- engine/src/witness/eth.rs | 50 +- engine/src/witness/evm/vault.rs | 27 +- .../cf-integration-tests/src/solana.rs | 80 +- .../cf-integration-tests/src/swapping.rs | 81 +- state-chain/chains/src/lib.rs | 8 + .../cf-ingress-egress/src/benchmarking.rs | 37 +- .../pallets/cf-ingress-egress/src/lib.rs | 868 ++++++++++++------ .../pallets/cf-ingress-egress/src/tests.rs | 135 ++- .../cf-ingress-egress/src/tests/boost.rs | 173 +++- state-chain/runtime/src/lib.rs | 26 +- 13 files changed, 1054 insertions(+), 544 deletions(-) diff --git a/engine/src/witness/arb.rs b/engine/src/witness/arb.rs index e1df799ce1..d0a53fb12f 100644 --- a/engine/src/witness/arb.rs +++ b/engine/src/witness/arb.rs @@ -8,11 +8,12 @@ use cf_chains::{ Arbitrum, CcmDepositMetadata, }; use cf_primitives::{ - chains::assets::arb::Asset as ArbAsset, Asset, AssetAmount, Beneficiary, EpochIndex, + chains::assets::arb::Asset as ArbAsset, Asset, AssetAmount, Beneficiary, ChannelId, EpochIndex, }; use cf_utilities::task_scope::Scope; use futures_core::Future; use itertools::Itertools; +use pallet_cf_ingress_egress::VaultDepositWitness; use sp_core::H160; use crate::{ @@ -185,7 +186,10 @@ impl super::evm::vault::IngressCallBuilder for ArbCallBuilder { type Chain = Arbitrum; fn vault_swap_request( + block_height: u64, source_asset: Asset, + deposit_address: cf_chains::eth::Address, + channel_id: ChannelId, deposit_amount: AssetAmount, destination_asset: Asset, destination_address: EncodedAddress, @@ -195,24 +199,29 @@ impl super::evm::vault::IngressCallBuilder for ArbCallBuilder { ) -> state_chain_runtime::RuntimeCall { state_chain_runtime::RuntimeCall::ArbitrumIngressEgress( pallet_cf_ingress_egress::Call::vault_swap_request { - input_asset: source_asset.try_into().expect("invalid asset for chain"), - output_asset: destination_asset, - deposit_amount, - destination_address, - deposit_metadata, - tx_id, - deposit_details: Box::new(DepositDetails { tx_hashes: Some(vec![tx_id]) }), - broker_fee: vault_swap_parameters.broker_fee, - affiliate_fees: vault_swap_parameters - .affiliate_fees - .into_iter() - .map(|entry| Beneficiary { account: entry.affiliate, bps: entry.fee.into() }) - .collect_vec() - .try_into() - .expect("runtime supports at least as many affiliates as we allow in cf_parameters encoding"), - boost_fee: vault_swap_parameters.boost_fee.into(), - dca_params: vault_swap_parameters.dca_params, - refund_params: Box::new(vault_swap_parameters.refund_params), + block_height, + deposits: vec![VaultDepositWitness { + input_asset: source_asset.try_into().expect("invalid asset for chain"), + output_asset: destination_asset, + deposit_amount, + destination_address, + deposit_metadata, + tx_id, + deposit_details: DepositDetails { tx_hashes: Some(vec![tx_id]) }, + broker_fee: vault_swap_parameters.broker_fee, + affiliate_fees: vault_swap_parameters + .affiliate_fees + .into_iter() + .map(|entry| Beneficiary { account: entry.affiliate, bps: entry.fee.into() }) + .collect_vec() + .try_into() + .expect("runtime supports at least as many affiliates as we allow in cf_parameters encoding"), + boost_fee: vault_swap_parameters.boost_fee.into(), + dca_params: vault_swap_parameters.dca_params, + refund_params: vault_swap_parameters.refund_params, + channel_id, + deposit_address, + }], }, ) } diff --git a/engine/src/witness/btc/deposits.rs b/engine/src/witness/btc/deposits.rs index cc3f556003..d5394a3d88 100644 --- a/engine/src/witness/btc/deposits.rs +++ b/engine/src/witness/btc/deposits.rs @@ -6,8 +6,12 @@ use itertools::Itertools; use pallet_cf_ingress_egress::{DepositChannelDetails, DepositWitness}; use state_chain_runtime::BitcoinInstance; -use super::super::common::chunked_chain_source::chunked_by_vault::{ - builder::ChunkedByVaultBuilder, private_deposit_channels::BrokerPrivateChannels, ChunkedByVault, +use super::{ + super::common::chunked_chain_source::chunked_by_vault::{ + builder::ChunkedByVaultBuilder, private_deposit_channels::BrokerPrivateChannels, + ChunkedByVault, + }, + vault_swaps::BtcIngressEgressCall, }; use crate::{ btc::rpc::VerboseTransaction, @@ -71,6 +75,7 @@ impl ChunkedByVaultBuilder { private_channels.clone().into_iter().map(move |(broker_id, channel_id)| { ( broker_id, + channel_id, DepositAddress::new( key, channel_id.try_into().expect("BTC channel id must fit in u32"), @@ -80,14 +85,23 @@ impl ChunkedByVaultBuilder { }) }; - for (broker_id, vault_address) in vault_addresses { + for (broker_id, channel_id, vault_address) in vault_addresses { for tx in &txs { - if let Some(call) = super::vault_swaps::try_extract_vault_swap_call( + if let Some(deposit) = super::vault_swaps::try_extract_vault_swap_witness( tx, &vault_address, + channel_id, &broker_id, ) { - process_call(call.into(), epoch.index).await; + process_call( + BtcIngressEgressCall::vault_swap_request { + block_height: header.index, + deposits: vec![deposit], + } + .into(), + epoch.index, + ) + .await; } } } diff --git a/engine/src/witness/btc/vault_swaps.rs b/engine/src/witness/btc/vault_swaps.rs index 8465f270ed..d51a2f94a7 100644 --- a/engine/src/witness/btc/vault_swaps.rs +++ b/engine/src/witness/btc/vault_swaps.rs @@ -8,7 +8,7 @@ use cf_chains::{ }, ChannelRefundParameters, ForeignChainAddress, }; -use cf_primitives::{AccountId, Beneficiary, DcaParameters}; +use cf_primitives::{AccountId, Beneficiary, ChannelId, DcaParameters}; use cf_utilities::SliceToArray; use codec::Decode; use itertools::Itertools; @@ -77,14 +77,18 @@ fn script_buf_to_script_pubkey(script: &ScriptBuf) -> Option { Some(pubkey) } -type BtcIngressEgressCall = +pub(super) type BtcIngressEgressCall = pallet_cf_ingress_egress::Call; -pub fn try_extract_vault_swap_call( +type VaultDepositWitness = + pallet_cf_ingress_egress::VaultDepositWitness; + +pub fn try_extract_vault_swap_witness( tx: &VerboseTransaction, vault_address: &DepositAddress, + channel_id: ChannelId, broker_id: &AccountId, -) -> Option { +) -> Option { // A correctly constructed transaction carrying CF swap parameters must have at least 3 outputs: let [utxo_to_vault, nulldata_utxo, change_utxo, ..] = &tx.vout[..] else { return None; @@ -130,18 +134,18 @@ pub fn try_extract_vault_swap_call( let tx_id: [u8; 32] = tx.txid.to_byte_array(); - Some(BtcIngressEgressCall::vault_swap_request { + Some(VaultDepositWitness { input_asset: NATIVE_ASSET, output_asset: data.output_asset, deposit_amount, destination_address: data.output_address, tx_id: H256::from(tx_id), - deposit_details: Box::new(Utxo { + deposit_details: Utxo { // we require the deposit to be the first UTXO id: UtxoId { tx_id: tx_id.into(), vout: 0 }, amount: deposit_amount, deposit_address: vault_address.clone(), - }), + }, deposit_metadata: None, // No ccm for BTC (yet?) broker_fee: Beneficiary { account: broker_id.clone(), @@ -155,17 +159,19 @@ pub fn try_extract_vault_swap_call( .collect_vec() .try_into() .expect("runtime supports at least as many affiliates as we allow in UTXO encoding"), - refund_params: Box::new(ChannelRefundParameters { + refund_params: ChannelRefundParameters { retry_duration: data.parameters.retry_duration.into(), refund_address: ForeignChainAddress::Btc(refund_address), min_price, - }), + }, dca_params: Some(DcaParameters { number_of_chunks: data.parameters.number_of_chunks.into(), chunk_interval: data.parameters.chunk_interval.into(), }), // This is only to be checked in the pre-witnessed version boost_fee: data.parameters.boost_fee.into(), + channel_id, + deposit_address: vault_address.script_pubkey(), }) } @@ -288,38 +294,42 @@ mod tests { None, ); + const CHANNEL_ID: ChannelId = 7; + assert_eq!( - try_extract_vault_swap_call(&tx, &vault_deposit_address, &BROKER), - Some(BtcIngressEgressCall::vault_swap_request { + try_extract_vault_swap_witness(&tx, &vault_deposit_address, CHANNEL_ID, &BROKER), + Some(VaultDepositWitness { input_asset: NATIVE_ASSET, output_asset: MOCK_SWAP_PARAMS.output_asset, deposit_amount: DEPOSIT_AMOUNT, destination_address: MOCK_SWAP_PARAMS.output_address.clone(), tx_id: tx.txid.to_byte_array().into(), - deposit_details: Box::new(Utxo { + deposit_details: Utxo { id: UtxoId { tx_id: tx.txid.to_byte_array().into(), vout: 0 }, amount: DEPOSIT_AMOUNT, - deposit_address: vault_deposit_address, - }), + deposit_address: vault_deposit_address.clone(), + }, broker_fee: Beneficiary { account: BROKER, bps: MOCK_SWAP_PARAMS.parameters.broker_fee.into() }, affiliate_fees: bounded_vec![MOCK_SWAP_PARAMS.parameters.affiliates[0].into()], deposit_metadata: None, - refund_params: Box::new(ChannelRefundParameters { + refund_params: ChannelRefundParameters { retry_duration: MOCK_SWAP_PARAMS.parameters.retry_duration.into(), refund_address: ForeignChainAddress::Btc(refund_pubkey), min_price: sqrt_price_to_price(bounded_sqrt_price( MOCK_SWAP_PARAMS.parameters.min_output_amount.into(), DEPOSIT_AMOUNT.into(), )), - }), + }, dca_params: Some(DcaParameters { number_of_chunks: MOCK_SWAP_PARAMS.parameters.number_of_chunks.into(), chunk_interval: MOCK_SWAP_PARAMS.parameters.chunk_interval.into(), }), boost_fee: MOCK_SWAP_PARAMS.parameters.boost_fee.into(), + deposit_address: vault_deposit_address.script_pubkey(), + channel_id: CHANNEL_ID, }) ); } diff --git a/engine/src/witness/eth.rs b/engine/src/witness/eth.rs index ade7ed7e89..0ee789688a 100644 --- a/engine/src/witness/eth.rs +++ b/engine/src/witness/eth.rs @@ -8,10 +8,13 @@ use cf_chains::{ evm::{DepositDetails, H256}, CcmDepositMetadata, Ethereum, }; -use cf_primitives::{chains::assets::eth::Asset as EthAsset, Asset, AssetAmount, EpochIndex}; +use cf_primitives::{ + chains::assets::eth::Asset as EthAsset, Asset, AssetAmount, ChannelId, EpochIndex, +}; use cf_utilities::task_scope::Scope; use futures_core::Future; use itertools::Itertools; +use pallet_cf_ingress_egress::VaultDepositWitness; use sp_core::H160; use crate::{ @@ -232,7 +235,10 @@ impl super::evm::vault::IngressCallBuilder for EthCallBuilder { type Chain = Ethereum; fn vault_swap_request( + block_height: u64, source_asset: Asset, + deposit_address: cf_chains::eth::Address, + channel_id: ChannelId, deposit_amount: AssetAmount, destination_asset: Asset, destination_address: EncodedAddress, @@ -242,24 +248,30 @@ impl super::evm::vault::IngressCallBuilder for EthCallBuilder { ) -> state_chain_runtime::RuntimeCall { state_chain_runtime::RuntimeCall::EthereumIngressEgress( pallet_cf_ingress_egress::Call::vault_swap_request { - input_asset: source_asset.try_into().expect("invalid asset for chain"), - output_asset: destination_asset, - deposit_amount, - destination_address, - deposit_metadata, - tx_id, - deposit_details: Box::new(DepositDetails { tx_hashes: Some(vec![tx_id]) }), - broker_fee: vault_swap_parameters.broker_fee, - affiliate_fees: vault_swap_parameters - .affiliate_fees - .into_iter() - .map(Into::into) - .collect_vec() - .try_into() - .expect("runtime supports at least as many affiliates as we allow in cf_parameters encoding"), - boost_fee: vault_swap_parameters.boost_fee.into(), - dca_params: vault_swap_parameters.dca_params, - refund_params: Box::new(vault_swap_parameters.refund_params), + block_height, + deposits: vec![ VaultDepositWitness { + input_asset: source_asset.try_into().expect("invalid asset for chain"), + output_asset: destination_asset, + deposit_amount, + destination_address, + deposit_metadata, + tx_id, + deposit_details: DepositDetails { tx_hashes: Some(vec![tx_id]) }, + broker_fee: vault_swap_parameters.broker_fee, + affiliate_fees: vault_swap_parameters + .affiliate_fees + .into_iter() + .map(Into::into) + .collect_vec() + .try_into() + .expect("runtime supports at least as many affiliates as we allow in cf_parameters encoding"), + boost_fee: vault_swap_parameters.boost_fee.into(), + dca_params: vault_swap_parameters.dca_params, + refund_params: vault_swap_parameters.refund_params, + channel_id, + deposit_address, + } + ], }, ) } diff --git a/engine/src/witness/evm/vault.rs b/engine/src/witness/evm/vault.rs index de1fb73040..99e4f0dc96 100644 --- a/engine/src/witness/evm/vault.rs +++ b/engine/src/witness/evm/vault.rs @@ -21,16 +21,18 @@ use cf_chains::{ evm::DepositDetails, CcmChannelMetadata, CcmDepositMetadata, Chain, }; -use cf_primitives::{Asset, AssetAmount, EpochIndex, ForeignChain}; +use cf_primitives::{Asset, AssetAmount, ChannelId, EpochIndex, ForeignChain}; use ethers::prelude::*; use state_chain_runtime::{EthereumInstance, Runtime, RuntimeCall}; abigen!(Vault, "$CF_ETH_CONTRACT_ABI_ROOT/$CF_ETH_CONTRACT_ABI_TAG/IVault.json"); pub fn call_from_event< - C: cf_chains::Chain, + C: cf_chains::Chain, CallBuilder: IngressCallBuilder, >( + block_height: u64, + contract_address: EthereumAddress, event: Event, // can be different for different EVM chains native_asset: Asset, @@ -56,6 +58,10 @@ where }) } + // The deposit address and channel id are always the same (unlike BTC vault swaps): + let deposit_address = contract_address; + let channel_id = 0; + Ok(match event.event_parameters { VaultEvents::SwapNativeFilter(SwapNativeFilter { dst_chain, @@ -71,7 +77,10 @@ where }) = VersionedCfParameters::decode(&mut &cf_parameters[..])?; Some(CallBuilder::vault_swap_request( + block_height, native_asset, + deposit_address, + channel_id, try_into_primitive(amount)?, try_into_primitive(dst_token)?, try_into_encoded_address(try_into_primitive(dst_chain)?, dst_address.to_vec())?, @@ -95,9 +104,12 @@ where }) = VersionedCfParameters::decode(&mut &cf_parameters[..])?; Some(CallBuilder::vault_swap_request( + block_height, *(supported_assets .get(&src_token) .ok_or(anyhow!("Source token {src_token:?} not found"))?), + deposit_address, + channel_id, try_into_primitive(amount)?, try_into_primitive(dst_token)?, try_into_encoded_address(try_into_primitive(dst_chain)?, dst_address.to_vec())?, @@ -122,7 +134,10 @@ where }) = VersionedCcmCfParameters::decode(&mut &cf_parameters[..])?; Some(CallBuilder::vault_swap_request( + block_height, native_asset, + deposit_address, + channel_id, try_into_primitive(amount)?, try_into_primitive(dst_token)?, try_into_encoded_address(try_into_primitive(dst_chain)?, dst_address.to_vec())?, @@ -163,9 +178,12 @@ where }) = VersionedCcmCfParameters::decode(&mut &cf_parameters[..])?; Some(CallBuilder::vault_swap_request( + block_height, *(supported_assets .get(&src_token) .ok_or(anyhow!("Source token {src_token:?} not found"))?), + deposit_address, + channel_id, try_into_primitive(amount)?, try_into_primitive(dst_token)?, try_into_encoded_address(try_into_primitive(dst_chain)?, dst_address.to_vec())?, @@ -224,7 +242,10 @@ pub trait IngressCallBuilder { type Chain: cf_chains::Chain; fn vault_swap_request( + block_height: ::ChainBlockNumber, source_asset: Asset, + deposit_address: ::ChainAccount, + channel_id: ChannelId, deposit_amount: cf_primitives::AssetAmount, destination_asset: Asset, destination_address: EncodedAddress, @@ -285,6 +306,8 @@ impl ChunkedByVaultBuilder { .await? { match call_from_event::( + header.index, + contract_address, event, native_asset, source_chain, diff --git a/state-chain/cf-integration-tests/src/solana.rs b/state-chain/cf-integration-tests/src/solana.rs index d04d3bf093..a929e48031 100644 --- a/state-chain/cf-integration-tests/src/solana.rs +++ b/state-chain/cf-integration-tests/src/solana.rs @@ -34,7 +34,7 @@ use pallet_cf_elections::{ vote_storage::{composite::tuple_7_impls::CompositeVote, AuthorityVote}, CompositeAuthorityVoteOf, CompositeElectionIdentifierOf, MAXIMUM_VOTES_PER_EXTRINSIC, }; -use pallet_cf_ingress_egress::{DepositWitness, FetchOrTransfer}; +use pallet_cf_ingress_egress::{DepositWitness, FetchOrTransfer, VaultDepositWitness}; use pallet_cf_validator::RotationPhase; use sp_core::ConstU32; use sp_runtime::BoundedBTreeMap; @@ -437,6 +437,30 @@ fn can_send_solana_ccm() { }); } +fn vault_swap_deposit_witness( + deposit_metadata: Option, +) -> VaultDepositWitness { + VaultDepositWitness { + input_asset: SolAsset::Sol, + output_asset: Asset::SolUsdc, + deposit_amount: 1_000_000_000_000u64, + destination_address: EncodedAddress::Sol([1u8; 32]), + deposit_metadata, + tx_id: Default::default(), + deposit_details: (), + broker_fee: cf_primitives::Beneficiary { + account: sp_runtime::AccountId32::new([0; 32]), + bps: 0, + }, + affiliate_fees: Default::default(), + refund_params: REFUND_PARAMS, + dca_params: None, + boost_fee: 0, + deposit_address: SolAddress([2u8; 32]), + channel_id: 0, + } +} + #[test] fn solana_ccm_fails_with_invalid_input() { const EPOCH_BLOCKS: u32 = 100; @@ -507,21 +531,8 @@ fn solana_ccm_fails_with_invalid_input() { // Contract call fails with invalid CCM assert_ok!(RuntimeCall::SolanaIngressEgress( pallet_cf_ingress_egress::Call::vault_swap_request { - input_asset: SolAsset::Sol, - output_asset: Asset::SolUsdc, - deposit_amount: 1_000_000_000_000u64, - destination_address: EncodedAddress::Sol([1u8; 32]), - deposit_metadata: Some(invalid_ccm), - tx_id: Default::default(), - deposit_details: Box::new(()), - broker_fee: cf_primitives::Beneficiary { - account: sp_runtime::AccountId32::new([0; 32]), - bps: 0, - }, - affiliate_fees: Default::default(), - refund_params: Box::new(REFUND_PARAMS), - dca_params: None, - boost_fee: 0, + block_height: 0, + deposits: vec![vault_swap_deposit_witness(Some(invalid_ccm))], } ) .dispatch_bypass_filter( @@ -565,21 +576,8 @@ fn solana_ccm_fails_with_invalid_input() { witness_call(RuntimeCall::SolanaIngressEgress( pallet_cf_ingress_egress::Call::vault_swap_request { - input_asset: SolAsset::Sol, - output_asset: Asset::SolUsdc, - deposit_amount: 1_000_000_000_000u64, - destination_address: EncodedAddress::Sol([1u8; 32]), - deposit_metadata: Some(ccm), - tx_id: Default::default(), - deposit_details: Box::new(()), - broker_fee: cf_primitives::Beneficiary { - account: sp_runtime::AccountId32::new([0; 32]), - bps: 0, - }, - affiliate_fees: Default::default(), - refund_params: Box::new(REFUND_PARAMS), - dca_params: None, - boost_fee: 0, + block_height: 0, + deposits: vec![vault_swap_deposit_witness(Some(ccm))], }, )); // Setting the current agg key will invalidate the CCM. @@ -793,23 +791,9 @@ fn solana_ccm_execution_error_can_trigger_fallback() { }; witness_call(RuntimeCall::SolanaIngressEgress( pallet_cf_ingress_egress::Call::vault_swap_request { - input_asset: SolAsset::Sol, - output_asset: Asset::SolUsdc, - deposit_amount: 1_000_000_000_000u64, - destination_address: EncodedAddress::Sol([1u8; 32]), - deposit_metadata: Some(ccm), - tx_id: Default::default(), - deposit_details: Box::new(()), - broker_fee: cf_primitives::Beneficiary { - account: sp_runtime::AccountId32::new([0; 32]), - bps: 0, - }, - affiliate_fees: Default::default(), - refund_params: Box::new(REFUND_PARAMS), - dca_params: None, - boost_fee: 0, - - }, + block_height: 0, + deposits: vec![vault_swap_deposit_witness(Some(ccm))], + } )); // Wait for the swaps to complete and call broadcasted. diff --git a/state-chain/cf-integration-tests/src/swapping.rs b/state-chain/cf-integration-tests/src/swapping.rs index d09ea5b8ee..c41794916c 100644 --- a/state-chain/cf-integration-tests/src/swapping.rs +++ b/state-chain/cf-integration-tests/src/swapping.rs @@ -37,10 +37,10 @@ use pallet_cf_broadcast::{ AwaitingBroadcast, BroadcastIdCounter, PendingApiCalls, RequestFailureCallbacks, RequestSuccessCallbacks, }; -use pallet_cf_ingress_egress::{DepositWitness, FailedForeignChainCall}; +use pallet_cf_ingress_egress::{DepositWitness, FailedForeignChainCall, VaultDepositWitness}; use pallet_cf_pools::{HistoricalEarnedFees, OrderId, RangeOrderSize}; use pallet_cf_swapping::{SwapRequestIdCounter, SwapRetryDelay}; -use sp_core::U256; +use sp_core::{H160, U256}; use state_chain_runtime::{ chainflip::{ address_derivation::AddressDerivation, ChainAddressConverter, EthTransactionBuilder, @@ -569,6 +569,31 @@ fn ccm_deposit_metadata_mock() -> CcmDepositMetadata { } } +fn vault_swap_deposit_witness( + deposit_amount: u128, + output_asset: Asset, +) -> VaultDepositWitness { + VaultDepositWitness { + input_asset: EthAsset::Eth, + output_asset, + deposit_amount, + destination_address: EncodedAddress::Eth([0x02; 20]), + deposit_metadata: Some(ccm_deposit_metadata_mock()), + tx_id: Default::default(), + deposit_details: DepositDetails { tx_hashes: None }, + broker_fee: cf_primitives::Beneficiary { + account: sp_runtime::AccountId32::new([0; 32]), + bps: 0, + }, + affiliate_fees: Default::default(), + refund_params: ETH_REFUND_PARAMS, + dca_params: None, + boost_fee: 0, + deposit_address: H160::from([0x03; 20]), + channel_id: 0, + } +} + #[test] fn can_process_ccm_via_direct_deposit() { super::genesis::with_test_defaults().build().execute_with(|| { @@ -578,21 +603,8 @@ fn can_process_ccm_via_direct_deposit() { witness_call(RuntimeCall::EthereumIngressEgress( pallet_cf_ingress_egress::Call::vault_swap_request { - input_asset: EthAsset::Flip, - output_asset: Asset::Usdc, - deposit_amount, - destination_address: EncodedAddress::Eth([0x02; 20]), - deposit_metadata: Some(ccm_deposit_metadata_mock()), - tx_id: Default::default(), - deposit_details: Box::new(DepositDetails { tx_hashes: None }), - broker_fee: cf_primitives::Beneficiary { - account: sp_runtime::AccountId32::new([0; 32]), - bps: 0, - }, - affiliate_fees: Default::default(), - refund_params: Box::new(ETH_REFUND_PARAMS), - dca_params: None, - boost_fee: 0, + block_height: 0, + deposits: vec![vault_swap_deposit_witness(deposit_amount, Asset::Usdc)], }, )); @@ -632,21 +644,8 @@ fn failed_swaps_are_rolled_back() { witness_call(RuntimeCall::EthereumIngressEgress( pallet_cf_ingress_egress::Call::vault_swap_request { - input_asset: EthAsset::Eth, - output_asset: Asset::Flip, - deposit_amount: 10_000 * DECIMALS, - destination_address: EncodedAddress::Eth(Default::default()), - tx_id: Default::default(), - deposit_metadata: None, - deposit_details: Box::new(DepositDetails { tx_hashes: None }), - broker_fee: cf_primitives::Beneficiary { - account: sp_runtime::AccountId32::new([0; 32]), - bps: 0, - }, - affiliate_fees: Default::default(), - refund_params: Box::new(ETH_REFUND_PARAMS), - dca_params: None, - boost_fee: 0, + block_height: 0, + deposits: vec![vault_swap_deposit_witness(10_000 * DECIMALS, Asset::Flip)], }, )); @@ -798,22 +797,8 @@ fn can_resign_failed_ccm() { witness_call(RuntimeCall::EthereumIngressEgress( pallet_cf_ingress_egress::Call::vault_swap_request { - input_asset: EthAsset::Flip, - output_asset: Asset::Usdc, - deposit_amount: 10_000_000_000_000, - destination_address: EncodedAddress::Eth([0x02; 20]), - deposit_metadata: Some(ccm_deposit_metadata_mock()), - tx_id: Default::default(), - deposit_details: Box::new(DepositDetails { tx_hashes: None }), - broker_fee: cf_primitives::Beneficiary { - account: sp_runtime::AccountId32::new([0; 32]), - bps: 0, - }, - affiliate_fees: Default::default(), - - refund_params: Box::new(ETH_REFUND_PARAMS), - dca_params: None, - boost_fee: 0, + block_height: 0, + deposits: vec![vault_swap_deposit_witness(10_000_000_000_000, Asset::Usdc)], }, )); diff --git a/state-chain/chains/src/lib.rs b/state-chain/chains/src/lib.rs index a063e1982b..ed5a855183 100644 --- a/state-chain/chains/src/lib.rs +++ b/state-chain/chains/src/lib.rs @@ -665,6 +665,12 @@ pub enum SwapOrigin { Internal, } +#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo)] +pub enum DepositOriginType { + DepositChannel, + Vault, +} + pub const MAX_CCM_MSG_LENGTH: u32 = 10_000; pub const MAX_CCM_ADDITIONAL_DATA_LENGTH: u32 = 1_000; @@ -785,6 +791,8 @@ impl
CcmDepositMetadataGeneric
{ let principal_swap_amount = deposit_amount.saturating_sub(gas_budget); + // TODO: we already check ccm support when opening a channel (and we have to). + // If we can also check this in vault swaps, we should be able to remove this here. let destination_chain: ForeignChain = destination_asset.into(); if !destination_chain.ccm_support() { return Err(CcmFailReason::UnsupportedForTargetChain) diff --git a/state-chain/pallets/cf-ingress-egress/src/benchmarking.rs b/state-chain/pallets/cf-ingress-egress/src/benchmarking.rs index 3d37dd5598..891dd2667e 100644 --- a/state-chain/pallets/cf-ingress-egress/src/benchmarking.rs +++ b/state-chain/pallets/cf-ingress-egress/src/benchmarking.rs @@ -307,22 +307,27 @@ mod benchmarks { }, }; let call = Call::::vault_swap_request { - input_asset: BenchmarkValue::benchmark_value(), - deposit_amount: 1_000u32.into(), - output_asset: Asset::Eth, - destination_address: EncodedAddress::benchmark_value(), - deposit_metadata: Some(deposit_metadata), - tx_id: TransactionInIdFor::::benchmark_value(), - deposit_details: Box::new(BenchmarkValue::benchmark_value()), - broker_fee: cf_primitives::Beneficiary { account: account("broker", 0, 0), bps: 0 }, - affiliate_fees: Default::default(), - refund_params: Box::new(ChannelRefundParameters { - retry_duration: Default::default(), - refund_address: ForeignChainAddress::Eth(Default::default()), - min_price: Default::default(), - }), - dca_params: None, - boost_fee: 0, + block_height: 0u32.into(), + deposits: vec![VaultDepositWitness { + input_asset: BenchmarkValue::benchmark_value(), + output_asset: Asset::Eth, + deposit_amount: 1_000u32.into(), + destination_address: EncodedAddress::benchmark_value(), + deposit_metadata: Some(deposit_metadata), + tx_id: TransactionInIdFor::::benchmark_value(), + deposit_details: BenchmarkValue::benchmark_value(), + broker_fee: cf_primitives::Beneficiary { account: account("broker", 0, 0), bps: 0 }, + affiliate_fees: Default::default(), + refund_params: ChannelRefundParameters { + retry_duration: Default::default(), + refund_address: ForeignChainAddress::Eth(Default::default()), + min_price: Default::default(), + }, + dca_params: None, + boost_fee: 0, + channel_id: 0, + deposit_address: BenchmarkValue::benchmark_value(), + }], }; #[block] diff --git a/state-chain/pallets/cf-ingress-egress/src/lib.rs b/state-chain/pallets/cf-ingress-egress/src/lib.rs index 92cb0cc458..1ae27d03b2 100644 --- a/state-chain/pallets/cf-ingress-egress/src/lib.rs +++ b/state-chain/pallets/cf-ingress-egress/src/lib.rs @@ -30,9 +30,9 @@ use cf_chains::{ ccm_checker::CcmValidityCheck, AllBatch, AllBatchError, CcmAdditionalData, CcmChannelMetadata, CcmDepositMetadata, CcmFailReason, CcmMessage, Chain, ChainCrypto, ChannelLifecycleHooks, ChannelRefundParameters, - ConsolidateCall, DepositChannel, DepositDetailsToTransactionInId, ExecutexSwapAndCall, - FetchAssetParams, ForeignChainAddress, IntoTransactionInIdForAnyChain, RejectCall, SwapOrigin, - TransferAssetParams, + ConsolidateCall, DepositChannel, DepositDetailsToTransactionInId, DepositOriginType, + ExecutexSwapAndCall, FetchAssetParams, ForeignChainAddress, IntoTransactionInIdForAnyChain, + RejectCall, SwapOrigin, TransactionInIdForAnyChain, TransferAssetParams, }; use cf_primitives::{ AccountRole, AffiliateShortId, Affiliates, Asset, AssetAmount, BasisPoints, Beneficiaries, @@ -62,7 +62,6 @@ use scale_info::{ }; use sp_runtime::traits::UniqueSaturatedInto; use sp_std::{ - boxed::Box, collections::{btree_map::BTreeMap, btree_set::BTreeSet}, marker::PhantomData, vec, @@ -140,6 +139,76 @@ pub enum DepositIgnoredReason { TransactionTainted, } +enum FullWitnessDepositOutcome { + BoostFinalised, + DepositActionPerformed, +} + +mod deposit_origin { + + use super::*; + + // TODO: replace SwapOrigin with this? + #[derive(Clone)] + pub(super) enum DepositOrigin { + DepositChannel { + deposit_address: EncodedAddress, + channel_id: ChannelId, + deposit_block_height: u64, + }, + Vault { + tx_id: TransactionInIdForAnyChain, + }, + } + + impl DepositOrigin { + pub(super) fn deposit_channel, I: 'static>( + deposit_address: ::ChainAccount, + channel_id: ChannelId, + deposit_block_height: ::ChainBlockNumber, + ) -> Self { + DepositOrigin::DepositChannel { + deposit_address: T::AddressConverter::to_encoded_address( + ::ChainAccount::into_foreign_chain_address( + deposit_address.clone(), + ), + ), + channel_id, + deposit_block_height: deposit_block_height.into(), + } + } + + pub(super) fn vault, I: 'static>(tx_id: TransactionInIdFor) -> Self { + DepositOrigin::Vault { tx_id: tx_id.into_transaction_in_id_for_any_chain() } + } + } + + impl From for DepositOriginType { + fn from(origin: DepositOrigin) -> Self { + match origin { + DepositOrigin::DepositChannel { .. } => DepositOriginType::DepositChannel, + DepositOrigin::Vault { .. } => DepositOriginType::Vault, + } + } + } + + impl From for SwapOrigin { + fn from(origin: DepositOrigin) -> SwapOrigin { + match origin { + DepositOrigin::Vault { tx_id } => SwapOrigin::Vault { tx_id }, + DepositOrigin::DepositChannel { + deposit_address, + channel_id, + deposit_block_height, + } => + SwapOrigin::DepositChannel { deposit_address, channel_id, deposit_block_height }, + } + } + } +} + +use deposit_origin::DepositOrigin; + /// Holds information about a tainted transaction. #[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, TypeInfo, CloneNoBound)] #[scale_info(skip_type_params(T, I))] @@ -283,6 +352,27 @@ pub mod pallet { pub deposit_details: C::DepositDetails, } + #[derive( + CloneNoBound, RuntimeDebugNoBound, PartialEqNoBound, EqNoBound, Encode, Decode, TypeInfo, + )] + #[scale_info(skip_type_params(T, I))] + pub struct VaultDepositWitness, I: 'static> { + pub input_asset: TargetChainAsset, + pub deposit_address: TargetChainAccount, + pub channel_id: ChannelId, + pub deposit_amount: ::ChainAmount, + pub deposit_details: ::DepositDetails, + pub output_asset: Asset, + pub destination_address: EncodedAddress, + pub deposit_metadata: Option, + pub tx_id: TransactionInIdFor, + pub broker_fee: Beneficiary, + pub affiliate_fees: Affiliates, + pub refund_params: ChannelRefundParameters, + pub dca_params: Option, + pub boost_fee: BasisPoints, + } + #[derive(CloneNoBound, RuntimeDebug, PartialEq, Eq, Encode, Decode, TypeInfo)] #[scale_info(skip_type_params(T, I))] pub struct DepositChannelDetails, I: 'static> { @@ -598,6 +688,16 @@ pub mod pallet { pub(crate) type FailedRejections, I: 'static = ()> = StorageValue<_, Vec>, ValueQuery>; + /// Stores transaction ids that have been boosted but have not yet been finalised. + #[pallet::storage] + pub(crate) type BoostedVaultTxs, I: 'static = ()> = StorageMap< + _, + Identity, + TransactionInIdFor, + BoostStatus>, + OptionQuery, + >; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event, I: 'static = ()> { @@ -612,6 +712,7 @@ pub mod pallet { ingress_fee: TargetChainAmount, action: DepositAction, channel_id: ChannelId, + origin_type: DepositOriginType, }, AssetEgressStatusChanged { asset: TargetChainAsset, @@ -699,6 +800,7 @@ pub mod pallet { // Total fee the user paid for their deposit to be boosted. boost_fee: TargetChainAmount, action: DepositAction, + origin_type: DepositOriginType, }, BoostFundsAdded { booster_id: T::AccountId, @@ -1301,37 +1403,19 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::vault_swap_request())] pub fn vault_swap_request( origin: OriginFor, - input_asset: TargetChainAsset, - output_asset: Asset, - deposit_amount: ::ChainAmount, - destination_address: EncodedAddress, - deposit_metadata: Option, - tx_id: TransactionInIdFor, - deposit_details: Box<::DepositDetails>, - broker_fee: Beneficiary, - affiliate_fees: Affiliates, - refund_params: Box, - dca_params: Option, - boost_fee: BasisPoints, + block_height: TargetChainBlockNumber, + deposits: Vec>, ) -> DispatchResult { if T::EnsureWitnessed::ensure_origin(origin.clone()).is_ok() { - Self::process_vault_swap_request( - input_asset, - deposit_amount, - output_asset, - destination_address, - deposit_metadata, - tx_id, - *deposit_details, - broker_fee, - affiliate_fees, - *refund_params, - dca_params, - boost_fee, - ); + for deposit in deposits { + Self::process_vault_swap_request_full_witness(block_height, deposit); + } } else { T::EnsurePrewitnessed::ensure_origin(origin)?; - // Pre-witnessed vault swaps are not supported yet. + + for deposit in deposits { + Self::process_vault_swap_request_prewitness(block_height, deposit); + } } Ok(()) @@ -1727,12 +1811,6 @@ impl, I: 'static> Pallet { ) -> DispatchResult { for DepositWitness { deposit_address, asset, amount, deposit_details } in deposit_witnesses { - if amount < MinimumDeposit::::get(asset) { - // We do not process/record pre-witnessed deposits for amounts smaller - // than MinimumDeposit to match how this is done on finalisation - continue; - } - let DepositChannelDetails { deposit_channel, action, @@ -1743,95 +1821,32 @@ impl, I: 'static> Pallet { } = DepositChannelLookup::::get(&deposit_address) .ok_or(Error::::InvalidDepositAddress)?; - if let Some(tx_id) = deposit_details.deposit_id() { - if TaintedTransactions::::mutate(&owner, &tx_id, |opt| { - match opt.as_mut() { - // Transaction has been reported, mark it as pre-witnessed. - Some(status @ TaintedTransactionStatus::Unseen) => { - *status = TaintedTransactionStatus::Prewitnessed; - true - }, - // Pre-witnessing twice is unlikely but possible. Either way we don't want - // to change the status and we don't want to allow boosting. - Some(TaintedTransactionStatus::Prewitnessed) => true, - // Transaction has not been reported, mark it as boosted to prevent further - // reports. - None => false, - } - }) { - continue; - } - } + let origin = DepositOrigin::deposit_channel::( + deposit_address.clone(), + deposit_channel.channel_id, + block_height, + ); - let prewitnessed_deposit_id = - PrewitnessedDepositIdCounter::::mutate(|id| -> u64 { - *id = id.saturating_add(1); - *id + if let Some(new_boost_status) = Self::process_prewitness_deposit_inner( + amount, + asset, + deposit_details, + deposit_address.clone(), + None, // source address is unknown + action, + &owner, + boost_fee, + boost_status, + deposit_channel.channel_id, + block_height, + origin, + ) { + // Update boost status + DepositChannelLookup::::mutate(&deposit_address, |details| { + if let Some(details) = details { + details.boost_status = new_boost_status; + } }); - - let channel_id = deposit_channel.channel_id; - - // Only boost on non-zero fee and if the channel isn't already boosted: - if T::SafeMode::get().boost_deposits_enabled && - boost_fee > 0 && - !matches!(boost_status, BoostStatus::Boosted { .. }) - { - match Self::try_boosting(asset, amount, boost_fee, prewitnessed_deposit_id) { - Ok(BoostOutput { used_pools, total_fee: boost_fee_amount }) => { - DepositChannelLookup::::mutate(&deposit_address, |details| { - if let Some(details) = details { - details.boost_status = BoostStatus::Boosted { - prewitnessed_deposit_id, - pools: used_pools.keys().cloned().collect(), - amount, - }; - } - }); - - let amount_after_boost_fee = amount.saturating_sub(boost_fee_amount); - - // Note that ingress fee is deducted at the time of boosting rather than the - // time the deposit is finalised (which allows us to perform the channel - // action immediately): - let AmountAndFeesWithheld { amount_after_fees, fees_withheld: ingress_fee } = - Self::withhold_ingress_or_egress_fee( - IngressOrEgress::Ingress, - asset, - amount_after_boost_fee, - ); - - let action = Self::perform_channel_action( - action, - deposit_channel, - amount_after_fees, - block_height, - )?; - - Self::deposit_event(Event::DepositBoosted { - deposit_address: deposit_address.clone(), - asset, - amounts: used_pools, - block_height, - prewitnessed_deposit_id, - channel_id, - deposit_details: deposit_details.clone(), - ingress_fee, - boost_fee: boost_fee_amount, - action, - }); - }, - Err(err) => { - Self::deposit_event(Event::InsufficientBoostLiquidity { - prewitnessed_deposit_id, - asset, - amount_attempted: amount, - channel_id, - }); - log::debug!( - "Deposit (id: {prewitnessed_deposit_id}) of {amount:?} {asset:?} and boost fee {boost_fee} could not be boosted: {err:?}" - ); - }, - } } } Ok(()) @@ -1839,22 +1854,11 @@ impl, I: 'static> Pallet { fn perform_channel_action( action: ChannelAction, - DepositChannel { asset, address: deposit_address, channel_id, .. }: DepositChannel< - T::TargetChain, - >, + asset: TargetChainAsset, + source_address: Option, amount_after_fees: TargetChainAmount, - block_height: TargetChainBlockNumber, + origin: DepositOrigin, ) -> Result, DispatchError> { - let swap_origin = SwapOrigin::DepositChannel { - deposit_address: T::AddressConverter::to_encoded_address( - ::ChainAccount::into_foreign_chain_address( - deposit_address.clone(), - ), - ), - channel_id, - deposit_block_height: block_height.into(), - }; - let action = match action { ChannelAction::LiquidityProvision { lp_account, .. } => { T::Balance::try_credit_account( @@ -1879,7 +1883,7 @@ impl, I: 'static> Pallet { broker_fees, refund_params, dca_params, - swap_origin, + origin.into(), ); DepositAction::Swap { swap_request_id } }, @@ -1894,7 +1898,7 @@ impl, I: 'static> Pallet { let deposit_metadata = CcmDepositMetadata { channel_metadata, source_chain: asset.into(), - source_address: None, + source_address, }; match deposit_metadata.clone().into_swap_metadata( amount_after_fees.into(), @@ -1913,7 +1917,7 @@ impl, I: 'static> Pallet { broker_fees, refund_params, dca_params, - swap_origin, + origin.into(), ); DepositAction::CcmTransfer { swap_request_id } }, @@ -1926,7 +1930,7 @@ impl, I: 'static> Pallet { deposit_metadata: deposit_metadata .clone() .to_encoded::(), - origin: swap_origin.clone(), + origin: origin.into(), }); DepositAction::NoAction }, @@ -1965,6 +1969,284 @@ impl, I: 'static> Pallet { return Err(Error::::InvalidDepositAddress.into()) } + let origin = DepositOrigin::deposit_channel::( + deposit_address.clone(), + channel_id, + block_height, + ); + + if let Ok(FullWitnessDepositOutcome::BoostFinalised) = + Self::process_full_witness_deposit_inner( + deposit_address.clone(), + asset, + deposit_amount, + deposit_details, + None, // source address is unknown + &deposit_channel_details.owner, + deposit_channel_details.boost_status, + channel_id, + deposit_channel_details.action, + block_height, + origin, + ) { + // This allows the channel to be boosted again: + DepositChannelLookup::::mutate(&deposit_address, |details| { + if let Some(details) = details { + details.boost_status = BoostStatus::NotBoosted; + } + }); + } + + Ok(()) + } + + fn assemble_broker_fees( + broker_fee: Beneficiary, + affiliate_fees: Affiliates, + ) -> Beneficiaries { + let primary_broker = broker_fee.account.clone(); + + if T::AccountRoleRegistry::has_account_role(&primary_broker, AccountRole::Broker) { + [broker_fee] + .into_iter() + .chain(affiliate_fees.into_iter().filter_map( + |Beneficiary { account: short_affiliate_id, bps }| { + if let Some(affiliate_id) = T::AffiliateRegistry::get_account_id( + &primary_broker, + short_affiliate_id, + ) { + Some(Beneficiary { account: affiliate_id, bps }) + } else { + // In case the entry not found, we ignore the entry, but process the + // swap (to avoid having to refund it). + Self::deposit_event(Event::::UnknownAffiliate { + broker_id: primary_broker.clone(), + short_affiliate_id, + }); + + None + } + }, + )) + .collect::>() + .try_into() + .expect( + "must fit since affiliates are limited to 1 fewer element than beneficiaries", + ) + } else { + Self::deposit_event(Event::::UnknownBroker { broker_id: primary_broker }); + Default::default() + } + } + + fn process_prewitness_deposit_inner( + amount: TargetChainAmount, + asset: TargetChainAsset, + deposit_details: ::DepositDetails, + deposit_address: TargetChainAccount, + source_address: Option, + action: ChannelAction, + broker: &T::AccountId, + boost_fee: u16, + boost_status: BoostStatus>, + channel_id: u64, + block_height: TargetChainBlockNumber, + origin: DepositOrigin, + ) -> Option>> { + if amount < MinimumDeposit::::get(asset) { + // We do not process/record pre-witnessed deposits for amounts smaller + // than MinimumDeposit to match how this is done on finalisation + return None; + } + + if let Some(tx_id) = deposit_details.deposit_id() { + if TaintedTransactions::::mutate(broker, &tx_id, |opt| { + match opt.as_mut() { + // Transaction has been reported, mark it as pre-witnessed. + Some(status @ TaintedTransactionStatus::Unseen) => { + *status = TaintedTransactionStatus::Prewitnessed; + true + }, + // Pre-witnessing twice is unlikely but possible. Either way we don't want + // to change the status and we don't want to allow boosting. + Some(TaintedTransactionStatus::Prewitnessed) => true, + // Transaction has not been reported, mark it as boosted to prevent further + // reports. + None => false, + } + }) { + return None; + } + } + + let prewitnessed_deposit_id = PrewitnessedDepositIdCounter::::mutate(|id| -> u64 { + *id = id.saturating_add(1); + *id + }); + + // Only boost on non-zero fee and if the channel isn't already boosted: + if T::SafeMode::get().boost_deposits_enabled && + boost_fee > 0 && + !matches!(boost_status, BoostStatus::Boosted { .. }) + { + match Self::try_boosting(asset, amount, boost_fee, prewitnessed_deposit_id) { + Ok(BoostOutput { used_pools, total_fee: boost_fee_amount }) => { + let amount_after_boost_fee = amount.saturating_sub(boost_fee_amount); + + // Note that ingress fee is deducted at the time of boosting rather than the + // time the deposit is finalised (which allows us to perform the channel + // action immediately): + let AmountAndFeesWithheld { amount_after_fees, fees_withheld: ingress_fee } = + Self::conditionally_withhold_ingress_fee( + asset, + amount_after_boost_fee, + &origin, + ); + + let used_pool_tiers = used_pools.keys().cloned().collect(); + + if let Ok(action) = Self::perform_channel_action( + action, + asset, + source_address, + amount_after_fees, + origin.clone(), + ) { + Self::deposit_event(Event::DepositBoosted { + deposit_address, + asset, + amounts: used_pools, + block_height, + prewitnessed_deposit_id, + channel_id, + deposit_details: deposit_details.clone(), + ingress_fee, + boost_fee: boost_fee_amount, + action, + origin_type: origin.into(), + }); + } else { + // TODO: emit error? + } + + return Some(BoostStatus::Boosted { + prewitnessed_deposit_id, + pools: used_pool_tiers, + amount, + }); + }, + Err(err) => { + Self::deposit_event(Event::InsufficientBoostLiquidity { + prewitnessed_deposit_id, + asset, + amount_attempted: amount, + channel_id, + }); + log::debug!( + "Deposit (id: {prewitnessed_deposit_id}) of {amount:?} {asset:?} and boost fee {boost_fee} could not be boosted: {err:?}" + ); + }, + } + } + + None + } + + fn process_vault_swap_request_prewitness( + block_height: TargetChainBlockNumber, + VaultDepositWitness { + input_asset: asset, + deposit_address, + channel_id, + deposit_amount: amount, + deposit_details, + output_asset, + destination_address, + deposit_metadata, + tx_id, + broker_fee, + affiliate_fees, + refund_params, + dca_params, + boost_fee, + }: VaultDepositWitness, + ) { + let destination_address_internal = + match T::AddressConverter::decode_and_validate_address_for_asset( + destination_address.clone(), + output_asset, + ) { + Ok(address) => address, + Err(err) => { + log::warn!("Failed to process vault swap due to invalid destination address. Tx hash: {tx_id:?}. Error: {err:?}"); + return; + }, + }; + + let broker = broker_fee.account.clone(); + + let broker_fees = Self::assemble_broker_fees(broker_fee, affiliate_fees); + + let (action, source_address) = if let Some(deposit_metadata) = deposit_metadata { + ( + ChannelAction::CcmTransfer { + destination_asset: output_asset, + destination_address: destination_address_internal, + broker_fees, + refund_params: Some(refund_params), + dca_params, + channel_metadata: deposit_metadata.channel_metadata, + }, + deposit_metadata.source_address, + ) + } else { + ( + ChannelAction::Swap { + destination_asset: output_asset, + destination_address: destination_address_internal, + broker_fees, + refund_params: Some(refund_params), + dca_params, + }, + None, + ) + }; + + let boost_status = BoostedVaultTxs::::get(&tx_id).unwrap_or(BoostStatus::NotBoosted); + + let origin = DepositOrigin::vault::(tx_id.clone()); + + if let Some(new_boost_status) = Self::process_prewitness_deposit_inner( + amount, + asset, + deposit_details, + deposit_address, + source_address, + action, + &broker, + boost_fee, + boost_status, + channel_id, + block_height, + origin, + ) { + BoostedVaultTxs::::insert(&tx_id, new_boost_status); + } + } + + fn process_full_witness_deposit_inner( + deposit_address: TargetChainAccount, + asset: TargetChainAsset, + deposit_amount: TargetChainAmount, + deposit_details: ::DepositDetails, + source_address: Option, + broker: &T::AccountId, + boost_status: BoostStatus>, + channel_id: u64, + action: ChannelAction, + block_height: TargetChainBlockNumber, + origin: DepositOrigin, + ) -> Result { // TODO: only apply this check if the deposit hasn't been boosted // already (in case MinimumDeposit increases after some small deposit // is boosted)? @@ -1979,16 +2261,14 @@ impl, I: 'static> Pallet { deposit_details, reason: DepositIgnoredReason::BelowMinimumDeposit, }); - return Ok(()) + return Err(()); } - let channel_owner = deposit_channel_details.owner.clone(); - if let Some(tx_id) = deposit_details.deposit_id() { - if TaintedTransactions::::take(&channel_owner, &tx_id).is_some() && - !matches!(deposit_channel_details.boost_status, BoostStatus::Boosted { .. }) + if TaintedTransactions::::take(broker, &tx_id).is_some() && + !matches!(boost_status, BoostStatus::Boosted { .. }) { - let refund_address = match deposit_channel_details.action.clone() { + let refund_address = match action.clone() { ChannelAction::Swap { refund_params, .. } => refund_params .as_ref() .map(|refund_params| refund_params.refund_address.clone()), @@ -2013,17 +2293,22 @@ impl, I: 'static> Pallet { deposit_details, reason: DepositIgnoredReason::TransactionTainted, }); - return Ok(()) + return Err(()); } } - ScheduledEgressFetchOrTransfer::::append(FetchOrTransfer::::Fetch { - asset, - deposit_address: deposit_address.clone(), - deposit_fetch_id: None, - amount: deposit_amount, - }); - Self::deposit_event(Event::::DepositFetchesScheduled { channel_id, asset }); + // Vault deposits don't need to be fetched: + if !matches!(origin, DepositOrigin::Vault { .. }) { + ScheduledEgressFetchOrTransfer::::append( + FetchOrTransfer::::Fetch { + asset, + deposit_address: deposit_address.clone(), + deposit_fetch_id: None, + amount: deposit_amount, + }, + ); + Self::deposit_event(Event::::DepositFetchesScheduled { channel_id, asset }); + } // Add the deposit to the balance. T::DepositHandler::on_deposit_made(deposit_details.clone()); @@ -2032,7 +2317,7 @@ impl, I: 'static> Pallet { // (i.e. awaiting finalisation), *and* the boosted amount matches the amount // in this deposit, finalise the boost by crediting boost pools with the deposit. // Process as non-boosted deposit otherwise: - let maybe_boost_to_process = match deposit_channel_details.boost_status { + let maybe_boost_to_process = match boost_status { BoostStatus::Boosted { prewitnessed_deposit_id, pools, amount } if amount == deposit_amount => Some((prewitnessed_deposit_id, pools)), @@ -2063,30 +2348,23 @@ impl, I: 'static> Pallet { }); } - // This allows the channel to be boosted again: - DepositChannelLookup::::mutate(&deposit_address, |details| { - if let Some(details) = details { - details.boost_status = BoostStatus::NotBoosted; - } - }); - Self::deposit_event(Event::DepositFinalised { deposit_address, asset, amount: deposit_amount, block_height, deposit_details, + // no ingrees fee as it was already charged at the time of boosting ingress_fee: 0u32.into(), action: DepositAction::BoostersCredited { prewitnessed_deposit_id }, channel_id, + origin_type: origin.into(), }); + + Ok(FullWitnessDepositOutcome::BoostFinalised) } else { let AmountAndFeesWithheld { amount_after_fees, fees_withheld } = - Self::withhold_ingress_or_egress_fee( - IngressOrEgress::Ingress, - deposit_channel_details.deposit_channel.asset, - deposit_amount, - ); + Self::conditionally_withhold_ingress_fee(asset, deposit_amount, &origin); if amount_after_fees.is_zero() { Self::deposit_event(Event::::DepositIgnored { @@ -2096,59 +2374,58 @@ impl, I: 'static> Pallet { deposit_details, reason: DepositIgnoredReason::NotEnoughToPayFees, }); + Err(()) } else { - let deposit_action = Self::perform_channel_action( - deposit_channel_details.action, - deposit_channel_details.deposit_channel, - amount_after_fees, - block_height, - )?; + // Processing as a non-boosted deposit: - Self::deposit_event(Event::DepositFinalised { - deposit_address, + if let Ok(action) = Self::perform_channel_action( + action, asset, - amount: deposit_amount, - block_height, - deposit_details, - ingress_fee: fees_withheld, - action: deposit_action, - channel_id, - }); + source_address, + amount_after_fees, + origin.clone(), + ) { + // TODO: this needs to include deposit type (vault/channel) + Self::deposit_event(Event::DepositFinalised { + deposit_address, + asset, + amount: deposit_amount, + block_height, + deposit_details, + ingress_fee: fees_withheld, + action, + channel_id, + origin_type: origin.into(), + }); + Ok(FullWitnessDepositOutcome::DepositActionPerformed) + } else { + Err(()) + } } } - - Ok(()) } - pub fn process_vault_swap_request( - source_asset: TargetChainAsset, - deposit_amount: ::ChainAmount, - destination_asset: Asset, - destination_address: EncodedAddress, - deposit_metadata: Option, - tx_id: TransactionInIdFor, - deposit_details: ::DepositDetails, - broker_fee: Beneficiary, - affiliate_fees: Affiliates, - refund_params: ChannelRefundParameters, - dca_params: Option, - // This is only to be checked in the pre-witnessed version (not implemented yet) - _boost_fee: BasisPoints, + fn process_vault_swap_request_full_witness( + block_height: TargetChainBlockNumber, + VaultDepositWitness { + input_asset: source_asset, + deposit_address, + channel_id, + deposit_amount, + deposit_details, + output_asset: destination_asset, + destination_address, + deposit_metadata, + tx_id, + broker_fee, + affiliate_fees, + refund_params, + dca_params, + // Boost fee is only relevant for prewitnessing + boost_fee: _, + }: VaultDepositWitness, ) { - if deposit_amount < MinimumDeposit::::get(source_asset) { - // If the deposit amount is below the minimum allowed, the deposit is ignored. - // TODO: track these funds somewhere, for example add them to the withheld fees. - Self::deposit_event(Event::::DepositIgnored { - deposit_address: None, - asset: source_asset, - amount: deposit_amount, - deposit_details, - reason: DepositIgnoredReason::BelowMinimumDeposit, - }); - return; - } - - T::DepositHandler::on_deposit_made(deposit_details); + let boost_status = BoostedVaultTxs::::get(&tx_id).unwrap_or(BoostStatus::NotBoosted); let destination_address_internal = match T::AddressConverter::decode_and_validate_address_for_asset( @@ -2162,116 +2439,96 @@ impl, I: 'static> Pallet { }, }; - let swap_origin = - SwapOrigin::Vault { tx_id: tx_id.clone().into_transaction_in_id_for_any_chain() }; - - let request_type = if let Some(deposit_metadata) = deposit_metadata { - let ccm_failed = |reason| { - log::warn!("Failed to process CCM. Tx id: {:?}, Reason: {:?}", tx_id, reason); - - Self::deposit_event(Event::::CcmFailed { - reason, - destination_address: destination_address.clone(), - deposit_metadata: deposit_metadata.clone().to_encoded::(), - origin: swap_origin.clone(), - }); - }; + let deposit_origin = DepositOrigin::vault::(tx_id.clone()); + if let Some(deposit_metadata) = &deposit_metadata { if T::CcmValidityChecker::check_and_decode( &deposit_metadata.channel_metadata, destination_asset, ) .is_err() { - ccm_failed(CcmFailReason::InvalidMetadata); + log::warn!( + "Failed to process CCM. Tx id: {:?}, Reason: {:?}", + tx_id, + CcmFailReason::InvalidMetadata + ); + + Self::deposit_event(Event::::CcmFailed { + reason: CcmFailReason::InvalidMetadata, + destination_address: destination_address.clone(), + deposit_metadata: deposit_metadata.clone().to_encoded::(), + origin: deposit_origin.clone().into(), + }); + return; }; - - match deposit_metadata.clone().into_swap_metadata( - deposit_amount.into(), - source_asset.into(), - destination_asset, - ) { - Ok(ccm_swap_metadata) => SwapRequestType::Ccm { - ccm_swap_metadata, - output_address: destination_address_internal.clone(), - }, - Err(reason) => { - ccm_failed(reason); - return; - }, - } - } else { - SwapRequestType::Regular { output_address: destination_address_internal.clone() } - }; + } if let Err(err) = T::SwapLimitsProvider::validate_refund_params(refund_params.retry_duration) { - log::warn!( - "Failed to process vault swap due to invalid refund params. Tx id: {tx_id:?}. Error: {err:?}", - ); + log::warn!("Failed to process vault swap due to invalid refund params. Tx id: {tx_id:?}. Error: {err:?}"); return; } if let Some(params) = &dca_params { if let Err(err) = T::SwapLimitsProvider::validate_dca_params(params) { - log::warn!( - "Failed to process vault swap due to invalid dca params. Tx id: {tx_id:?}. Error: {err:?}", - ); + log::warn!("Failed to process vault swap due to invalid dca params. Tx id: {tx_id:?}. Error: {err:?}"); return; } } - let primary_broker = broker_fee.account.clone(); - - let broker_fees = - if T::AccountRoleRegistry::has_account_role(&primary_broker, AccountRole::Broker) { - [broker_fee] - .into_iter() - .chain(affiliate_fees.into_iter().filter_map( - |Beneficiary { account: short_affiliate_id, bps }| { - if let Some(affiliate_id) = - T::AffiliateRegistry::get_account_id(&primary_broker, short_affiliate_id) - { - Some(Beneficiary { account: affiliate_id, bps }) - } else { - // In case the entry not found, we ignore the entry, but process the - // swap (to avoid having to refund it). - Self::deposit_event(Event::::UnknownAffiliate { - broker_id: primary_broker.clone(), - short_affiliate_id, - }); - - None - } - }, - )) - .collect::>() - .try_into() - .expect("must fit since affiliates are limited to 1 fewer element than beneficiaries") - } else { - Self::deposit_event(Event::::UnknownBroker { broker_id: primary_broker }); - Default::default() - }; + let broker = broker_fee.account.clone(); + let broker_fees = Self::assemble_broker_fees(broker_fee, affiliate_fees); if let Err(err) = T::SwapLimitsProvider::validate_broker_fees(&broker_fees) { - log::warn!( - "Failed to process vault swap due to invalid broker fees. Tx id: {tx_id:?}. Error: {err:?}", - ); + log::warn!("Failed to process vault swap due to invalid broker fees. Tx id: {tx_id:?}. Error: {err:?}"); return; } - T::SwapRequestHandler::init_swap_request( - source_asset.into(), - deposit_amount.into(), - destination_asset, - request_type, - broker_fees, - Some(refund_params), - dca_params, - swap_origin, - ); + let (action, source_address) = if let Some(deposit_metadata) = deposit_metadata { + ( + ChannelAction::CcmTransfer { + destination_asset, + destination_address: destination_address_internal, + broker_fees, + refund_params: Some(refund_params), + dca_params, + channel_metadata: deposit_metadata.channel_metadata, + }, + deposit_metadata.source_address, + ) + } else { + ( + ChannelAction::Swap { + destination_asset, + destination_address: destination_address_internal, + broker_fees, + refund_params: Some(refund_params), + dca_params, + }, + None, + ) + }; + + if let Ok(FullWitnessDepositOutcome::BoostFinalised) = + Self::process_full_witness_deposit_inner( + deposit_address.clone(), + source_asset, + deposit_amount, + deposit_details, + source_address, + &broker, + boost_status, + channel_id, + action, + block_height, + deposit_origin, + ) { + // This allows the channel to be boosted again: + BoostedVaultTxs::::remove(&tx_id); + } } fn expiry_and_recycle_block_height( @@ -2396,6 +2653,23 @@ impl, I: 'static> Pallet { .cloned() } + // Withholds ingress fee, but only after checking the origin + fn conditionally_withhold_ingress_fee( + asset: TargetChainAsset, + available_amount: TargetChainAmount, + origin: &DepositOrigin, + ) -> AmountAndFeesWithheld { + if matches!(origin, &DepositOrigin::DepositChannel { .. }) { + Self::withhold_ingress_or_egress_fee(IngressOrEgress::Ingress, asset, available_amount) + } else { + // No ingress fee for vault swaps. + AmountAndFeesWithheld { + amount_after_fees: available_amount, + fees_withheld: 0u32.into(), + } + } + } + /// Withholds the fee for a given amount. /// /// Returns the remaining amount after the fee has been withheld, and the fee itself, both diff --git a/state-chain/pallets/cf-ingress-egress/src/tests.rs b/state-chain/pallets/cf-ingress-egress/src/tests.rs index 132461e325..e25c2e82c9 100644 --- a/state-chain/pallets/cf-ingress-egress/src/tests.rs +++ b/state-chain/pallets/cf-ingress-egress/src/tests.rs @@ -8,7 +8,7 @@ use crate::{ DisabledEgressAssets, EgressDustLimit, Event as PalletEvent, FailedForeignChainCall, FailedForeignChainCalls, FetchOrTransfer, MinimumDeposit, Pallet, PalletConfigUpdate, PalletSafeMode, PrewitnessedDepositIdCounter, ScheduledEgressCcm, - ScheduledEgressFetchOrTransfer, + ScheduledEgressFetchOrTransfer, VaultDepositWitness, }; use cf_chains::{ address::{AddressConverter, EncodedAddress}, @@ -16,11 +16,12 @@ use cf_chains::{ btc::{BitcoinNetwork, ScriptPubkey}, evm::{DepositDetails, EvmFetchId, H256}, mocks::MockEthereum, - CcmChannelMetadata, CcmFailReason, ChannelRefundParameters, DepositChannel, + CcmChannelMetadata, CcmFailReason, ChannelRefundParameters, DepositChannel, DepositOriginType, ExecutexSwapAndCall, SwapOrigin, TransactionInIdForAnyChain, TransferAssetParams, }; use cf_primitives::{ - AffiliateShortId, AssetAmount, BasisPoints, Beneficiary, ChannelId, ForeignChain, + AffiliateShortId, Affiliates, AssetAmount, BasisPoints, Beneficiary, ChannelId, DcaParameters, + ForeignChain, }; use cf_test_utilities::{assert_events_eq, assert_has_event, assert_has_matching_event}; use cf_traits::{ @@ -47,7 +48,7 @@ use frame_support::{ weights::Weight, }; use sp_core::{bounded_vec, H160}; -use sp_runtime::DispatchError; +use sp_runtime::{DispatchError, DispatchResult}; const ALICE_ETH_ADDRESS: EthereumAddress = H160([100u8; 20]); const BOB_ETH_ADDRESS: EthereumAddress = H160([101u8; 20]); @@ -855,6 +856,7 @@ fn deposits_below_minimum_are_rejected() { ingress_fee: 0, action: DepositAction::LiquidityProvision { lp_account: LP_ACCOUNT }, channel_id, + origin_type: DepositOriginType::DepositChannel, }, )); }); @@ -1790,6 +1792,43 @@ fn do_not_process_more_ccm_swaps_than_allowed_by_limit() { }); } +fn submit_vault_swap_request( + input_asset: Asset, + output_asset: Asset, + deposit_amount: AssetAmount, + deposit_address: H160, + destination_address: EncodedAddress, + deposit_metadata: Option, + tx_id: H256, + deposit_details: DepositDetails, + broker_fee: Beneficiary, + affiliate_fees: Affiliates, + refund_params: ChannelRefundParameters, + dca_params: Option, + boost_fee: BasisPoints, +) -> DispatchResult { + IngressEgress::vault_swap_request( + RuntimeOrigin::root(), + 0, + vec![VaultDepositWitness { + input_asset: input_asset.try_into().unwrap(), + deposit_address, + channel_id: 0, + deposit_amount, + deposit_details, + output_asset, + destination_address, + deposit_metadata, + tx_id, + broker_fee, + affiliate_fees, + refund_params, + dca_params, + boost_fee, + }], + ) +} + #[test] fn can_request_swap_via_extrinsic() { const INPUT_ASSET: Asset = Asset::Eth; @@ -1799,20 +1838,20 @@ fn can_request_swap_via_extrinsic() { let output_address = ForeignChainAddress::Eth([1; 20].into()); new_test_ext().execute_with(|| { - assert_ok!(IngressEgress::vault_swap_request( - RuntimeOrigin::root(), - INPUT_ASSET.try_into().unwrap(), + assert_ok!(submit_vault_swap_request( + INPUT_ASSET, OUTPUT_ASSET, INPUT_AMOUNT, + Default::default(), MockAddressConverter::to_encoded_address(output_address.clone()), None, Default::default(), - Box::new(DepositDetails { tx_hashes: None }), + DepositDetails { tx_hashes: None }, Beneficiary { account: BROKER, bps: 0 }, Default::default(), - Box::new(ETH_REFUND_PARAMS), + ETH_REFUND_PARAMS, None, - 0, + 0 )); assert_eq!( @@ -1855,21 +1894,21 @@ fn vault_swaps_support_affiliate_fees() { // have no effect on the test: MockAffiliateRegistry::register_affiliate(BROKER + 1, AFFILIATE_2, AFFILIATE_SHORT_1); - assert_ok!(IngressEgress::vault_swap_request( - RuntimeOrigin::root(), - INPUT_ASSET.try_into().unwrap(), + assert_ok!(submit_vault_swap_request( + INPUT_ASSET, OUTPUT_ASSET, INPUT_AMOUNT, + Default::default(), MockAddressConverter::to_encoded_address(output_address.clone()), None, Default::default(), - Box::new(DepositDetails { tx_hashes: None }), + DepositDetails { tx_hashes: None }, Beneficiary { account: BROKER, bps: BROKER_FEE }, bounded_vec![ Beneficiary { account: AFFILIATE_SHORT_1, bps: AFFILIATE_FEE }, Beneficiary { account: AFFILIATE_SHORT_2, bps: AFFILIATE_FEE } ], - Box::new(ETH_REFUND_PARAMS), + ETH_REFUND_PARAMS, None, 0 )); @@ -1913,18 +1952,18 @@ fn charge_no_broker_fees_on_unknown_primary_broker() { let output_address = ForeignChainAddress::Eth([1; 20].into()); - assert_ok!(IngressEgress::vault_swap_request( - RuntimeOrigin::root(), - INPUT_ASSET.try_into().unwrap(), + assert_ok!(submit_vault_swap_request( + INPUT_ASSET, OUTPUT_ASSET, INPUT_AMOUNT, + Default::default(), MockAddressConverter::to_encoded_address(output_address.clone()), None, Default::default(), - Box::new(DepositDetails { tx_hashes: None }), + DepositDetails { tx_hashes: None }, Beneficiary { account: NOT_A_BROKER, bps: BROKER_FEE }, Default::default(), - Box::new(ETH_REFUND_PARAMS), + ETH_REFUND_PARAMS, None, 0 )); @@ -1959,7 +1998,7 @@ fn can_request_ccm_swap_via_extrinsic() { let ccm_deposit_metadata = CcmDepositMetadata { source_chain: ForeignChain::Ethereum, - source_address: Some(ForeignChainAddress::Eth([0xcf; 20].into())), + source_address: None, channel_metadata: CcmChannelMetadata { message: vec![0x01].try_into().unwrap(), gas_budget: 1_000, @@ -1970,18 +2009,18 @@ fn can_request_ccm_swap_via_extrinsic() { let output_address = ForeignChainAddress::Eth([1; 20].into()); new_test_ext().execute_with(|| { - assert_ok!(IngressEgress::vault_swap_request( - RuntimeOrigin::root(), - INPUT_ASSET.try_into().unwrap(), + assert_ok!(submit_vault_swap_request( + INPUT_ASSET, OUTPUT_ASSET, 10_000, + Default::default(), MockAddressConverter::to_encoded_address(output_address.clone()), Some(ccm_deposit_metadata.clone()), Default::default(), - Box::new(DepositDetails { tx_hashes: None }), + DepositDetails { tx_hashes: None }, Beneficiary { account: BROKER, bps: 0 }, Default::default(), - Box::new(ETH_REFUND_PARAMS), + ETH_REFUND_PARAMS, None, 0 )); @@ -2020,41 +2059,41 @@ fn rejects_invalid_swap_by_witnesser() { MockAddressConverter::to_encoded_address(ForeignChainAddress::Btc(script_pubkey)); // Is valid Bitcoin address, but asset is Dot, so not compatible - assert_ok!(IngressEgress::vault_swap_request( - RuntimeOrigin::root(), - EthAsset::Eth, + assert_ok!(submit_vault_swap_request( + Asset::Eth, Asset::Dot, 10000, + Default::default(), btc_encoded_address, None, Default::default(), - Box::new(DepositDetails { tx_hashes: None }), + DepositDetails { tx_hashes: None }, Beneficiary { account: 0, bps: 0 }, Default::default(), - Box::new(ETH_REFUND_PARAMS), + ETH_REFUND_PARAMS, None, 0 - ),); + )); // No swap request created -> the call was ignored assert!(MockSwapRequestHandler::::get_swap_requests().is_empty()); // Invalid BTC address: - assert_ok!(IngressEgress::vault_swap_request( - RuntimeOrigin::root(), - EthAsset::Eth, + assert_ok!(submit_vault_swap_request( + Asset::Eth, Asset::Btc, 10000, + Default::default(), EncodedAddress::Btc(vec![0x41, 0x80, 0x41]), None, Default::default(), - Box::new(DepositDetails { tx_hashes: None }), + DepositDetails { tx_hashes: None }, Beneficiary { account: 0, bps: 0 }, Default::default(), - Box::new(ETH_REFUND_PARAMS), + ETH_REFUND_PARAMS, None, 0 - ),); + )); assert!(MockSwapRequestHandler::::get_swap_requests().is_empty()); }); @@ -2076,18 +2115,18 @@ fn failed_ccm_deposit_can_deposit_event() { new_test_ext().execute_with(|| { // CCM is not supported for Dot: - assert_ok!(IngressEgress::vault_swap_request( - RuntimeOrigin::root(), - EthAsset::Flip, + assert_ok!(submit_vault_swap_request( + Asset::Flip, Asset::Dot, 10_000, + Default::default(), EncodedAddress::Dot(Default::default()), Some(ccm_deposit_metadata.clone()), Default::default(), - Box::new(DepositDetails { tx_hashes: None }), + DepositDetails { tx_hashes: None }, Beneficiary { account: 0, bps: 0 }, Default::default(), - Box::new(ETH_REFUND_PARAMS), + ETH_REFUND_PARAMS, None, 0 )); @@ -2103,18 +2142,18 @@ fn failed_ccm_deposit_can_deposit_event() { System::reset_events(); // Insufficient deposit amount: - assert_ok!(IngressEgress::vault_swap_request( - RuntimeOrigin::root(), - EthAsset::Flip, + assert_ok!(submit_vault_swap_request( + Asset::Flip, Asset::Eth, GAS_BUDGET - 1, + Default::default(), EncodedAddress::Eth(Default::default()), Some(ccm_deposit_metadata), Default::default(), - Box::new(DepositDetails { tx_hashes: None }), + DepositDetails { tx_hashes: None }, Beneficiary { account: 0, bps: 0 }, Default::default(), - Box::new(ETH_REFUND_PARAMS), + ETH_REFUND_PARAMS, None, 0 )); diff --git a/state-chain/pallets/cf-ingress-egress/src/tests/boost.rs b/state-chain/pallets/cf-ingress-egress/src/tests/boost.rs index 46b32a561f..5da283efe5 100644 --- a/state-chain/pallets/cf-ingress-egress/src/tests/boost.rs +++ b/state-chain/pallets/cf-ingress-egress/src/tests/boost.rs @@ -1,6 +1,6 @@ use super::*; -use cf_chains::FeeEstimationApi; +use cf_chains::{DepositOriginType, FeeEstimationApi}; use cf_primitives::{AssetAmount, BasisPoints, PrewitnessedDepositId}; use cf_test_utilities::assert_event_sequence; use cf_traits::{ @@ -227,6 +227,7 @@ fn basic_passive_boosting() { ingress_fee: INGRESS_FEE, boost_fee: POOL_1_FEE + POOL_2_FEE, action: DepositAction::LiquidityProvision { lp_account: LP_ACCOUNT }, + origin_type: DepositOriginType::DepositChannel, })); assert_boosted(deposit_address, prewitnessed_deposit_id, [TIER_5_BPS, TIER_10_BPS]); @@ -254,6 +255,7 @@ fn basic_passive_boosting() { ingress_fee: 0, action: DepositAction::BoostersCredited { prewitnessed_deposit_id }, channel_id, + origin_type: DepositOriginType::DepositChannel, })); assert_eq!(get_available_amount(ASSET, TIER_5_BPS), BOOSTER_AMOUNT_1 + POOL_1_FEE); @@ -989,3 +991,172 @@ fn test_create_boost_pools() { ); }); } + +mod vault_swaps { + + use crate::BoostedVaultTxs; + + use super::*; + + #[test] + fn vault_swap_boosting() { + new_test_ext().execute_with(|| { + let output_address = ForeignChainAddress::Eth([1; 20].into()); + + let block_height = 10; + let deposit_address = [1; 20].into(); + + const BOOSTER_AMOUNT: AssetAmount = 500_000_000; + const DEPOSIT_AMOUNT: AssetAmount = 100_000_000; + const INPUT_ASSET: Asset = Asset::Eth; + const OUTPUT_ASSET: Asset = Asset::Flip; + + const BOOST_FEE: AssetAmount = DEPOSIT_AMOUNT * TIER_5_BPS as u128 / 10_000; + const PREWITNESS_DEPOSIT_ID: PrewitnessedDepositId = 1; + const CHANNEL_ID: ChannelId = 1; + + setup(); + + assert_ok!(IngressEgress::add_boost_funds( + RuntimeOrigin::signed(BOOSTER_1), + EthAsset::Eth, + BOOSTER_AMOUNT, + TIER_5_BPS + )); + + let tx_id = [9u8; 32].into(); + + // Initially tx is not recorded as boosted + assert!(!BoostedVaultTxs::::contains_key(tx_id)); + + let deposit = VaultDepositWitness { + input_asset: INPUT_ASSET.try_into().unwrap(), + deposit_address, + channel_id: CHANNEL_ID, + deposit_amount: DEPOSIT_AMOUNT, + deposit_details: Default::default(), + output_asset: OUTPUT_ASSET, + destination_address: MockAddressConverter::to_encoded_address( + output_address.clone(), + ), + deposit_metadata: None, + tx_id, + broker_fee: Beneficiary { account: BROKER, bps: 5 }, + affiliate_fees: Default::default(), + refund_params: ChannelRefundParameters { + retry_duration: 2, + refund_address: ForeignChainAddress::Eth([2; 20].into()), + min_price: Default::default(), + }, + dca_params: None, + boost_fee: 5, + }; + + // Prewitnessing a deposit for the first time should result in a boost: + { + IngressEgress::process_vault_swap_request_prewitness(block_height, deposit.clone()); + assert_eq!(PrewitnessedDepositIdCounter::::get(), PREWITNESS_DEPOSIT_ID); + + assert_eq!( + BoostPools::::get(EthAsset::Eth, TIER_5_BPS) + .unwrap() + .get_pending_boost_ids() + .len(), + 1 + ); + + assert_eq!( + MockSwapRequestHandler::::get_swap_requests(), + vec![MockSwapRequest { + input_asset: INPUT_ASSET, + output_asset: OUTPUT_ASSET, + // Note that ingress fee is not charged: + input_amount: DEPOSIT_AMOUNT - BOOST_FEE, + swap_type: SwapRequestType::Regular { output_address }, + broker_fees: bounded_vec![Beneficiary { account: BROKER, bps: 5 }], + origin: SwapOrigin::Vault { tx_id: TransactionInIdForAnyChain::Evm(tx_id) }, + },] + ); + + assert_has_matching_event!( + Test, + RuntimeEvent::IngressEgress(Event::DepositBoosted { + prewitnessed_deposit_id: PREWITNESS_DEPOSIT_ID, + channel_id: CHANNEL_ID, + action: DepositAction::Swap { .. }, + .. + }) + ); + + // Now the tx is recorded as boosted + assert!(BoostedVaultTxs::::contains_key(tx_id)); + } + + // Prewitnessing the same deposit (e.g. due to a reorg) should not result in a second + // boost: + { + IngressEgress::process_vault_swap_request_prewitness(block_height, deposit.clone()); + + assert_eq!( + BoostPools::::get(EthAsset::Eth, TIER_5_BPS) + .unwrap() + .get_pending_boost_ids() + .len(), + 1 + ); + + assert_eq!(MockSwapRequestHandler::::get_swap_requests().len(), 1); + } + + // Prewitnessing a different deposit *should* result in a second boost: + { + let other_deposit = + VaultDepositWitness { tx_id: [10u8; 32].into(), ..deposit.clone() }; + IngressEgress::process_vault_swap_request_prewitness(block_height, other_deposit); + + assert_eq!( + BoostPools::::get(EthAsset::Eth, TIER_5_BPS) + .unwrap() + .get_pending_boost_ids() + .len(), + 2 + ); + + assert_eq!(MockSwapRequestHandler::::get_swap_requests().len(), 2); + } + + // Fully witnessing a boosted deposit should finalise boost: + { + IngressEgress::process_vault_swap_request_full_witness( + block_height, + deposit.clone(), + ); + + // No new swap is initiated: + assert_eq!(MockSwapRequestHandler::::get_swap_requests().len(), 2); + + assert_eq!( + BoostPools::::get(EthAsset::Eth, TIER_5_BPS) + .unwrap() + .get_pending_boost_ids() + .len(), + 1 + ); + + assert_has_matching_event!( + Test, + RuntimeEvent::IngressEgress(Event::DepositFinalised { + channel_id: CHANNEL_ID, + action: DepositAction::BoostersCredited { + prewitnessed_deposit_id: PREWITNESS_DEPOSIT_ID + }, + .. + }) + ); + + // Boost record for tx is removed: + assert!(!BoostedVaultTxs::::contains_key(tx_id)); + } + }); + } +} diff --git a/state-chain/runtime/src/lib.rs b/state-chain/runtime/src/lib.rs index 49da0c11b4..5595749a82 100644 --- a/state-chain/runtime/src/lib.rs +++ b/state-chain/runtime/src/lib.rs @@ -1924,39 +1924,15 @@ impl_runtime_apis! { let current_block_events = System::read_events_no_consensus(); for event in current_block_events { + #[allow(clippy::collapsible_match)] match *event { frame_system::EventRecord:: { event: RuntimeEvent::Witnesser(pallet_cf_witnesser::Event::Prewitnessed { call }), ..} => { match call { - RuntimeCall::EthereumIngressEgress(pallet_cf_ingress_egress::Call::vault_swap_request { - input_asset: swap_from, output_asset: swap_to, deposit_amount, .. - }) if from == swap_from.into() && to == swap_to => { - all_prewitnessed_swaps.push(deposit_amount); - } - RuntimeCall::ArbitrumIngressEgress(pallet_cf_ingress_egress::Call::vault_swap_request { - input_asset: swap_from, output_asset: swap_to, deposit_amount, .. - }) if from == swap_from.into() && to == swap_to => { - all_prewitnessed_swaps.push(deposit_amount); - } - RuntimeCall::EthereumIngressEgress(pallet_cf_ingress_egress::Call::process_deposits::<_, EthereumInstance> { - deposit_witnesses, .. - }) => { - all_prewitnessed_swaps.extend(filter_deposit_swaps::(from, to, deposit_witnesses)); - }, - RuntimeCall::ArbitrumIngressEgress(pallet_cf_ingress_egress::Call::process_deposits::<_, ArbitrumInstance> { - deposit_witnesses, .. - }) => { - all_prewitnessed_swaps.extend(filter_deposit_swaps::(from, to, deposit_witnesses)); - }, RuntimeCall::BitcoinIngressEgress(pallet_cf_ingress_egress::Call::process_deposits { deposit_witnesses, .. }) => { all_prewitnessed_swaps.extend(filter_deposit_swaps::(from, to, deposit_witnesses)); }, - RuntimeCall::PolkadotIngressEgress(pallet_cf_ingress_egress::Call::process_deposits { - deposit_witnesses, .. - }) => { - all_prewitnessed_swaps.extend(filter_deposit_swaps::(from, to, deposit_witnesses)); - }, _ => { // ignore, we only care about calls that trigger swaps. },