diff --git a/api/bin/chainflip-broker-api/src/main.rs b/api/bin/chainflip-broker-api/src/main.rs index f39bb9f91d..fb574e0315 100644 --- a/api/bin/chainflip-broker-api/src/main.rs +++ b/api/bin/chainflip-broker-api/src/main.rs @@ -1,13 +1,15 @@ use cf_utilities::{ health::{self, HealthCheckOptions}, + rpc::NumberOrHex, task_scope::{task_scope, Scope}, + try_parse_number_or_hex, }; use chainflip_api::{ self, primitives::{AccountRole, Affiliates, Asset, BasisPoints, CcmChannelMetadata, DcaParameters}, settings::StateChain, AccountId32, AddressString, BrokerApi, OperatorApi, RefundParameters, StateChainApi, - SwapDepositAddress, WithdrawFeesDetail, + SwapDepositAddress, SwapPayload, WithdrawFeesDetail, }; use clap::Parser; use custom_rpc::to_rpc_error; @@ -48,6 +50,20 @@ pub trait Rpc { asset: Asset, destination_address: AddressString, ) -> RpcResult; + + #[method(name = "request_swap_parameter_encoding", aliases = ["broker_requestSwapParameterEncoding"])] + async fn request_swap_parameter_encoding( + &self, + source_asset: Asset, + destination_asset: Asset, + destination_address: AddressString, + broker_commission: BasisPoints, + retry_duration: u32, + min_output_amount: NumberOrHex, + dca_parameters: Option, + boost_fee: Option, + affiliate_fees: Option>, + ) -> RpcResult; } pub struct RpcServerImpl { @@ -120,6 +136,36 @@ impl RpcServer for RpcServerImpl { .await .map_err(to_rpc_error)?) } + + async fn request_swap_parameter_encoding( + &self, + source_asset: Asset, + destination_asset: Asset, + destination_address: AddressString, + broker_commission: BasisPoints, + retry_duration: u32, + min_output_amount: NumberOrHex, + dca_parameters: Option, + boost_fee: Option, + affiliate_fees: Option>, + ) -> RpcResult { + Ok(self + .api + .broker_api() + .request_swap_parameter_encoding( + source_asset, + destination_asset, + destination_address, + retry_duration, + try_parse_number_or_hex(min_output_amount).map_err(to_rpc_error)?, + boost_fee, + dca_parameters, + broker_commission, + affiliate_fees, + ) + .await + .map_err(to_rpc_error)?) + } } #[derive(Parser, Debug, Clone, Default)] diff --git a/api/bin/chainflip-ingress-egress-tracker/src/witnessing/snapshots/chainflip_ingress_egress_tracker__witnessing__state_chain__tests__handle_deposit_calls-2.snap b/api/bin/chainflip-ingress-egress-tracker/src/witnessing/snapshots/chainflip_ingress_egress_tracker__witnessing__state_chain__tests__handle_deposit_calls-2.snap index 2bc2890ef3..024d4127f4 100644 --- a/api/bin/chainflip-ingress-egress-tracker/src/witnessing/snapshots/chainflip_ingress_egress_tracker__witnessing__state_chain__tests__handle_deposit_calls-2.snap +++ b/api/bin/chainflip-ingress-egress-tracker/src/witnessing/snapshots/chainflip_ingress_egress_tracker__witnessing__state_chain__tests__handle_deposit_calls-2.snap @@ -3,4 +3,4 @@ source: api/bin/chainflip-ingress-egress-tracker/src/witnessing/state_chain.rs assertion_line: 487 expression: "store.storage.get(format!(\"deposit:Polkadot:{}\", format!\n (\"0x{}\", hex ::\n encode(polkadot_account_id.aliased_ref()))).as_str()).unwrap()" --- -[{"amount":"0x64","asset":{"asset":"DOT","chain":"Polkadot"},"deposit_chain_block_height":1}] +[{"amount":"0x64","asset":{"asset":"DOT","chain":"Polkadot"},"deposit_chain_block_height":1,"deposit_details":{"extrinsic_index":1}}] diff --git a/api/bin/chainflip-ingress-egress-tracker/src/witnessing/snapshots/chainflip_ingress_egress_tracker__witnessing__state_chain__tests__handle_deposit_calls-3.snap b/api/bin/chainflip-ingress-egress-tracker/src/witnessing/snapshots/chainflip_ingress_egress_tracker__witnessing__state_chain__tests__handle_deposit_calls-3.snap index aafc0683b6..a12dc7e5ef 100644 --- a/api/bin/chainflip-ingress-egress-tracker/src/witnessing/snapshots/chainflip_ingress_egress_tracker__witnessing__state_chain__tests__handle_deposit_calls-3.snap +++ b/api/bin/chainflip-ingress-egress-tracker/src/witnessing/snapshots/chainflip_ingress_egress_tracker__witnessing__state_chain__tests__handle_deposit_calls-3.snap @@ -3,4 +3,4 @@ source: api/bin/chainflip-ingress-egress-tracker/src/witnessing/state_chain.rs assertion_line: 497 expression: "store.storage.get(format!(\"deposit:Ethereum:{}\",\n eth_address_str2.to_lowercase()).as_str()).unwrap()" --- -[{"amount":"0x64","asset":{"asset":"ETH","chain":"Ethereum"},"deposit_chain_block_height":1}] +[{"amount":"0x64","asset":{"asset":"ETH","chain":"Ethereum"},"deposit_chain_block_height":1,"deposit_details":null}] diff --git a/api/bin/chainflip-ingress-egress-tracker/src/witnessing/snapshots/chainflip_ingress_egress_tracker__witnessing__state_chain__tests__handle_deposit_calls-4.snap b/api/bin/chainflip-ingress-egress-tracker/src/witnessing/snapshots/chainflip_ingress_egress_tracker__witnessing__state_chain__tests__handle_deposit_calls-4.snap index 242c6f9a20..911ef47e1e 100644 --- a/api/bin/chainflip-ingress-egress-tracker/src/witnessing/snapshots/chainflip_ingress_egress_tracker__witnessing__state_chain__tests__handle_deposit_calls-4.snap +++ b/api/bin/chainflip-ingress-egress-tracker/src/witnessing/snapshots/chainflip_ingress_egress_tracker__witnessing__state_chain__tests__handle_deposit_calls-4.snap @@ -3,4 +3,4 @@ source: api/bin/chainflip-ingress-egress-tracker/src/witnessing/state_chain.rs assertion_line: 521 expression: "store.storage.get(format!(\"deposit:Ethereum:{}\",\n eth_address_str1.to_lowercase()).as_str()).unwrap()" --- -[{"amount":"0x64","asset":{"asset":"ETH","chain":"Ethereum"},"deposit_chain_block_height":1},{"amount":"0x1e8480","asset":{"asset":"ETH","chain":"Ethereum"},"deposit_chain_block_height":1}] +[{"amount":"0x64","asset":{"asset":"ETH","chain":"Ethereum"},"deposit_chain_block_height":1,"deposit_details":null},{"amount":"0x1e8480","asset":{"asset":"ETH","chain":"Ethereum"},"deposit_chain_block_height":1,"deposit_details":null}] diff --git a/api/bin/chainflip-ingress-egress-tracker/src/witnessing/snapshots/chainflip_ingress_egress_tracker__witnessing__state_chain__tests__handle_deposit_calls.snap b/api/bin/chainflip-ingress-egress-tracker/src/witnessing/snapshots/chainflip_ingress_egress_tracker__witnessing__state_chain__tests__handle_deposit_calls.snap index fa3c0c426d..417a687ccd 100644 --- a/api/bin/chainflip-ingress-egress-tracker/src/witnessing/snapshots/chainflip_ingress_egress_tracker__witnessing__state_chain__tests__handle_deposit_calls.snap +++ b/api/bin/chainflip-ingress-egress-tracker/src/witnessing/snapshots/chainflip_ingress_egress_tracker__witnessing__state_chain__tests__handle_deposit_calls.snap @@ -3,4 +3,4 @@ source: api/bin/chainflip-ingress-egress-tracker/src/witnessing/state_chain.rs assertion_line: 483 expression: "store.storage.get(format!(\"deposit:Ethereum:{}\",\n eth_address_str1.to_lowercase()).as_str()).unwrap()" --- -[{"amount":"0x64","asset":{"asset":"ETH","chain":"Ethereum"},"deposit_chain_block_height":1}] +[{"amount":"0x64","asset":{"asset":"ETH","chain":"Ethereum"},"deposit_chain_block_height":1,"deposit_details":null}] diff --git a/api/bin/chainflip-ingress-egress-tracker/src/witnessing/state_chain.rs b/api/bin/chainflip-ingress-egress-tracker/src/witnessing/state_chain.rs index eef0dbb7ad..a1ec67c1be 100644 --- a/api/bin/chainflip-ingress-egress-tracker/src/witnessing/state_chain.rs +++ b/api/bin/chainflip-ingress-egress-tracker/src/witnessing/state_chain.rs @@ -4,7 +4,7 @@ use crate::{ }; use cf_chains::{ address::ToHumanreadableAddress, - dot::PolkadotTransactionId, + dot::{PolkadotExtrinsicIndex, PolkadotTransactionId}, evm::{SchnorrVerificationComponents, H256}, AnyChain, Arbitrum, Bitcoin, Chain, Ethereum, Polkadot, }; @@ -58,6 +58,15 @@ enum TransactionId { Arbitrum { signature: SchnorrVerificationComponents }, } +#[derive(Serialize)] +#[serde(untagged)] +enum DepositDetails { + Bitcoin { tx_id: H256, vout: u32 }, + Ethereum { tx_hashes: Vec }, + Polkadot { extrinsic_index: PolkadotExtrinsicIndex }, + Arbitrum { tx_hashes: Vec }, +} + #[derive(Serialize)] #[serde(untagged)] enum WitnessInformation { @@ -67,6 +76,7 @@ enum WitnessInformation { deposit_address: String, amount: NumberOrHex, asset: cf_chains::assets::any::Asset, + deposit_details: Option, }, Broadcast { #[serde(skip_serializing)] @@ -121,6 +131,10 @@ impl From> for WitnessInformation { deposit_address: hex_encode_bytes(value.deposit_address.as_bytes()), amount: value.amount.into(), asset: value.asset.into(), + deposit_details: value + .deposit_details + .tx_hashes + .map(|tx_hashes| DepositDetails::Ethereum { tx_hashes }), } } } @@ -132,6 +146,10 @@ impl From> for WitnessInformation { deposit_address: value.deposit_address.to_humanreadable(network), amount: value.amount.into(), asset: value.asset.into(), + deposit_details: Some(DepositDetails::Bitcoin { + tx_id: value.deposit_details.utxo_id.tx_id, + vout: value.deposit_details.utxo_id.vout, + }), } } } @@ -143,6 +161,9 @@ impl From> for WitnessInformation { deposit_address: hex_encode_bytes(value.deposit_address.aliased_ref()), amount: value.amount.into(), asset: value.asset.into(), + deposit_details: Some(DepositDetails::Polkadot { + extrinsic_index: value.deposit_details, + }), } } } @@ -154,6 +175,10 @@ impl From> for WitnessInformation { deposit_address: hex_encode_bytes(value.deposit_address.as_bytes()), amount: value.amount.into(), asset: value.asset.into(), + deposit_details: value + .deposit_details + .tx_hashes + .map(|tx_hashes| DepositDetails::Arbitrum { tx_hashes }), } } } @@ -208,15 +233,8 @@ where deposit_witnesses, block_height, }) => - for witness in deposit_witnesses as Vec> { - store - .save_to_array(&WitnessInformation::from(( - witness, - block_height, - chainflip_network, - ))) - .await?; - }, + save_deposit_witnesses(store, deposit_witnesses, block_height, chainflip_network) + .await?, ArbitrumIngressEgress(IngressEgressCall::process_deposits { deposit_witnesses, block_height, diff --git a/api/lib/src/lib.rs b/api/lib/src/lib.rs index 033d691f2a..22bbe80436 100644 --- a/api/lib/src/lib.rs +++ b/api/lib/src/lib.rs @@ -4,13 +4,16 @@ use anyhow::{anyhow, bail, Context, Result}; use async_trait::async_trait; use cf_chains::{ address::{try_from_encoded_address, EncodedAddress}, + btc::smart_contract_encoding::{ + encode_swap_params_in_nulldata_utxo, SharedCfParameters, UtxoEncodedData, + }, dot::PolkadotAccountId, evm::to_evm_address, sol::SolAddress, CcmChannelMetadata, ChannelRefundParametersGeneric, ForeignChain, ForeignChainAddress, }; pub use cf_primitives::{AccountRole, Affiliates, Asset, BasisPoints, ChannelId, SemVer}; -use cf_primitives::{BlockNumber, DcaParameters, NetworkEnvironment, Price}; +use cf_primitives::{AssetAmount, BlockNumber, DcaParameters, NetworkEnvironment, Price}; use pallet_cf_account_roles::MAX_LENGTH_FOR_VANITY_NAME; use pallet_cf_governance::ExecutionMode; use serde::{Deserialize, Serialize}; @@ -107,6 +110,11 @@ pub async fn request_block( .ok_or_else(|| anyhow!("unknown block hash")) } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum SwapPayload { + Bitcoin { nulldata_utxo: Bytes }, +} + pub struct StateChainApi { pub state_chain_client: Arc, } @@ -321,8 +329,8 @@ impl AddressString { .map_err(|_| anyhow!("Failed to parse address")) } - pub fn from_encoded_address(address: &EncodedAddress) -> Self { - Self(address.to_string()) + pub fn from_encoded_address>(address: T) -> Self { + Self(address.borrow().to_string()) } } @@ -515,6 +523,78 @@ pub trait BrokerApi: SignedExtrinsicApi + StorageApi + Sized + Send + Sync + 'st self.simple_submission_with_dry_run(pallet_cf_swapping::Call::deregister_as_broker {}) .await } + + async fn request_swap_parameter_encoding( + &self, + input_asset: Asset, + output_asset: Asset, + output_address: AddressString, + retry_duration: BlockNumber, + min_output_amount: AssetAmount, + boost_fee: Option, + dca_parameters: Option, + broker_commission: BasisPoints, + affiliate_fees: Option>, + ) -> Result { + // Check if safe mode is active + let block_hash = self.base_rpc_api().latest_finalized_block_hash().await?; + let safe_mode = self + .storage_value::>( + block_hash, + ) + .await?; + if !safe_mode.swapping.swaps_enabled { + bail!("Safe mode is active. Swaps are disabled."); + } + + // Validate params + frame_support::ensure!( + broker_commission == 0 && affiliate_fees.map_or(true, |fees| fees.is_empty()), + anyhow!("Broker/Affi fees are not yet supported for vault swaps. Request a deposit address or remove the broker fees.") + ); + self.base_rpc_api() + .validate_refund_params(retry_duration, Some(block_hash)) + .await?; + if let Some(params) = dca_parameters.as_ref() { + self.base_rpc_api() + .validate_dca_params( + params.number_of_chunks, + params.chunk_interval, + Some(block_hash), + ) + .await?; + } + + // Encode swap + match ForeignChain::from(input_asset) { + ForeignChain::Bitcoin => { + let params = UtxoEncodedData { + output_asset, + output_address: output_address + .try_parse_to_encoded_address(output_asset.into())?, + parameters: SharedCfParameters { + retry_duration: retry_duration.try_into()?, + min_output_amount, + number_of_chunks: dca_parameters + .as_ref() + .map(|params| params.number_of_chunks) + .unwrap_or(1) + .try_into()?, + chunk_interval: dca_parameters + .as_ref() + .map(|params| params.chunk_interval) + .unwrap_or(2) + .try_into()?, + boost_fee: boost_fee.unwrap_or_default().try_into()?, + }, + }; + Ok(SwapPayload::Bitcoin { + nulldata_utxo: encode_swap_params_in_nulldata_utxo(params).raw().into(), + }) + }, + _ => bail!("Unsupported input asset"), + } + } } #[async_trait] diff --git a/bouncer/shared/swapping.ts b/bouncer/shared/swapping.ts index ba65c7728f..425a6ab6c4 100644 --- a/bouncer/shared/swapping.ts +++ b/bouncer/shared/swapping.ts @@ -89,6 +89,8 @@ function newSolanaCfParameters(maxAccounts: number) { cfReceiver.is_writable ? 1 : 0, ]); + const fallbackAddrBytes = new PublicKey(getContractAddress('Solana', 'FALLBACK')).toBytes(); + const remainingAccounts = []; const numRemainingAccounts = Math.floor(Math.random() * maxAccounts); @@ -104,6 +106,7 @@ function newSolanaCfParameters(maxAccounts: number) { // Inserted by the codec::Encode 4 * remainingAccounts.length, ...remainingAccounts.flatMap((account) => Array.from(account)), + ...fallbackAddrBytes, ]); return arrayToHexString(cfParameters); diff --git a/bouncer/shared/utils.ts b/bouncer/shared/utils.ts index 8ce6075902..fa090d8ef3 100644 --- a/bouncer/shared/utils.ts +++ b/bouncer/shared/utils.ts @@ -105,6 +105,9 @@ export function getContractAddress(chain: Chain, contract: string): string { return '8pBPaVfTAcjLeNfC187Fkvi9b1XEFhRNJ95BQXXVksmH'; case 'SWAP_ENDPOINT': return '35uYgHdfZQT4kHkaaXQ6ZdCkK5LFrsk43btTLbGCRCNT'; + case 'FALLBACK': + /// 3wDSVR6YSRDFiWdwsnZZRjAKHKvsmb4fouYVqoBt5yd4vSrY7aQdvtLJKMvEb3AMWGD5fxunfdotvwPwnSChWMWx + return 'CFf51BPWnybvgbZrxy61s4SCCvEohBC7achsPLuoACUG'; default: throw new Error(`Unsupported contract: ${contract}`); } @@ -723,7 +726,7 @@ export async function observeSolanaCcmEvent( const remainingAccountSize = publicKeySize + 1; // Extra byte for the encoded length - const remainingAccountsBytes = cfParameters.slice(remainingAccountSize + 1); + const remainingAccountsBytes = cfParameters.slice(remainingAccountSize + 1, -publicKeySize); const remainingAccounts = []; const remainingIsWritable = []; @@ -737,7 +740,10 @@ export async function observeSolanaCcmEvent( remainingIsWritable.push(Boolean(isWritable)); } - return { remainingAccounts, remainingIsWritable }; + // fallback account + const fallbackAccount = cfParameters.slice(-publicKeySize); + + return { remainingAccounts, remainingIsWritable, fallbackAccount }; } const connection = getSolConnection(); diff --git a/engine/src/state_chain_observer/client/base_rpc_api.rs b/engine/src/state_chain_observer/client/base_rpc_api.rs index 8cd1f686f3..e105cb4994 100644 --- a/engine/src/state_chain_observer/client/base_rpc_api.rs +++ b/engine/src/state_chain_observer/client/base_rpc_api.rs @@ -161,6 +161,19 @@ pub trait BaseRpcApi { params: Option>, unsub: &str, ) -> RpcResult>>; + + async fn validate_refund_params( + &self, + retry_duration: u32, + block_hash: Option, + ) -> RpcResult<()>; + + async fn validate_dca_params( + &self, + number_of_chunks: u32, + chunk_interval: u32, + block_hash: Option, + ) -> RpcResult<()>; } pub struct BaseRpcClient { @@ -321,6 +334,29 @@ impl BaseRpcApi for BaseRpcClient, + ) -> RpcResult<()> { + self.raw_rpc_client + .cf_validate_refund_params(retry_duration, block_hash) + .await + .map_err(to_rpc_error) + } + + async fn validate_dca_params( + &self, + number_of_chunks: u32, + chunk_interval: u32, + block_hash: Option, + ) -> RpcResult<()> { + self.raw_rpc_client + .cf_validate_dca_params(number_of_chunks, chunk_interval, block_hash) + .await + .map_err(to_rpc_error) + } } struct Params(Option>); diff --git a/engine/src/witness/arb.rs b/engine/src/witness/arb.rs index e6976c1edb..6e9ad67ff3 100644 --- a/engine/src/witness/arb.rs +++ b/engine/src/witness/arb.rs @@ -2,7 +2,7 @@ mod chain_tracking; use std::{collections::HashMap, sync::Arc}; -use cf_chains::Arbitrum; +use cf_chains::{evm::DepositDetails, Arbitrum}; use cf_primitives::EpochIndex; use cf_utilities::task_scope::Scope; use futures_core::Future; @@ -207,6 +207,13 @@ impl super::evm::vault::IngressCallBuilder for ArbCallBuilder { deposit_amount, destination_address, tx_hash, + deposit_details: Box::new(DepositDetails { + tx_hashes: Some(vec![tx_hash.into()]), + }), + // TODO: use real parameters when we can decode them + boost_fee: 0, + dca_params: None, + refund_params: None, } }, ) diff --git a/engine/src/witness/btc.rs b/engine/src/witness/btc.rs index 6e1fce6bd2..0db09eaabd 100644 --- a/engine/src/witness/btc.rs +++ b/engine/src/witness/btc.rs @@ -1,6 +1,6 @@ mod chain_tracking; mod deposits; -mod smart_contract; +pub mod smart_contract; pub mod source; use crate::{ @@ -212,15 +212,15 @@ mod tests { let txs = vec![ fake_transaction(vec![], Some(Amount::from_sat(FEE_0))), fake_transaction( - fake_verbose_vouts(vec![(2324, vec![0, 32, 121, 9])]), + fake_verbose_vouts(vec![(2324, &DepositAddress::new([0; 32], 0))]), Some(Amount::from_sat(FEE_1)), ), fake_transaction( - fake_verbose_vouts(vec![(232232, vec![32, 32, 121, 9])]), + fake_verbose_vouts(vec![(232232, &DepositAddress::new([32; 32], 0))]), Some(Amount::from_sat(FEE_2)), ), fake_transaction( - fake_verbose_vouts(vec![(232232, vec![32, 32, 121, 9])]), + fake_verbose_vouts(vec![(232232, &DepositAddress::new([32; 32], 0))]), Some(Amount::from_sat(FEE_3)), ), ]; diff --git a/engine/src/witness/btc/deposits.rs b/engine/src/witness/btc/deposits.rs index e85bb15c26..ba5734914e 100644 --- a/engine/src/witness/btc/deposits.rs +++ b/engine/src/witness/btc/deposits.rs @@ -19,7 +19,7 @@ use crate::{ use bitcoin::{hashes::Hash, BlockHash}; use cf_chains::{ assets::btc, - btc::{ScriptPubkey, UtxoId}, + btc::{deposit_address::DepositAddress, BtcDepositDetails, UtxoId}, Bitcoin, }; @@ -55,12 +55,37 @@ impl ChunkedByVaultBuilder { self.then(move |epoch, header| { let process_call = process_call.clone(); async move { + let vault_addresses = { + use cf_chains::btc::{ + deposit_address::DepositAddress, AggKey, CHANGE_ADDRESS_SALT, + }; + + let key: &AggKey = &epoch.info.0; + + let maybe_previous_vault_address = + key.previous.map(|key| DepositAddress::new(key, CHANGE_ADDRESS_SALT)); + let current_vault_address = + DepositAddress::new(key.current, CHANGE_ADDRESS_SALT); + + [current_vault_address].into_iter().chain(maybe_previous_vault_address) + }; + // TODO: Make addresses a Map of some kind? - let (((), txs), addresses) = header.data; + let (((), txs), deposit_channels) = header.data; + + for vault_address in vault_addresses { + for tx in &txs { + if let Some(call) = + super::smart_contract::try_extract_contract_call(tx, &vault_address) + { + process_call(call.into(), epoch.index).await; + } + } + } - let script_addresses = script_addresses(addresses); + let deposit_addresses = map_script_addresses(deposit_channels); - let deposit_witnesses = deposit_witnesses(&txs, &script_addresses); + let deposit_witnesses = deposit_witnesses(&txs, &deposit_addresses); // Submit all deposit witnesses for the block. if !deposit_witnesses.is_empty() { @@ -82,19 +107,22 @@ impl ChunkedByVaultBuilder { fn deposit_witnesses( txs: &[VerboseTransaction], - script_addresses: &HashMap, ScriptPubkey>, + deposit_addresses: &HashMap, DepositAddress>, ) -> Vec> { txs.iter() .flat_map(|tx| { Iterator::zip(0.., &tx.vout) .filter(|(_vout, tx_out)| tx_out.value.to_sat() > 0) .filter_map(|(vout, tx_out)| { - script_addresses.get(tx_out.script_pubkey.as_bytes()).map(|bitcoin_script| { + deposit_addresses.get(tx_out.script_pubkey.as_bytes()).map(|deposit_address| { DepositWitness:: { - deposit_address: bitcoin_script.clone(), + deposit_address: deposit_address.script_pubkey(), asset: btc::Asset::Btc, amount: tx_out.value.to_sat(), - deposit_details: UtxoId { tx_id: tx.txid.to_byte_array().into(), vout }, + deposit_details: BtcDepositDetails { + utxo_id: UtxoId { tx_id: tx.txid.to_byte_array().into(), vout }, + deposit_address: deposit_address.clone(), + }, } }) }) @@ -114,15 +142,17 @@ fn deposit_witnesses( .collect() } -fn script_addresses( - addresses: Vec>, -) -> HashMap, ScriptPubkey> { - addresses +fn map_script_addresses( + deposit_channels: Vec>, +) -> HashMap, DepositAddress> { + deposit_channels .into_iter() .map(|channel| { assert_eq!(channel.deposit_channel.asset, btc::Asset::Btc); + let deposit_address = channel.deposit_channel.state; let script_pubkey = channel.deposit_channel.address; - (script_pubkey.bytes(), script_pubkey) + + (script_pubkey.bytes(), deposit_address) }) .collect() } @@ -137,10 +167,7 @@ pub mod tests { absolute::{Height, LockTime}, Amount, ScriptBuf, Txid, }; - use cf_chains::{ - btc::{deposit_address::DepositAddress, ScriptPubkey}, - DepositChannel, - }; + use cf_chains::{btc::deposit_address::DepositAddress, DepositChannel}; use pallet_cf_ingress_egress::{BoostStatus, ChannelAction}; use rand::{seq::SliceRandom, Rng, SeedableRng}; use sp_runtime::AccountId32; @@ -164,16 +191,16 @@ pub mod tests { } fn fake_details( - address: ScriptPubkey, + deposit_address: DepositAddress, ) -> DepositChannelDetails { DepositChannelDetails::<_, BitcoinInstance> { opened_at: 1, expires_at: 10, deposit_channel: DepositChannel { channel_id: 1, - address, + address: deposit_address.script_pubkey(), asset: btc::Asset::Btc, - state: DepositAddress::new([0; 32], 1), + state: deposit_address, }, action: ChannelAction::::LiquidityProvision { lp_account: AccountId32::new([0xab; 32]), @@ -183,14 +210,16 @@ pub mod tests { } } - pub fn fake_verbose_vouts(vals_and_scripts: Vec<(u64, Vec)>) -> Vec { - vals_and_scripts + pub fn fake_verbose_vouts( + amounts_and_addresses: Vec<(u64, &DepositAddress)>, + ) -> Vec { + amounts_and_addresses .into_iter() .enumerate() - .map(|(n, (value, script_bytes))| VerboseTxOut { - value: Amount::from_sat(value), + .map(|(n, (amount, address))| VerboseTxOut { + value: Amount::from_sat(amount), n: n as u64, - script_pubkey: ScriptBuf::from(script_bytes), + script_pubkey: ScriptBuf::from(address.script_pubkey().bytes()), }) .collect() } @@ -204,19 +233,16 @@ pub mod tests { #[test] fn filter_out_value_0() { - let btc_deposit_script: ScriptPubkey = DepositAddress::new([0; 32], 9).script_pubkey(); + let deposit_address = DepositAddress::new([0; 32], 9); const UTXO_WITNESSED_1: u64 = 2324; let txs = vec![fake_transaction( - fake_verbose_vouts(vec![ - (2324, btc_deposit_script.bytes()), - (0, btc_deposit_script.bytes()), - ]), + fake_verbose_vouts(vec![(2324, &deposit_address), (0, &deposit_address)]), None, )]; let deposit_witnesses = - deposit_witnesses(&txs, &script_addresses(vec![(fake_details(btc_deposit_script))])); + deposit_witnesses(&txs, &map_script_addresses(vec![(fake_details(deposit_address))])); assert_eq!(deposit_witnesses.len(), 1); assert_eq!(deposit_witnesses[0].amount, UTXO_WITNESSED_1); } @@ -227,15 +253,15 @@ pub mod tests { const UTXO_TO_DEPOSIT_2: u64 = 1234; const UTXO_TO_DEPOSIT_3: u64 = 2000; - let btc_deposit_script: ScriptPubkey = DepositAddress::new([0; 32], 9).script_pubkey(); + let deposit_address = DepositAddress::new([0; 32], 9); let txs = vec![ fake_transaction( fake_verbose_vouts(vec![ - (UTXO_TO_DEPOSIT_2, btc_deposit_script.bytes()), - (12223, vec![0, 32, 121, 9]), - (LARGEST_UTXO_TO_DEPOSIT, btc_deposit_script.bytes()), - (UTXO_TO_DEPOSIT_3, btc_deposit_script.bytes()), + (UTXO_TO_DEPOSIT_2, &deposit_address), + (12223, &DepositAddress::new([0; 32], 10)), + (LARGEST_UTXO_TO_DEPOSIT, &deposit_address), + (UTXO_TO_DEPOSIT_3, &deposit_address), ]), None, ), @@ -243,7 +269,7 @@ pub mod tests { ]; let deposit_witnesses = - deposit_witnesses(&txs, &script_addresses(vec![fake_details(btc_deposit_script)])); + deposit_witnesses(&txs, &map_script_addresses(vec![fake_details(deposit_address)])); assert_eq!(deposit_witnesses.len(), 1); assert_eq!(deposit_witnesses[0].amount, LARGEST_UTXO_TO_DEPOSIT); } @@ -254,16 +280,16 @@ pub mod tests { const UTXO_TO_DEPOSIT_2: u64 = 1234; const UTXO_FOR_SECOND_DEPOSIT: u64 = 2000; - let btc_deposit_script_1: ScriptPubkey = DepositAddress::new([0; 32], 9).script_pubkey(); - let btc_deposit_script_2: ScriptPubkey = DepositAddress::new([0; 32], 1232).script_pubkey(); + let deposit_address_1 = DepositAddress::new([0; 32], 9); + let deposit_address_2 = DepositAddress::new([0; 32], 1232); let txs = vec![ fake_transaction( fake_verbose_vouts(vec![ - (UTXO_TO_DEPOSIT_2, btc_deposit_script_1.bytes()), - (12223, vec![0, 32, 121, 9]), - (LARGEST_UTXO_TO_DEPOSIT, btc_deposit_script_1.bytes()), - (UTXO_FOR_SECOND_DEPOSIT, btc_deposit_script_2.bytes()), + (UTXO_TO_DEPOSIT_2, &deposit_address_1), + (12223, &DepositAddress::new([0; 32], 999)), + (LARGEST_UTXO_TO_DEPOSIT, &deposit_address_1), + (UTXO_FOR_SECOND_DEPOSIT, &deposit_address_2), ]), None, ), @@ -273,78 +299,82 @@ pub mod tests { let deposit_witnesses = deposit_witnesses( &txs, // watching 2 addresses - &script_addresses(vec![ - fake_details(btc_deposit_script_1.clone()), - fake_details(btc_deposit_script_2.clone()), + &map_script_addresses(vec![ + fake_details(deposit_address_1.clone()), + fake_details(deposit_address_2.clone()), ]), ); // We should have one deposit per address. assert_eq!(deposit_witnesses.len(), 2); assert_eq!(deposit_witnesses[0].amount, UTXO_FOR_SECOND_DEPOSIT); - assert_eq!(deposit_witnesses[0].deposit_address, btc_deposit_script_2); + assert_eq!(deposit_witnesses[0].deposit_address, deposit_address_2.script_pubkey()); assert_eq!(deposit_witnesses[1].amount, LARGEST_UTXO_TO_DEPOSIT); - assert_eq!(deposit_witnesses[1].deposit_address, btc_deposit_script_1); + assert_eq!(deposit_witnesses[1].deposit_address, deposit_address_1.script_pubkey()); } #[test] fn deposit_witnesses_ordering_is_consistent() { - let btc_deposit_script_1: ScriptPubkey = DepositAddress::new([0; 32], 9).script_pubkey(); - let btc_deposit_script_2: ScriptPubkey = DepositAddress::new([0; 32], 1232).script_pubkey(); + let address_1 = DepositAddress::new([0; 32], 9); + let address_2 = DepositAddress::new([0; 32], 1232); + DepositAddress::new([0; 32], 0); - let script_addresses = script_addresses(vec![ - fake_details(btc_deposit_script_1.clone()), - fake_details(btc_deposit_script_2.clone()), + let addresses = map_script_addresses(vec![ + fake_details(address_1.clone()), + fake_details(address_2.clone()), ]); let txs: Vec = vec![ fake_transaction( fake_verbose_vouts(vec![ - (3, btc_deposit_script_1.bytes()), - (5, vec![3]), - (7, btc_deposit_script_1.bytes()), - (11, btc_deposit_script_2.bytes()), + (3, &address_1), + (5, &DepositAddress::new([3; 32], 0)), + (7, &address_1), + (11, &address_2), ]), None, ), fake_transaction( fake_verbose_vouts(vec![ - (13, btc_deposit_script_2.bytes()), - (17, btc_deposit_script_2.bytes()), - (19, vec![5]), - (23, btc_deposit_script_1.bytes()), + (13, &address_2), + (17, &address_2), + (19, &DepositAddress::new([5; 32], 0)), + (23, &address_1), ]), None, ), fake_transaction( fake_verbose_vouts(vec![ - (13, btc_deposit_script_2.bytes()), - (19, vec![7]), - (23, btc_deposit_script_1.bytes()), + (13, &address_2), + (19, &DepositAddress::new([7; 32], 0)), + (23, &address_1), ]), None, ), fake_transaction( fake_verbose_vouts(vec![ - (29, btc_deposit_script_1.bytes()), - (31, btc_deposit_script_2.bytes()), - (37, vec![11]), + (29, &address_1), + (31, &address_2), + (37, &DepositAddress::new([11; 32], 0)), ]), None, ), fake_transaction( - fake_verbose_vouts(vec![(41, btc_deposit_script_2.bytes()), (43, vec![17])]), + fake_verbose_vouts(vec![(41, &address_2), (43, &DepositAddress::new([17; 32], 0))]), + None, + ), + fake_transaction( + fake_verbose_vouts(vec![(47, &address_1), (53, &DepositAddress::new([19; 32], 0))]), None, ), fake_transaction( - fake_verbose_vouts(vec![(47, btc_deposit_script_1.bytes()), (53, vec![19])]), + fake_verbose_vouts(vec![(61, &DepositAddress::new([23; 32], 0)), (59, &address_2)]), None, ), fake_transaction( - fake_verbose_vouts(vec![(61, vec![23]), (59, btc_deposit_script_2.bytes())]), + fake_verbose_vouts(vec![(67, &DepositAddress::new([29; 32], 0))]), None, ), - fake_transaction(fake_verbose_vouts(vec![(67, vec![29])]), None), ]; let mut rng = rand::rngs::StdRng::from_seed([3; 32]); @@ -352,36 +382,36 @@ pub mod tests { for _i in 0..10 { let n = rng.gen_range(0..txs.len()); let test_txs = txs.as_slice().choose_multiple(&mut rng, n).cloned().collect::>(); - assert!((0..10).map(|_| deposit_witnesses(&test_txs, &script_addresses)).all_equal()); + assert!((0..10).map(|_| deposit_witnesses(&test_txs, &addresses)).all_equal()); } } #[test] fn deposit_witnesses_several_diff_tx() { - let btc_deposit_script: ScriptPubkey = DepositAddress::new([0; 32], 9).script_pubkey(); + let address = DepositAddress::new([0; 32], 9); const UTXO_WITNESSED_1: u64 = 2324; const UTXO_WITNESSED_2: u64 = 1234; let txs = vec![ fake_transaction( fake_verbose_vouts(vec![ - (UTXO_WITNESSED_1, btc_deposit_script.bytes()), - (12223, vec![0, 32, 121, 9]), - (UTXO_WITNESSED_1 - 1, btc_deposit_script.bytes()), + (UTXO_WITNESSED_1, &address), + (12223, &DepositAddress::new([0; 32], 11)), + (UTXO_WITNESSED_1 - 1, &address), ]), None, ), fake_transaction( fake_verbose_vouts(vec![ - (UTXO_WITNESSED_2 - 10, btc_deposit_script.bytes()), - (UTXO_WITNESSED_2, btc_deposit_script.bytes()), + (UTXO_WITNESSED_2 - 10, &address), + (UTXO_WITNESSED_2, &address), ]), None, ), ]; let deposit_witnesses = - deposit_witnesses(&txs, &script_addresses(vec![fake_details(btc_deposit_script)])); + deposit_witnesses(&txs, &map_script_addresses(vec![fake_details(address)])); assert_eq!(deposit_witnesses.len(), 2); assert_eq!(deposit_witnesses[0].amount, UTXO_WITNESSED_1); assert_eq!(deposit_witnesses[1].amount, UTXO_WITNESSED_2); diff --git a/engine/src/witness/btc/smart_contract.rs b/engine/src/witness/btc/smart_contract.rs index 0df2affade..f44df90d8b 100644 --- a/engine/src/witness/btc/smart_contract.rs +++ b/engine/src/witness/btc/smart_contract.rs @@ -1,35 +1,23 @@ -use bitcoin::{opcodes::all::OP_RETURN, ScriptBuf}; +use bitcoin::{hashes::Hash as btcHash, opcodes::all::OP_RETURN, ScriptBuf}; use cf_amm::common::{bounded_sqrt_price, sqrt_price_to_price}; use cf_chains::{ - address::EncodedAddress, - btc::{smart_contract_encoding::UtxoEncodedData, ScriptPubkey}, + btc::{ + deposit_address::DepositAddress, smart_contract_encoding::UtxoEncodedData, + BtcDepositDetails, ScriptPubkey, UtxoId, + }, + ChannelRefundParameters, ForeignChainAddress, }; -use cf_primitives::{Asset, AssetAmount, Price}; +use cf_primitives::{Asset, DcaParameters}; use cf_utilities::SliceToArray; use codec::Decode; use itertools::Itertools; +use state_chain_runtime::BitcoinInstance; use crate::btc::rpc::VerboseTransaction; const OP_PUSHBYTES_75: u8 = 0x4b; const OP_PUSHDATA1: u8 = 0x4c; -#[derive(PartialEq, Debug)] -pub struct BtcContractCall { - output_asset: Asset, - deposit_amount: AssetAmount, - output_address: EncodedAddress, - // --- FoK --- - retry_duration: u16, - refund_address: ScriptPubkey, - min_price: Price, - // --- DCA --- - number_of_chunks: u16, - chunk_interval: u16, - // --- Boost --- - boost_fee: u8, -} - fn try_extract_utxo_encoded_data(script: &bitcoin::ScriptBuf) -> Option<&[u8]> { let bytes = script.as_script().as_bytes(); @@ -86,19 +74,20 @@ fn script_buf_to_script_pubkey(script: &ScriptBuf) -> Option { Some(pubkey) } -// Currently unused, but will be used by the deposit wintesser: -#[allow(dead_code)] +type BtcIngressEgressCall = + pallet_cf_ingress_egress::Call; + pub fn try_extract_contract_call( tx: &VerboseTransaction, - vault_address: ScriptPubkey, -) -> Option { + vault_address: &DepositAddress, +) -> 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; }; // First output must be a deposit into our vault: - if utxo_to_vault.script_pubkey.as_bytes() != vault_address.bytes() { + if utxo_to_vault.script_pubkey.as_bytes() != vault_address.script_pubkey().bytes() { return None; } @@ -135,16 +124,30 @@ pub fn try_extract_contract_call( deposit_amount.into(), )); - Some(BtcContractCall { - output_asset: data.output_asset, - deposit_amount: deposit_amount as AssetAmount, - output_address: data.output_address, - retry_duration: data.parameters.retry_duration, - refund_address, - min_price, - number_of_chunks: data.parameters.number_of_chunks, - chunk_interval: data.parameters.chunk_interval, - boost_fee: data.parameters.boost_fee, + let tx_id: [u8; 32] = tx.txid.to_byte_array(); + + Some(BtcIngressEgressCall::contract_swap_request { + from: Asset::Btc, + to: data.output_asset, + deposit_amount, + destination_address: data.output_address, + tx_hash: tx_id, + deposit_details: Box::new(BtcDepositDetails { + // we require the deposit to be the first UTXO + utxo_id: UtxoId { tx_id: tx_id.into(), vout: 0 }, + deposit_address: vault_address.clone(), + }), + refund_params: Some(ChannelRefundParameters { + retry_duration: data.parameters.retry_duration as u32, + refund_address: ForeignChainAddress::Btc(refund_address), + min_price, + }), + dca_params: Some(DcaParameters { + number_of_chunks: data.parameters.number_of_chunks as u32, + chunk_interval: data.parameters.chunk_interval as u32, + }), + // This is only to be checked in the pre-witnessed version + boost_fee: data.parameters.boost_fee as u16, }) } @@ -222,13 +225,12 @@ mod tests { fn test_extract_contract_call_from_tx() { use bitcoin::Amount; - const VAULT_PK_HASH: [u8; 20] = [7; 20]; const REFUND_PK_HASH: [u8; 20] = [8; 20]; + const DEPOSIT_AMOUNT: u64 = 1000; - // Addresses represented in both `ScriptPubkey` and `ScriptBuf` to satisfy interfaces: - let vault_pubkey = ScriptPubkey::P2PKH(VAULT_PK_HASH); - let vault_script = ScriptBuf::new_p2pkh(&PubkeyHash::from_byte_array(VAULT_PK_HASH)); - assert_eq!(vault_pubkey.bytes(), vault_script.to_bytes()); + // Addresses have different representations to satisfy interfaces: + let vault_deposit_address = DepositAddress::new([7; 32], 0); + let vault_script = ScriptBuf::from_bytes(vault_deposit_address.script_pubkey().bytes()); let refund_pubkey = ScriptPubkey::P2PKH(REFUND_PK_HASH); let refund_script = ScriptBuf::new_p2pkh(&PubkeyHash::from_byte_array(REFUND_PK_HASH)); @@ -238,11 +240,11 @@ mod tests { vec![ // A UTXO spending into our vault; VerboseTxOut { - value: Amount::from_sat(1000), + value: Amount::from_sat(DEPOSIT_AMOUNT), n: 0, script_pubkey: vault_script.clone(), }, - // A nulddata UTXO encoding some swap parameters: + // A nulldata UTXO encoding some swap parameters: VerboseTxOut { value: Amount::from_sat(0), n: 1, @@ -261,21 +263,31 @@ mod tests { ); assert_eq!( - try_extract_contract_call(&tx, vault_pubkey).unwrap(), - BtcContractCall { - output_asset: MOCK_SWAP_PARAMS.output_asset, - deposit_amount: 1000, - output_address: MOCK_SWAP_PARAMS.output_address.clone(), - retry_duration: MOCK_SWAP_PARAMS.parameters.retry_duration, - refund_address: refund_pubkey, - min_price: sqrt_price_to_price(bounded_sqrt_price( - MOCK_SWAP_PARAMS.parameters.min_output_amount.into(), - 1000.into(), - )), - number_of_chunks: MOCK_SWAP_PARAMS.parameters.number_of_chunks, - chunk_interval: MOCK_SWAP_PARAMS.parameters.chunk_interval, - boost_fee: MOCK_SWAP_PARAMS.parameters.boost_fee, - } + try_extract_contract_call(&tx, &vault_deposit_address), + Some(BtcIngressEgressCall::contract_swap_request { + from: Asset::Btc, + to: MOCK_SWAP_PARAMS.output_asset, + deposit_amount: DEPOSIT_AMOUNT, + destination_address: MOCK_SWAP_PARAMS.output_address.clone(), + tx_hash: tx.hash.to_byte_array(), + deposit_details: Box::new(BtcDepositDetails { + utxo_id: UtxoId { tx_id: tx.txid.to_byte_array().into(), vout: 0 }, + deposit_address: vault_deposit_address, + }), + refund_params: Some(ChannelRefundParameters { + retry_duration: MOCK_SWAP_PARAMS.parameters.retry_duration as u32, + 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 as u32, + chunk_interval: MOCK_SWAP_PARAMS.parameters.chunk_interval as u32, + }), + boost_fee: MOCK_SWAP_PARAMS.parameters.boost_fee as u16, + }) ); } diff --git a/engine/src/witness/eth.rs b/engine/src/witness/eth.rs index 07ca45495b..15dd7a4a4b 100644 --- a/engine/src/witness/eth.rs +++ b/engine/src/witness/eth.rs @@ -3,7 +3,7 @@ mod state_chain_gateway; use std::{collections::HashMap, sync::Arc}; -use cf_chains::Ethereum; +use cf_chains::{evm::DepositDetails, Ethereum}; use cf_primitives::{chains::assets::eth, EpochIndex}; use cf_utilities::task_scope::Scope; use futures_core::Future; @@ -252,6 +252,13 @@ impl super::evm::vault::IngressCallBuilder for EthCallBuilder { deposit_amount, destination_address, tx_hash, + deposit_details: Box::new(DepositDetails { + tx_hashes: Some(vec![tx_hash.into()]), + }), + // TODO: use real parameters when we can decode them + boost_fee: 0, + dca_params: None, + refund_params: None, } }, ) diff --git a/engine/src/witness/evm/evm_deposits.rs b/engine/src/witness/evm/evm_deposits.rs index 94c21be203..19a610e166 100644 --- a/engine/src/witness/evm/evm_deposits.rs +++ b/engine/src/witness/evm/evm_deposits.rs @@ -179,7 +179,7 @@ where } /// To ensure we don't double witness deposits, we use the following pseudo-code, implemented by -/// `eth_ingresses_at_block`. +/// [eth_ingresses_at_block]. /// /// if !address.hasContract: /// swap = address.balanceAtCurrentBlock - address.balanceAtPreviousBlock diff --git a/engine/src/witness/sol.rs b/engine/src/witness/sol.rs index 80c38e8671..5194a8484c 100644 --- a/engine/src/witness/sol.rs +++ b/engine/src/witness/sol.rs @@ -137,9 +137,12 @@ impl VoterApi for SolanaEgressWitnessingVoter { <::Vote as VoteStorage>::Vote, anyhow::Error, > { - Ok(TransactionSuccessDetails { - tx_fee: egress_witnessing::get_finalized_fee(&self.client, signature).await?, - }) + egress_witnessing::get_finalized_fee_and_success_status(&self.client, signature) + .await + .map(|(tx_fee, transaction_successful)| TransactionSuccessDetails { + tx_fee, + transaction_successful, + }) } } diff --git a/engine/src/witness/sol/egress_witnessing.rs b/engine/src/witness/sol/egress_witnessing.rs index 2e62d2b2f5..5bf4c46a56 100644 --- a/engine/src/witness/sol/egress_witnessing.rs +++ b/engine/src/witness/sol/egress_witnessing.rs @@ -10,10 +10,10 @@ use anyhow::Result; use cf_chains::sol::{SolSignature, LAMPORTS_PER_SIGNATURE}; use itertools::Itertools; -pub async fn get_finalized_fee( +pub async fn get_finalized_fee_and_success_status( sol_client: &SolRetryRpcClient, signature: SolSignature, -) -> Result { +) -> Result<(u64, bool)> { match sol_client .get_signature_statuses(&[signature], false) .await @@ -46,12 +46,12 @@ pub async fn get_finalized_fee( .meta; Ok(match transaction_meta { - Some(meta) => meta.fee, - // This shouldn't happen. Want to avoid Erroring. We either default to - // 5000 or return OK(()) so we don't submit transaction_succeeded and - // retry again later. Defaulting to avoid potentially getting stuck not - // witness something because no meta is returned. - None => LAMPORTS_PER_SIGNATURE, + Some(meta) => (meta.fee, meta.err.is_none()), + // This shouldn't happen. We want to avoid Erroring. + // Therefore we return default value (5000, true) so we don't submit + // transaction_succeeded and retry again later. Also avoids potentially getting + // stuck not witness something because no meta is returned. + None => (LAMPORTS_PER_SIGNATURE, true), }) }, Some(TransactionStatus { confirmation_status: other_status, .. }) => Err(anyhow::anyhow!( @@ -102,10 +102,11 @@ mod tests { SolSignature::from_str( "4udChXyRXrqBxUTr9F3nbTcPyvteLJtFQ3wM35J53NdP4GWwUp2wBwdTJEYs2aiNz7DyCqitok6ci7qqHPkRByb2").unwrap(); - let fee = get_finalized_fee(&client, monitored_tx_signature).await.unwrap(); + let (fee, tx_successful) = get_finalized_fee_and_success_status(&client, monitored_tx_signature).await.unwrap(); - println!("{:?}", fee); + println!("{:?}", (fee, tx_successful)); assert_eq!(fee, LAMPORTS_PER_SIGNATURE); + assert!(tx_successful); Ok(()) } diff --git a/localnet/create.sh b/localnet/create.sh index 184c4bbeb1..af3c87bf6a 100755 --- a/localnet/create.sh +++ b/localnet/create.sh @@ -5,8 +5,6 @@ ## Usage ./localnet/create.sh -b -n -t ## Example ./localnet/create.sh -b ./target/debug -n 1 -s y -source ./localnet/common.sh - # Parse command-line arguments while getopts "b:n:t:h" opt; do case $opt in @@ -14,7 +12,7 @@ while getopts "b:n:t:h" opt; do n) NODE_COUNT=$OPTARG ;; t) START_TRACKER=$OPTARG ;; h) echo "Usage: ./localnet/create.sh -b -n -t "; exit 0 ;; - \?) echo "Invalid option -$OPTARG" >&2 ;; + \?) echo "Invalid option -$OPTARG" >&2 ; exit 0 ;; esac done if [[ -n "$NODE_COUNT" && "$NODE_COUNT" != "1" && "$NODE_COUNT" != "3" ]]; then @@ -26,6 +24,8 @@ if [[ -n "$START_TRACKER" && "$START_TRACKER" != "y" && "$START_TRACKER" != "" ] exit 1 fi +source ./localnet/common.sh + # Set default values if not provided export BINARY_ROOT_PATH=${BINARY_ROOT_PATH:-"./target/debug"} export NODE_COUNT=${NODE_COUNT:-"1-node"} diff --git a/localnet/recreate.sh b/localnet/recreate.sh index ca92e4e9f0..f5cce4b94e 100755 --- a/localnet/recreate.sh +++ b/localnet/recreate.sh @@ -1,17 +1,40 @@ #!/bin/bash -## Stops the existing localnet and starts a new one using the same settings. -## The settings are saved in the settings.sh file in the tmp directory. +## Stops the existing localnet (if running) and starts a new one using the same settings. +## The settings are saved in the settings.sh file in the tmp directory on creation of a localnet. +## Use the -d flag to use default values if no settings file is found. + +# Parse arguments +USE_DEFAULTS=false +while getopts "dh" opt; do + case $opt in + d) USE_DEFAULTS=true;; + h) echo "Use -d to create with deafult values if no settings file is found"; exit 0;; + \?) echo "Invalid option -$OPTARG" >&2 ; exit 0 ;; + esac +done source ./localnet/common.sh -# Load the env vars that the last network used +# Load the env vars that the last localnet used load_settings + +# Use default values or error if no settings file was found if [ -z "$NODE_COUNT" ] || [ -z "$BINARY_ROOT_PATH" ]; then - echo "❌ Error: no existing network to recreate. Please run the manage script first to build a network." - exit 1 + if [ "$USE_DEFAULTS" = true ]; then + export BINARY_ROOT_PATH="./target/debug" + export NODE_COUNT="1-node" + export START_TRACKER="NO" + echo "No settings file found. Using default values:" + echo "BINARY_ROOT_PATH: $BINARY_ROOT_PATH" + echo "NODE_COUNT: $NODE_COUNT" + echo "START_TRACKER: $START_TRACKER" + else + echo "❌ Error: no settings file found. Use -d to create one with defaults, or you can create one using the create/manage scripts." + exit 1 + fi fi -# Destroy and start a new network +# Destroy and start a new localnet destroy sleep 5 build-localnet \ No newline at end of file diff --git a/state-chain/cf-integration-tests/src/solana.rs b/state-chain/cf-integration-tests/src/solana.rs index 3af8d2f7bc..ae68e0c059 100644 --- a/state-chain/cf-integration-tests/src/solana.rs +++ b/state-chain/cf-integration-tests/src/solana.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use std::marker::PhantomData; +use std::{collections::BTreeMap, marker::PhantomData}; use super::*; use cf_chains::{ @@ -26,15 +26,23 @@ use frame_support::{ assert_err, traits::{OnFinalize, UnfilteredDispatchable}, }; -use pallet_cf_ingress_egress::DepositWitness; +use pallet_cf_elections::{ + electoral_system::{ElectionIdentifierOf, ElectoralSystem}, + vote_storage::{composite::tuple_6_impls::CompositeVote, AuthorityVote, VoteStorage}, + MAXIMUM_VOTES_PER_EXTRINSIC, +}; +use pallet_cf_ingress_egress::{DepositWitness, FetchOrTransfer}; use pallet_cf_validator::RotationPhase; +use sp_core::ConstU32; +use sp_runtime::BoundedBTreeMap; use state_chain_runtime::{ chainflip::{ - address_derivation::AddressDerivation, ChainAddressConverter, SolEnvironment, + address_derivation::AddressDerivation, solana_elections::TransactionSuccessDetails, + ChainAddressConverter, SolEnvironment, SolanaTransactionBuilder as RuntimeSolanaTransactionBuilder, }, - Runtime, RuntimeCall, RuntimeEvent, SolanaIngressEgress, SolanaInstance, SolanaThresholdSigner, - Swapping, + Runtime, RuntimeCall, RuntimeEvent, SolanaElections, SolanaIngressEgress, SolanaInstance, + SolanaThresholdSigner, Swapping, }; use crate::{ @@ -48,6 +56,16 @@ const ALICE: AccountId = AccountId::new([0x33; 32]); const BOB: AccountId = AccountId::new([0x44; 32]); const DEPOSIT_AMOUNT: u64 = 5_000_000_000u64; // 5 Sol +const FALLBACK_ADDRESS: SolAddress = SolAddress([0xf0; 32]); + +type SolanaElectionVote = BoundedBTreeMap::< + ElectionIdentifierOf<>::ElectoralSystem>, + AuthorityVote< + <<>::ElectoralSystem as ElectoralSystem>::Vote as VoteStorage>::PartialVote, + <<>::ElectoralSystem as ElectoralSystem>::Vote as VoteStorage>::Vote, + >, + ConstU32, +>; fn setup_sol_environments() { // Environment::SolanaApiEnvironment @@ -449,6 +467,7 @@ fn solana_ccm_fails_with_invalid_input() { SolCcmAddress { pubkey: SolPubkey([0x01; 32]), is_writable: false }, SolCcmAddress { pubkey: SolPubkey([0x02; 32]), is_writable: false }, ], + fallback_address: FALLBACK_ADDRESS.into(), } .encode() .try_into() @@ -631,3 +650,113 @@ fn solana_resigning() { } }); } + +#[test] +fn solana_ccm_execution_error_can_trigger_fallback() { + const EPOCH_BLOCKS: u32 = 100; + const MAX_AUTHORITIES: AuthorityCount = 10; + super::genesis::with_test_defaults() + .blocks_per_epoch(EPOCH_BLOCKS) + .max_authorities(MAX_AUTHORITIES) + .with_additional_accounts(&[ + (DORIS, AccountRole::LiquidityProvider, 5 * FLIPPERINOS_PER_FLIP), + (ZION, AccountRole::Broker, 5 * FLIPPERINOS_PER_FLIP), + ]) + .build() + .execute_with(|| { + setup_sol_environments(); + + let (mut testnet, _, _) = network::fund_authorities_and_join_auction(MAX_AUTHORITIES); + assert_ok!(RuntimeCall::SolanaVault( + pallet_cf_vaults::Call::::initialize_chain {} + ) + .dispatch_bypass_filter(pallet_cf_governance::RawOrigin::GovernanceApproval.into())); + setup_pool_and_accounts(vec![Asset::Sol, Asset::SolUsdc], OrderType::LimitOrder); + testnet.move_to_the_next_epoch(); + + // Trigger a CCM swap + let ccm = CcmDepositMetadata { + source_chain: ForeignChain::Ethereum, + source_address: Some(ForeignChainAddress::Eth([0xff; 20].into())), + channel_metadata: CcmChannelMetadata { + message: vec![0u8, 1u8, 2u8, 3u8].try_into().unwrap(), + gas_budget: 1_000_000_000u128, + cf_parameters: SolCcmAccounts { + cf_receiver: SolCcmAddress { pubkey: SolPubkey([0x10; 32]), is_writable: true }, + remaining_accounts: vec![ + SolCcmAddress { pubkey: SolPubkey([0x01; 32]), is_writable: false }, + SolCcmAddress { pubkey: SolPubkey([0x02; 32]), is_writable: false }, + ], + fallback_address: FALLBACK_ADDRESS.into(), + } + .encode() + .try_into() + .unwrap(), + }, + }; + witness_call(RuntimeCall::SolanaIngressEgress( + pallet_cf_ingress_egress::Call::contract_ccm_swap_request { + source_asset: Asset::Sol, + deposit_amount: 1_000_000_000_000u128, + destination_asset: Asset::SolUsdc, + destination_address: EncodedAddress::Sol([1u8; 32]), + deposit_metadata: ccm, + tx_hash: Default::default(), + }, + )); + + // Wait for the swaps to complete and call broadcasted. + testnet.move_forward_blocks(5); + + // Get the broadcast ID for the ccm. There should be only one broadcast pending. + assert_eq!(pallet_cf_broadcast::PendingBroadcasts::::get().len(), 1); + let ccm_broadcast_id = pallet_cf_broadcast::PendingBroadcasts::::get().into_iter().next().unwrap(); + + // Get the election identifier of the Solana egress. + let election_id = SolanaElections::with_electoral_access_and_identifiers( + |_, election_identifiers| { + Ok(election_identifiers.last().cloned().unwrap()) + }, + ).unwrap(); + + // Submit vote to witness: transaction success, but execution failure + let vote: SolanaElectionVote = BTreeMap::from_iter([(election_id, + AuthorityVote::Vote(CompositeVote::EE(TransactionSuccessDetails { + tx_fee: 1_000, + transaction_successful: false, + })) + )]).try_into().unwrap(); + + for v in Validator::current_authorities() { + assert_ok!(SolanaElections::stop_ignoring_my_votes( + RuntimeOrigin::signed(v.clone()), + )); + + assert_ok!(SolanaElections::vote( + RuntimeOrigin::signed(v), + vote.clone() + )); + } + + // Egress queue should be empty + assert_eq!(pallet_cf_ingress_egress::ScheduledEgressFetchOrTransfer::::decode_len(), Some(0)); + + // on_finalize: reach consensus on the egress vote and trigger the fallback mechanism. + SolanaElections::on_finalize(System::block_number() + 1); + assert_eq!(pallet_cf_ingress_egress::ScheduledEgressFetchOrTransfer::::decode_len(), Some(1)); + assert!(matches!(pallet_cf_ingress_egress::ScheduledEgressFetchOrTransfer::::get()[0], + FetchOrTransfer::Transfer { + egress_id: (ForeignChain::Solana, 2), + asset: cf_chains::assets::sol::Asset::SolUsdc, + destination_address: FALLBACK_ADDRESS, + .. + } + )); + + // Ensure the previous broadcast data has been cleaned up. + assert!(!pallet_cf_broadcast::PendingBroadcasts::::get().contains(&ccm_broadcast_id)); + assert!(!pallet_cf_broadcast::AwaitingBroadcast::::contains_key(ccm_broadcast_id)); + assert!(!pallet_cf_broadcast::TransactionOutIdToBroadcastId::::iter_values().any(|(broadcast_id, _)|broadcast_id == ccm_broadcast_id)); + assert!(!pallet_cf_broadcast::PendingApiCalls::::contains_key(ccm_broadcast_id)); + }); +} diff --git a/state-chain/cf-integration-tests/src/swapping.rs b/state-chain/cf-integration-tests/src/swapping.rs index 8ec0ed2eaa..f71dd53698 100644 --- a/state-chain/cf-integration-tests/src/swapping.rs +++ b/state-chain/cf-integration-tests/src/swapping.rs @@ -17,6 +17,7 @@ use cf_chains::{ address::{AddressConverter, AddressDerivationApi, EncodedAddress}, assets::eth::Asset as EthAsset, eth::{api::EthereumApi, EthereumTrackedData}, + evm::DepositDetails, CcmChannelMetadata, CcmDepositMetadata, Chain, ChainState, DefaultRetryPolicy, Ethereum, ExecutexSwapAndCall, ForeignChain, ForeignChainAddress, RetryPolicy, SwapOrigin, TransactionBuilder, TransferAssetParams, @@ -622,6 +623,10 @@ fn failed_swaps_are_rolled_back() { deposit_amount: 10_000 * DECIMALS, destination_address: EncodedAddress::Eth(Default::default()), tx_hash: Default::default(), + deposit_details: Box::new(DepositDetails { tx_hashes: None }), + refund_params: None, + dca_params: None, + boost_fee: 0, }, )); diff --git a/state-chain/chains/src/benchmarking_value.rs b/state-chain/chains/src/benchmarking_value.rs index a7cc2da8f9..0fb0fabf5f 100644 --- a/state-chain/chains/src/benchmarking_value.rs +++ b/state-chain/chains/src/benchmarking_value.rs @@ -15,6 +15,7 @@ use sp_std::vec; #[cfg(feature = "runtime-benchmarks")] use crate::{ address::{EncodedAddress, ForeignChainAddress}, + btc::{BtcDepositDetails, UtxoId}, dot::PolkadotTransactionId, evm::{DepositDetails, EvmFetchId, EvmTransactionMetadata}, }; @@ -223,6 +224,16 @@ impl BenchmarkValue for DepositDetails { } } +#[cfg(feature = "runtime-benchmarks")] +impl BenchmarkValue for BtcDepositDetails { + fn benchmark_value() -> Self { + BtcDepositDetails { + utxo_id: UtxoId::benchmark_value(), + deposit_address: crate::btc::deposit_address::DepositAddress::new([0; 32], 0), + } + } +} + 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 7999af390e..437ef73ffd 100644 --- a/state-chain/chains/src/btc.rs +++ b/state-chain/chains/src/btc.rs @@ -227,6 +227,12 @@ impl BitcoinFeeInfo { } } +#[derive(Encode, Decode, TypeInfo, Clone, RuntimeDebug, PartialEq, Eq, MaxEncodedLen)] +pub struct BtcDepositDetails { + pub utxo_id: UtxoId, + pub deposit_address: DepositAddress, +} + impl Chain for Bitcoin { const NAME: &'static str = "Bitcoin"; const GAS_ASSET: Self::ChainAsset = assets::btc::Asset::Btc; @@ -244,7 +250,7 @@ impl Chain for Bitcoin { type ChainAccount = ScriptPubkey; type DepositFetchId = BitcoinFetchId; type DepositChannelState = DepositAddress; - type DepositDetails = UtxoId; + type DepositDetails = BtcDepositDetails; type Transaction = BitcoinTransactionData; type TransactionMetadata = (); type TransactionRef = Hash; @@ -262,6 +268,7 @@ pub enum PreviousOrCurrent { #[derive(Clone, Debug, PartialEq, Eq)] pub struct BitcoinCrypto; impl ChainCrypto for BitcoinCrypto { + const NAME: &'static str = "Bitcoin"; type UtxoChain = ConstBool; type AggKey = AggKey; diff --git a/state-chain/chains/src/btc/smart_contract_encoding.rs b/state-chain/chains/src/btc/smart_contract_encoding.rs index 82add655a3..4d63ab6581 100644 --- a/state-chain/chains/src/btc/smart_contract_encoding.rs +++ b/state-chain/chains/src/btc/smart_contract_encoding.rs @@ -69,7 +69,6 @@ pub struct SharedCfParameters { pub boost_fee: u8, } -#[allow(dead_code)] pub fn encode_data_in_nulldata_utxo(data: &[u8]) -> Option { if data.len() > MAX_NULLDATA_LENGTH { return None; diff --git a/state-chain/chains/src/ccm_checker.rs b/state-chain/chains/src/ccm_checker.rs index 13ccc8cd47..b6a659c89e 100644 --- a/state-chain/chains/src/ccm_checker.rs +++ b/state-chain/chains/src/ccm_checker.rs @@ -124,6 +124,7 @@ mod test { cf_parameters: SolCcmAccounts { cf_receiver: SolCcmAddress { pubkey: SolPubkey([0x01; 32]), is_writable: true }, remaining_accounts: vec![], + fallback_address: SolPubkey([0xf0; 32]), } .encode() .try_into() @@ -146,6 +147,7 @@ mod test { pubkey: SolPubkey([0x01; 32]), is_writable: true, }], + fallback_address: SolPubkey([0xf0; 32]), } .encode() .try_into() @@ -163,6 +165,7 @@ mod test { gas_budget: 0, cf_parameters: SolCcmAccounts { cf_receiver: SolCcmAddress { pubkey: SolPubkey([0x01; 32]), is_writable: true }, + fallback_address: SolPubkey([0xf0; 32]), remaining_accounts: vec![], } .encode() @@ -186,6 +189,7 @@ mod test { pubkey: SolPubkey([0x01; 32]), is_writable: true, }], + fallback_address: SolPubkey([0xf0; 32]), } .encode() .try_into() @@ -259,6 +263,7 @@ mod test { SolCcmAddress { pubkey: crate::sol::SolPubkey([0x01; 32]), is_writable: false }, SolCcmAddress { pubkey: crate::sol::SolPubkey([0x02; 32]), is_writable: false }, ], + fallback_address: SolPubkey([0xf0; 32]), }; assert_err!( check_ccm_for_blacklisted_accounts(&ccm_accounts, blacklisted_accounts()), @@ -277,6 +282,7 @@ mod test { }, SolCcmAddress { pubkey: crate::sol::SolPubkey([0x02; 32]), is_writable: false }, ], + fallback_address: SolPubkey([0xf0; 32]), }; assert_err!( check_ccm_for_blacklisted_accounts(&ccm_accounts, blacklisted_accounts()), @@ -293,6 +299,7 @@ mod test { SolCcmAddress { pubkey: crate::sol::SolPubkey([0x01; 32]), is_writable: false }, SolCcmAddress { pubkey: crate::sol::SolPubkey([0x02; 32]), is_writable: false }, ], + fallback_address: SolPubkey([0xf0; 32]), }; assert_err!( check_ccm_for_blacklisted_accounts(&ccm_accounts, blacklisted_accounts()), @@ -308,6 +315,7 @@ mod test { SolCcmAddress { pubkey: sol_test_values::agg_key().into(), is_writable: false }, SolCcmAddress { pubkey: crate::sol::SolPubkey([0x02; 32]), is_writable: false }, ], + fallback_address: SolPubkey([0xf0; 32]), }; assert_err!( check_ccm_for_blacklisted_accounts(&ccm_accounts, blacklisted_accounts()), diff --git a/state-chain/chains/src/dot.rs b/state-chain/chains/src/dot.rs index ce76109be5..3eb846ab1f 100644 --- a/state-chain/chains/src/dot.rs +++ b/state-chain/chains/src/dot.rs @@ -359,6 +359,7 @@ impl ChannelLifecycleHooks for PolkadotChannelState { #[derive(Clone, Debug, PartialEq, Eq)] pub struct PolkadotCrypto; impl ChainCrypto for PolkadotCrypto { + const NAME: &'static str = "Polkadot"; type UtxoChain = ConstBool; type AggKey = PolkadotPublicKey; diff --git a/state-chain/chains/src/evm.rs b/state-chain/chains/src/evm.rs index 35ce8299d9..c157129acc 100644 --- a/state-chain/chains/src/evm.rs +++ b/state-chain/chains/src/evm.rs @@ -33,6 +33,7 @@ pub struct DepositDetails { pub struct EvmCrypto; impl ChainCrypto for EvmCrypto { + const NAME: &'static str = "EVM"; type UtxoChain = ConstBool; type AggKey = evm::AggKey; diff --git a/state-chain/chains/src/lib.rs b/state-chain/chains/src/lib.rs index 7fc4c80909..4e2a3088f1 100644 --- a/state-chain/chains/src/lib.rs +++ b/state-chain/chains/src/lib.rs @@ -280,6 +280,7 @@ pub trait Chain: Member + Parameter + ChainInstanceAlias { /// Common crypto-related types and operations for some external chain. pub trait ChainCrypto: ChainCryptoInstanceAlias { + const NAME: &'static str; type UtxoChain: Get; /// The chain's `AggKey` format. The AggKey is the threshold key that controls the vault. diff --git a/state-chain/chains/src/mocks.rs b/state-chain/chains/src/mocks.rs index 2aacf1c90f..b51b494cae 100644 --- a/state-chain/chains/src/mocks.rs +++ b/state-chain/chains/src/mocks.rs @@ -253,6 +253,7 @@ pub const BAD_AGG_KEY_POST_HANDOVER: MockAggKey = MockAggKey(*b"bad!"); #[derive(Copy, Clone, RuntimeDebug, Default, PartialEq, Eq, Encode, Decode, TypeInfo)] pub struct MockEthereumChainCrypto; impl ChainCrypto for MockEthereumChainCrypto { + const NAME: &'static str = "MockEthereum"; type UtxoChain = ConstBool; type AggKey = MockAggKey; diff --git a/state-chain/chains/src/none.rs b/state-chain/chains/src/none.rs index 2a6d024cf2..72bc55e9ae 100644 --- a/state-chain/chains/src/none.rs +++ b/state-chain/chains/src/none.rs @@ -43,6 +43,7 @@ impl FeeRefundCalculator for () { #[derive(Clone, Debug, PartialEq, Eq)] pub struct NoneChainCrypto; impl ChainCrypto for NoneChainCrypto { + const NAME: &'static str = "None"; type UtxoChain = ConstBool; type AggKey = (); type Payload = (); diff --git a/state-chain/chains/src/sol.rs b/state-chain/chains/src/sol.rs index b8a344ad0d..351807c50f 100644 --- a/state-chain/chains/src/sol.rs +++ b/state-chain/chains/src/sol.rs @@ -84,6 +84,7 @@ impl Chain for Solana { pub struct SolanaCrypto; impl ChainCrypto for SolanaCrypto { + const NAME: &'static str = "Solana"; type UtxoChain = ConstBool; type KeyHandoverIsRequired = ConstBool; diff --git a/state-chain/chains/src/sol/api.rs b/state-chain/chains/src/sol/api.rs index ff1b9442bf..1b330d9ff9 100644 --- a/state-chain/chains/src/sol/api.rs +++ b/state-chain/chains/src/sol/api.rs @@ -113,8 +113,12 @@ pub enum SolanaTransactionType { BatchFetch, Transfer, RotateAggKey, - CcmTransfer, + #[deprecated] + CcmTransferLegacy, SetGovKeyWithAggKey, + CcmTransfer { + fallback: TransferAssetParams, + }, } /// The Solana Api call. Contains a call_type and the actual Transaction itself. @@ -302,6 +306,12 @@ impl SolanaApi { let compute_price = Environment::compute_price()?; let durable_nonce = Environment::nonce_account()?; + let fallback = TransferAssetParams { + asset: transfer_param.asset, + amount: transfer_param.amount, + to: ccm_accounts.fallback_address.into(), + }; + // Build the transaction let transaction = match transfer_param.asset { SolAsset::Sol => SolanaTransactionBuilder::ccm_transfer_native( @@ -363,7 +373,7 @@ impl SolanaApi { })?; Ok(Self { - call_type: SolanaTransactionType::CcmTransfer, + call_type: SolanaTransactionType::CcmTransfer { fallback }, transaction, signer: None, _phantom: Default::default(), diff --git a/state-chain/chains/src/sol/sol_tx_core.rs b/state-chain/chains/src/sol/sol_tx_core.rs index 87c249fa18..7a29fad8e3 100644 --- a/state-chain/chains/src/sol/sol_tx_core.rs +++ b/state-chain/chains/src/sol/sol_tx_core.rs @@ -804,6 +804,7 @@ impl From for AccountMeta { pub struct CcmAccounts { pub cf_receiver: CcmAddress, pub remaining_accounts: Vec, + pub fallback_address: Pubkey, } impl CcmAccounts { @@ -820,6 +821,7 @@ fn ccm_extra_accounts_encoding() { CcmAddress { pubkey: Pubkey([0x22; 32]), is_writable: true }, CcmAddress { pubkey: Pubkey([0x33; 32]), is_writable: true }, ], + fallback_address: Pubkey([0xf0; 32]), }; let encoded = Encode::encode(&extra_accounts); @@ -835,7 +837,8 @@ fn ccm_extra_accounts_encoding() { "1111111111111111111111111111111111111111111111111111111111111111 00 08 2222222222222222222222222222222222222222222222222222222222222222 01 - 3333333333333333333333333333333333333333333333333333333333333333 01" + 3333333333333333333333333333333333333333333333333333333333333333 01 + F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0" ) ); } @@ -936,6 +939,7 @@ pub mod sol_test_values { pubkey: const_address("CFp37nEY6E9byYHiuxQZg6vMCnzwNrgiF9nFGT6Zwcnx").into(), is_writable: false, }], + fallback_address: const_address("AkYRjwVHBCcE1HsjZaTFr5SrTNHPRX7PtwZxdSDMcTvb").into(), } } @@ -1428,7 +1432,7 @@ mod tests { let serialized_tx = tx.finalize_and_serialize().unwrap(); let expected_serialized_tx = hex_literal::hex!("017663fd8be6c54a3ce492a4aac1f50ed8a1589f8aa091d04b52e6fa8a43f22d359906e21630ca3dd93179e989bc1fdccbae8f9a30f6470ef9d5c17a7625f0050a01000411f79d5e026f12edc6443a534b2cdd5072233989b415d7596573e743f3e5b386fb0e14940a2247d0a8a33650d7dfe12d269ecabce61c1219b5a6dcdb6961026e0917eb2b10d3377bda2bc7bea65bec6b8372f4fc3463ec2cd6f9fde4b2c633d1926744e9d9790761c45a800a074687b5ff47b449a90c722a3852543be5439900448541f57201f277c5f3ffb631d0212e26e7f47749c26c4808718174a0ab2a09a18cd28baa84f2067bbdf24513c2d44e44bf408f2e6da6e60762e3faa4a62a0adbcd644e45426a41a7cb8369b8a0c1c89bb3f86cf278fdd9cc38b0f69784ad5667e392cd98d3284fd551604be95c14cc8e20123e2940ef9fb784e6b591c7442864e5e1869817a4fd88ddf7ab7a5f7252d7c345b39721769888608592912e8ca9acf0f13460b3fd04b7d53d7421fc874ec00eec769cf36480895e1a407bf1249475f2b2e24122be016983be9369965246cc45e1f621d40fba300c56c7ac50c3874df4f83bd213a59c9785110cf83c718f9486c3484f918593bce20c61dc6a96036afecc89e3b031824af6363174d19bbec12d3a13c4a173e5aeb349b63042bc138f00000000000000000000000000000000000000000000000000000000000000000306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a4000000006a7d517192c568ee08a845f73d29788cf035c3145b21ab344d8062ea940000072b5d2051d300b10b74314b7e25ace9998ca66eb2c7fbc10ef130dd67028293cc27e9074fac5e8d36cf04f94a0606fdd8ddbb420e99a489c7915ce5699e489000e0d03020f0004040000000e00090340420f00000000000e000502e093040010040100030d094e518fabdda5d68b000d02020024070000006744e9d9790761c45a800a074687b5ff47b449a90c722a3852543be5439900440d020b0024070000006744e9d9790761c45a800a074687b5ff47b449a90c722a3852543be5439900440d02090024070000006744e9d9790761c45a800a074687b5ff47b449a90c722a3852543be5439900440d020a0024070000006744e9d9790761c45a800a074687b5ff47b449a90c722a3852543be5439900440d02070024070000006744e9d9790761c45a800a074687b5ff47b449a90c722a3852543be5439900440d02060024070000006744e9d9790761c45a800a074687b5ff47b449a90c722a3852543be5439900440d02040024070000006744e9d9790761c45a800a074687b5ff47b449a90c722a3852543be5439900440d020c0024070000006744e9d9790761c45a800a074687b5ff47b449a90c722a3852543be5439900440d02080024070000006744e9d9790761c45a800a074687b5ff47b449a90c722a3852543be5439900440d02050024070000006744e9d9790761c45a800a074687b5ff47b449a90c722a3852543be543990044").to_vec(); - println!("tx:{:?}", hex::encode(serialized_tx.clone())); + // println!("tx:{:?}", hex::encode(serialized_tx.clone())); assert_eq!(serialized_tx, expected_serialized_tx); assert!(serialized_tx.len() <= MAX_TRANSACTION_LENGTH) diff --git a/state-chain/chains/src/sol/transaction_builder.rs b/state-chain/chains/src/sol/transaction_builder.rs index 9d281e7995..71ff3a61ac 100644 --- a/state-chain/chains/src/sol/transaction_builder.rs +++ b/state-chain/chains/src/sol/transaction_builder.rs @@ -516,7 +516,7 @@ mod test { .finalize_and_serialize() .expect("Transaction serialization must succeed"); - println!("Serialized tx length: {:?}", serialized_tx.len()); + // println!("Serialized tx length: {:?}", serialized_tx.len()); assert!(serialized_tx.len() <= MAX_TRANSACTION_LENGTH); if serialized_tx != expected_serialized_tx { diff --git a/state-chain/custom-rpc/src/lib.rs b/state-chain/custom-rpc/src/lib.rs index 2cac8ca9b6..cc7ffc221a 100644 --- a/state-chain/custom-rpc/src/lib.rs +++ b/state-chain/custom-rpc/src/lib.rs @@ -912,6 +912,21 @@ pub trait CustomApi { proposed_votes: Vec, at: Option, ) -> RpcResult>; + + #[method(name = "validate_dca_params")] + fn cf_validate_dca_params( + &self, + number_of_chunks: u32, + chunk_interval: u32, + at: Option, + ) -> RpcResult<()>; + + #[method(name = "validate_refund_params")] + fn cf_validate_refund_params( + &self, + retry_duration: u32, + at: Option, + ) -> RpcResult<()>; } /// An RPC extension for the state chain node. @@ -1973,6 +1988,31 @@ where ) .map_err(to_rpc_error) } + + fn cf_validate_dca_params( + &self, + number_of_chunks: u32, + chunk_interval: u32, + at: Option, + ) -> RpcResult<()> { + self.client + .runtime_api() + .cf_validate_dca_params(self.unwrap_or_best(at), number_of_chunks, chunk_interval) + .map_err(to_rpc_error) + .and_then(|result| result.map_err(map_dispatch_error)) + } + + fn cf_validate_refund_params( + &self, + retry_duration: u32, + at: Option, + ) -> RpcResult<()> { + self.client + .runtime_api() + .cf_validate_refund_params(self.unwrap_or_best(at), retry_duration) + .map_err(to_rpc_error) + .and_then(|result| result.map_err(map_dispatch_error)) + } } impl CustomRpc diff --git a/state-chain/pallets/cf-broadcast/src/lib.rs b/state-chain/pallets/cf-broadcast/src/lib.rs index b3cb2c8641..67745cd1cc 100644 --- a/state-chain/pallets/cf-broadcast/src/lib.rs +++ b/state-chain/pallets/cf-broadcast/src/lib.rs @@ -59,7 +59,7 @@ pub enum PalletConfigUpdate { BroadcastTimeout { blocks: u32 }, } -pub const PALLET_VERSION: StorageVersion = StorageVersion::new(9); +pub const PALLET_VERSION: StorageVersion = StorageVersion::new(10); #[frame_support::pallet] pub mod pallet { @@ -1034,6 +1034,15 @@ impl, I: 'static> Pallet { // insert items. FailedBroadcasters::::decode_non_dedup_len(broadcast_id).unwrap_or_default() as u32 } + + /// Returns the ApiCall from a `transaction_out_id`. + pub fn pending_api_call_from_out_id( + tx_out_id: TransactionOutIdFor, + ) -> Option<(BroadcastId, ApiCallFor)> { + TransactionOutIdToBroadcastId::::get(tx_out_id).and_then(|(broadcast_id, _)| { + PendingApiCalls::::get(broadcast_id).map(|api_call| (broadcast_id, api_call)) + }) + } } impl, I: 'static> Broadcaster for Pallet { diff --git a/state-chain/pallets/cf-broadcast/src/migrations.rs b/state-chain/pallets/cf-broadcast/src/migrations.rs index 1d9cadbd3e..d706760962 100644 --- a/state-chain/pallets/cf-broadcast/src/migrations.rs +++ b/state-chain/pallets/cf-broadcast/src/migrations.rs @@ -10,4 +10,5 @@ pub type PalletMigration = ( VersionedMigration, migrate_timeouts::Migration, 7, 8>, PlaceholderMigration, 8>, // Migration 8->9 is SerializeSolanaBroadcastMigration in runtime lib. + // Migration 9->10 is SolanaEgressSuccessWitnessMigration in runtime lib. ); diff --git a/state-chain/pallets/cf-elections/src/lib.rs b/state-chain/pallets/cf-elections/src/lib.rs index 90c6678696..8ad7b79b50 100644 --- a/state-chain/pallets/cf-elections/src/lib.rs +++ b/state-chain/pallets/cf-elections/src/lib.rs @@ -222,7 +222,7 @@ pub mod pallet { Self(unique_monotonic, extra) } - pub(crate) fn with_extra( + pub fn with_extra( &self, other_extra: OtherExtra, ) -> ElectionIdentifier { @@ -581,7 +581,7 @@ pub mod pallet { // ---------------------------------------------------------------------------------------- // - pub(crate) mod access_impls { + pub mod access_impls { use super::*; /// Implements traits to allow electoral systems to read/write an Election's details. @@ -590,9 +590,7 @@ pub mod pallet { _phantom: core::marker::PhantomData<(T, I)>, } impl, I: 'static> ElectionAccess { - pub(crate) fn new( - election_identifier: ElectionIdentifierOf, - ) -> Self { + pub fn new(election_identifier: ElectionIdentifierOf) -> Self { Self { election_identifier, _phantom: Default::default() } } diff --git a/state-chain/pallets/cf-ingress-egress/src/benchmarking.rs b/state-chain/pallets/cf-ingress-egress/src/benchmarking.rs index 5f8d853d7b..9af3401b1f 100644 --- a/state-chain/pallets/cf-ingress-egress/src/benchmarking.rs +++ b/state-chain/pallets/cf-ingress-egress/src/benchmarking.rs @@ -288,15 +288,19 @@ mod benchmarks { #[benchmark] fn contract_swap_request() { - let deposit_amount = 1_000; + let deposit_amount = 1_000u32; let witness_origin = T::EnsureWitnessed::try_successful_origin().unwrap(); let call = Call::::contract_swap_request { from: Asset::Usdc, to: Asset::Eth, - deposit_amount, + deposit_amount: deposit_amount.into(), destination_address: EncodedAddress::benchmark_value(), tx_hash: [0; 32], + deposit_details: Box::new(BenchmarkValue::benchmark_value()), + refund_params: None, + dca_params: None, + boost_fee: 0, }; #[block] diff --git a/state-chain/pallets/cf-ingress-egress/src/lib.rs b/state-chain/pallets/cf-ingress-egress/src/lib.rs index 072013cf10..2f32808296 100644 --- a/state-chain/pallets/cf-ingress-egress/src/lib.rs +++ b/state-chain/pallets/cf-ingress-egress/src/lib.rs @@ -34,7 +34,7 @@ use cf_chains::{ use cf_primitives::{ Asset, AssetAmount, BasisPoints, Beneficiaries, BoostPoolTier, BroadcastId, ChannelId, DcaParameters, EgressCounter, EgressId, EpochIndex, ForeignChain, PrewitnessedDepositId, - SwapRequestId, ThresholdSignatureRequestId, TransactionHash, SWAP_DELAY_BLOCKS, + SwapRequestId, ThresholdSignatureRequestId, TransactionHash, }; use cf_runtime_utilities::log_or_panic; use cf_traits::{ @@ -57,6 +57,7 @@ use scale_info::{ }; use sp_runtime::traits::UniqueSaturatedInto; use sp_std::{ + boxed::Box, collections::{btree_map::BTreeMap, btree_set::BTreeSet}, marker::PhantomData, vec, @@ -657,6 +658,10 @@ pub mod pallet { deposit_metadata: CcmDepositMetadataEncoded, origin: SwapOrigin, }, + CcmFallbackScheduled { + broadcast_id: BroadcastId, + egress_details: ScheduledEgressDetails, + }, } #[derive(CloneNoBound, PartialEqNoBound, EqNoBound)] @@ -694,11 +699,6 @@ pub mod pallet { DepositChannelCreationDisabled, /// The specified boost pool does not exist. BoostPoolDoesNotExist, - /// Swap Retry duration is set above the max allowed. - SwapRetryDurationTooLong, - /// The number of chunks must be greater than 0, the interval must be greater than 2 and - /// the total duration of the swap request must be less then the max allowed. - InvalidDcaParameters, /// CCM parameters from a contract swap failed validity check. InvalidCcm, } @@ -1100,9 +1100,14 @@ pub mod pallet { origin: OriginFor, from: Asset, to: Asset, - deposit_amount: AssetAmount, + deposit_amount: ::ChainAmount, destination_address: EncodedAddress, tx_hash: TransactionHash, + deposit_details: Box<::DepositDetails>, + refund_params: Option, + dca_params: Option, + // This is only to be checked in the pre-witnessed version (not implemented yet) + _boost_fee: BasisPoints, ) -> DispatchResult { T::EnsureWitnessed::ensure_origin(origin)?; @@ -1118,16 +1123,20 @@ pub mod pallet { }, }; + T::DepositHandler::on_deposit_made(*deposit_details, deposit_amount); + + // TODO: ensure minimum deposit? + + // TODO: validate dca_params and refund_params + T::SwapRequestHandler::init_swap_request( from, - deposit_amount, + deposit_amount.into(), to, SwapRequestType::Regular { output_address: destination_address_internal.clone() }, Default::default(), - // NOTE: FoK not yet supported for swaps from the contract - None, - // NOTE: DCA not yet supported for swaps from the contract - None, + refund_params, + dca_params, SwapOrigin::Vault { tx_hash }, ); @@ -1819,11 +1828,7 @@ impl, I: 'static> Pallet { Self::deposit_event(Event::::DepositFetchesScheduled { channel_id, asset }); // Add the deposit to the balance. - T::DepositHandler::on_deposit_made( - deposit_details.clone(), - deposit_amount, - &deposit_channel_details.deposit_channel, - ); + T::DepositHandler::on_deposit_made(deposit_details.clone(), deposit_amount); // We received a deposit on a channel. If channel has been boosted earlier // (i.e. awaiting finalisation), *and* the boosted amount matches the amount @@ -2093,6 +2098,27 @@ impl, I: 'static> Pallet { fees_withheld, } } + + /// If a Ccm failed, we want to refund the user their assets. + /// This function will schedule a transfer to the fallback address, and emit an event on + /// success. IMPORTANT: Currently only used for Solana. + pub fn do_ccm_fallback( + broadcast_id: BroadcastId, + fallback: TransferAssetParams, + ) { + match Self::schedule_egress( + fallback.asset, + fallback.amount, + fallback.to, + None, + ) { + Ok(egress_details) => Self::deposit_event(Event::::CcmFallbackScheduled { + broadcast_id, + egress_details, + }), + Err(e) => log::error!("Ccm fallback failed to schedule the fallback egress: Target chain: {:?}, broadcast_id: {:?}, error: {:?}", T::TargetChain::get(), broadcast_id, e), + } + } } impl, I: 'static> EgressApi for Pallet { @@ -2217,31 +2243,11 @@ impl, I: 'static> DepositApi for Pallet { (ChannelId, ForeignChainAddress, ::ChainBlockNumber, Self::Amount), DispatchError, > { - let swap_limits = T::SwapLimitsProvider::get_swap_limits(); if let Some(params) = &refund_params { - ensure!( - params.retry_duration <= swap_limits.max_swap_retry_duration_blocks, - DispatchError::from(Error::::SwapRetryDurationTooLong) - ); + T::SwapLimitsProvider::validate_refund_params(params.retry_duration)?; } - if let Some(params) = &dca_params { - if params.number_of_chunks != 1 { - ensure!( - params.number_of_chunks > 0 && params.chunk_interval >= SWAP_DELAY_BLOCKS, - DispatchError::from(Error::::InvalidDcaParameters) - ); - let total_swap_request_duration = params - .number_of_chunks - .saturating_sub(1) - .checked_mul(params.chunk_interval) - .ok_or(Error::::InvalidDcaParameters)?; - - ensure!( - total_swap_request_duration <= swap_limits.max_swap_request_duration_blocks, - DispatchError::from(Error::::InvalidDcaParameters) - ); - } + T::SwapLimitsProvider::validate_dca_params(params)?; } let (channel_id, deposit_address, expiry_height, channel_opening_fee) = Self::open_channel( diff --git a/state-chain/pallets/cf-ingress-egress/src/tests.rs b/state-chain/pallets/cf-ingress-egress/src/tests.rs index 4c8ca43572..9d393f7186 100644 --- a/state-chain/pallets/cf-ingress-egress/src/tests.rs +++ b/state-chain/pallets/cf-ingress-egress/src/tests.rs @@ -13,12 +13,10 @@ use cf_chains::{ btc::{BitcoinNetwork, ScriptPubkey}, evm::{DepositDetails, EvmFetchId}, mocks::MockEthereum, - CcmChannelMetadata, CcmFailReason, ChannelRefundParameters, DepositChannel, - ExecutexSwapAndCall, SwapOrigin, TransferAssetParams, -}; -use cf_primitives::{ - chains::assets::eth, AssetAmount, ChannelId, DcaParameters, ForeignChain, SWAP_DELAY_BLOCKS, + CcmChannelMetadata, CcmFailReason, DepositChannel, ExecutexSwapAndCall, SwapOrigin, + TransferAssetParams, }; +use cf_primitives::{chains::assets::eth, AssetAmount, ChannelId, ForeignChain}; use cf_test_utilities::{assert_events_eq, assert_has_event, assert_has_matching_event}; use cf_traits::{ mocks::{ @@ -35,7 +33,7 @@ use cf_traits::{ swap_request_api::{MockSwapRequest, MockSwapRequestHandler}, }, BalanceApi, DepositApi, EgressApi, EpochInfo, FetchesTransfersLimitProvider, FundingInfo, - GetBlockHeight, SafeMode, ScheduledEgressDetails, SwapLimitsProvider, SwapRequestType, + GetBlockHeight, SafeMode, ScheduledEgressDetails, SwapRequestType, }; use frame_support::{ assert_err, assert_noop, assert_ok, @@ -468,104 +466,6 @@ fn reused_address_channel_id_matches() { }); } -#[test] -fn test_refund_parameter_validation() { - fn request_fok_swap(retry_duration: u32) -> Result<(), DispatchError> { - IngressEgress::request_swap_deposit_address( - eth::Asset::Flip, - Asset::Eth, - ForeignChainAddress::Eth(Default::default()), - Default::default(), - 1, - None, - 0, - Some(ChannelRefundParameters { - retry_duration, - refund_address: ForeignChainAddress::Eth(Default::default()), - min_price: Default::default(), - }), - None, - ) - .map(|_| ()) - } - - new_test_ext().execute_with(|| { - let max_swap_retry_duration_blocks = - ::SwapLimitsProvider::get_swap_limits() - .max_swap_retry_duration_blocks; - - assert_ok!(request_fok_swap(0)); - assert_ok!(request_fok_swap(max_swap_retry_duration_blocks)); - assert_err!( - request_fok_swap(max_swap_retry_duration_blocks + 1), - DispatchError::from(crate::Error::::SwapRetryDurationTooLong) - ); - }); -} - -#[test] -fn test_dca_parameter_validation() { - fn request_dca_swap(number_of_chunks: u32, chunk_interval: u32) -> Result<(), DispatchError> { - IngressEgress::request_swap_deposit_address( - eth::Asset::Flip, - Asset::Eth, - ForeignChainAddress::Eth(Default::default()), - Default::default(), - 1, - None, - 0, - None, - Some(DcaParameters { number_of_chunks, chunk_interval }), - ) - .map(|_| ()) - } - - new_test_ext().execute_with(|| { - const MIN_CHUNK_INTERVAL: u32 = SWAP_DELAY_BLOCKS; - let max_swap_request_duration_blocks = - ::SwapLimitsProvider::get_swap_limits() - .max_swap_request_duration_blocks; - - // Trivially ok - assert_ok!(request_dca_swap(1, MIN_CHUNK_INTERVAL)); - assert_ok!(request_dca_swap(2, MIN_CHUNK_INTERVAL)); - - // Equal to the limit - assert_ok!(request_dca_swap( - (max_swap_request_duration_blocks / MIN_CHUNK_INTERVAL) + 1, - MIN_CHUNK_INTERVAL - )); - assert_ok!(request_dca_swap(2, max_swap_request_duration_blocks)); - - // Limit is ignored because there is only 1 chunk - assert_ok!(request_dca_swap(1, max_swap_request_duration_blocks + 100)); - assert_ok!(request_dca_swap(1, 0)); - - // Exceeding limit - assert_err!( - request_dca_swap( - (max_swap_request_duration_blocks / MIN_CHUNK_INTERVAL) + 2, - MIN_CHUNK_INTERVAL - ), - DispatchError::from(crate::Error::::InvalidDcaParameters) - ); - assert_err!( - request_dca_swap(2, max_swap_request_duration_blocks + 1), - DispatchError::from(crate::Error::::InvalidDcaParameters) - ); - - // Below the minimum - assert_err!( - request_dca_swap(10, 1), - DispatchError::from(crate::Error::::InvalidDcaParameters) - ); - assert_err!( - request_dca_swap(0, MIN_CHUNK_INTERVAL), - DispatchError::from(crate::Error::::InvalidDcaParameters) - ); - }); -} - #[test] fn can_egress_ccm() { new_test_ext().execute_with(|| { @@ -1851,6 +1751,10 @@ fn can_request_swap_via_extrinsic() { INPUT_AMOUNT, MockAddressConverter::to_encoded_address(output_address.clone()), TX_HASH, + Box::new(DepositDetails { tx_hashes: None }), + None, + None, + 0, )); assert_eq!( @@ -1935,18 +1839,27 @@ fn rejects_invalid_swap_by_witnesser() { 10000, btc_encoded_address, Default::default(), + Box::new(DepositDetails { tx_hashes: None }), + None, + None, + 0 ),); // No swap request created -> the call was ignored assert!(MockSwapRequestHandler::::get_swap_requests().is_empty()); + // Invalid BTC address: assert_ok!(IngressEgress::contract_swap_request( RuntimeOrigin::root(), Asset::Eth, Asset::Btc, 10000, EncodedAddress::Btc(vec![0x41, 0x80, 0x41]), - Default::default() + Default::default(), + Box::new(DepositDetails { tx_hashes: None }), + None, + None, + 0 ),); assert!(MockSwapRequestHandler::::get_swap_requests().is_empty()); diff --git a/state-chain/pallets/cf-swapping/src/lib.rs b/state-chain/pallets/cf-swapping/src/lib.rs index 40158c9bc3..ed2524c2e9 100644 --- a/state-chain/pallets/cf-swapping/src/lib.rs +++ b/state-chain/pallets/cf-swapping/src/lib.rs @@ -672,6 +672,16 @@ pub mod pallet { ZeroSwapRetryDelayNotAllowed, /// Setting the max swap request duration to less than the swap delay is not allowed. MaxSwapRequestDurationTooShort, + /// Swap Retry duration is set above the max allowed. + RetryDurationTooHigh, + /// The number of DCA chunks must be greater than 0. + ZeroNumberOfChunksNotAllowed, + /// The chunk interval must be greater than the swap delay (2). + ChunkIntervalTooLow, + /// The total duration of a DCA swap request must be less then the max allowed. + SwapRequestDurationTooLong, + /// Invalid DCA parameters. + InvalidDcaParameters, } #[pallet::genesis_config] @@ -2275,6 +2285,37 @@ impl cf_traits::SwapLimitsProvider for Pallet { max_swap_request_duration_blocks: MaxSwapRequestDurationBlocks::::get(), } } + + fn validate_refund_params(retry_duration: u32) -> Result<(), DispatchError> { + let max_swap_retry_duration_blocks = MaxSwapRetryDurationBlocks::::get(); + if retry_duration > max_swap_retry_duration_blocks { + return Err(DispatchError::from(Error::::RetryDurationTooHigh)); + } + Ok(()) + } + + fn validate_dca_params(params: &cf_primitives::DcaParameters) -> Result<(), DispatchError> { + let max_swap_request_duration_blocks = MaxSwapRequestDurationBlocks::::get(); + + if params.number_of_chunks != 1 { + if params.number_of_chunks == 0 { + return Err(DispatchError::from(Error::::ZeroNumberOfChunksNotAllowed)); + } + if params.chunk_interval < SWAP_DELAY_BLOCKS { + return Err(DispatchError::from(Error::::ChunkIntervalTooLow)); + } + if let Some(total_swap_request_duration) = + params.number_of_chunks.saturating_sub(1).checked_mul(params.chunk_interval) + { + if total_swap_request_duration > max_swap_request_duration_blocks { + return Err(DispatchError::from(Error::::SwapRequestDurationTooLong)); + } + } else { + return Err(DispatchError::from(Error::::InvalidDcaParameters)); + } + } + Ok(()) + } } pub struct NoPendingSwaps(PhantomData); diff --git a/state-chain/pallets/cf-swapping/src/tests/dca.rs b/state-chain/pallets/cf-swapping/src/tests/dca.rs index 2fa9b6e6e7..3087cf1813 100644 --- a/state-chain/pallets/cf-swapping/src/tests/dca.rs +++ b/state-chain/pallets/cf-swapping/src/tests/dca.rs @@ -1,3 +1,5 @@ +use frame_support::assert_err; + use super::*; const CHUNK_INTERVAL: u32 = 3; @@ -1055,3 +1057,58 @@ fn test_minimum_chunk_size() { set_and_test_chunk_size(1, 1000, 1000, 0); }); } + +#[test] +fn test_dca_parameter_validation() { + use cf_traits::SwapLimitsProvider; + + fn validate_dca_params( + number_of_chunks: u32, + chunk_interval: u32, + ) -> Result<(), DispatchError> { + Swapping::validate_dca_params(&DcaParameters { number_of_chunks, chunk_interval }) + } + + new_test_ext().execute_with(|| { + const MIN_CHUNK_INTERVAL: u32 = SWAP_DELAY_BLOCKS; + let max_swap_request_duration_blocks = MaxSwapRequestDurationBlocks::::get(); + + // Trivially ok + assert_ok!(validate_dca_params(1, MIN_CHUNK_INTERVAL)); + assert_ok!(validate_dca_params(2, MIN_CHUNK_INTERVAL)); + + // Equal to the limit + assert_ok!(validate_dca_params( + (max_swap_request_duration_blocks / MIN_CHUNK_INTERVAL) + 1, + MIN_CHUNK_INTERVAL + )); + assert_ok!(validate_dca_params(2, max_swap_request_duration_blocks)); + + // Limit is ignored because there is only 1 chunk + assert_ok!(validate_dca_params(1, max_swap_request_duration_blocks + 100)); + assert_ok!(validate_dca_params(1, 0)); + + // Exceeding limit + assert_err!( + validate_dca_params( + (max_swap_request_duration_blocks / MIN_CHUNK_INTERVAL) + 2, + MIN_CHUNK_INTERVAL + ), + DispatchError::from(crate::Error::::SwapRequestDurationTooLong) + ); + assert_err!( + validate_dca_params(2, max_swap_request_duration_blocks + 1), + DispatchError::from(crate::Error::::SwapRequestDurationTooLong) + ); + + // Below the minimum + assert_err!( + validate_dca_params(10, 1), + DispatchError::from(crate::Error::::ChunkIntervalTooLow) + ); + assert_err!( + validate_dca_params(0, MIN_CHUNK_INTERVAL), + DispatchError::from(crate::Error::::ZeroNumberOfChunksNotAllowed) + ); + }); +} diff --git a/state-chain/pallets/cf-swapping/src/tests/fill_or_kill.rs b/state-chain/pallets/cf-swapping/src/tests/fill_or_kill.rs index 0b565e4cdc..0f932fa2d1 100644 --- a/state-chain/pallets/cf-swapping/src/tests/fill_or_kill.rs +++ b/state-chain/pallets/cf-swapping/src/tests/fill_or_kill.rs @@ -1,3 +1,5 @@ +use frame_support::assert_err; + use super::*; const BROKER_FEE: AssetAmount = INPUT_AMOUNT * BROKER_FEE_BPS as u128 / 10_000; @@ -539,3 +541,19 @@ fn fok_ccm_refunded_no_gas_swap() { ); }); } + +#[test] +fn test_refund_parameter_validation() { + use cf_traits::SwapLimitsProvider; + + new_test_ext().execute_with(|| { + let max_swap_retry_duration_blocks = MaxSwapRetryDurationBlocks::::get(); + + assert_ok!(Swapping::validate_refund_params(0)); + assert_ok!(Swapping::validate_refund_params(max_swap_retry_duration_blocks)); + assert_err!( + Swapping::validate_refund_params(max_swap_retry_duration_blocks + 1), + DispatchError::from(crate::Error::::RetryDurationTooHigh) + ); + }); +} diff --git a/state-chain/pallets/cf-threshold-signature/src/lib.rs b/state-chain/pallets/cf-threshold-signature/src/lib.rs index 850da1da15..4e74930079 100644 --- a/state-chain/pallets/cf-threshold-signature/src/lib.rs +++ b/state-chain/pallets/cf-threshold-signature/src/lib.rs @@ -18,7 +18,7 @@ mod response_status; use response_status::ResponseStatus; use codec::{Decode, Encode, MaxEncodedLen}; -use scale_info::TypeInfo; +use scale_info::{build::Fields, Path, Type, TypeInfo}; use cf_chains::ChainCrypto; use cf_primitives::{ @@ -247,8 +247,7 @@ pub mod pallet { }; use frame_system::ensure_none; /// Context for tracking the progress of a threshold signature ceremony. - #[derive(Clone, RuntimeDebug, PartialEq, Eq, Encode, Decode, TypeInfo)] - #[scale_info(skip_type_params(T, I))] + #[derive(Clone, RuntimeDebug, PartialEq, Eq, Encode, Decode)] pub struct CeremonyContext, I: 'static> { pub request_context: RequestContext, /// The respondents that have yet to reply. @@ -265,8 +264,7 @@ pub mod pallet { pub threshold_ceremony_type: ThresholdCeremonyType, } - #[derive(Clone, RuntimeDebug, PartialEq, Eq, Encode, Decode, TypeInfo)] - #[scale_info(skip_type_params(T, I))] + #[derive(Clone, RuntimeDebug, PartialEq, Eq, Encode, Decode)] pub struct RequestContext, I: 'static> { pub request_id: RequestId, /// The number of ceremonies attempted so far, excluding the current one. @@ -1195,6 +1193,88 @@ pub mod pallet { } } +macro_rules! append_chain_to_name { + ($name:ident) => { + match T::TargetChainCrypto::NAME { + "EVM" => concat!(stringify!($name), "EVM"), + "Polkadot" => concat!(stringify!($name), "Polkadot"), + "Bitcoin" => concat!(stringify!($name), "Bitcoin"), + "Solana" => concat!(stringify!($name), "Solana"), + _ => concat!(stringify!($name), "Other"), + } + }; +} + +impl TypeInfo for pallet::CeremonyContext +where + T: Config, + I: 'static, +{ + type Identity = Self; + fn type_info() -> Type { + Type::builder() + .path(Path::new(append_chain_to_name!(CeremonyContext), module_path!())) + .composite( + Fields::named() + .field(|f| { + f.ty::>() + .type_name(append_chain_to_name!(RequestContext)) + .name("request_context") + }) + .field(|f| { + f.ty::>() + .type_name("BTreeSet") + .name("remaining_respondents") + }) + .field(|f| { + f.ty::>() + .type_name("BTreeMap") + .name("blame_counts") + }) + .field(|f| { + f.ty::>() + .type_name("BTreeSet") + .name("candidates") + }) + .field(|f| f.ty::().type_name("EpochIndex").name("epoch")) + .field(|f| { + f.ty::<::AggKey>() + .type_name(append_chain_to_name!(Key)) + .name("key") + }) + .field(|f| { + f.ty::() + .type_name("ThresholdCeremonyType") + .name("threshold_ceremony_type") + }), + ) + } +} + +impl TypeInfo for pallet::RequestContext +where + T: Config, + I: 'static, +{ + type Identity = Self; + fn type_info() -> Type { + Type::builder() + .path(Path::new(append_chain_to_name!(RequestContext), module_path!())) + .composite( + Fields::named() + .field(|f| f.ty::().type_name("RequestId").name("request_id")) + .field(|f| { + f.ty::().type_name("AttemptCount").name("attempt_count") + }) + .field(|f| { + f.ty::>() + .type_name(append_chain_to_name!(PayloadFor)) + .name("payload") + }), + ) + } +} + impl, I: 'static> Pallet { /// Initiate a new signature request, returning the request id. fn inner_request_signature( diff --git a/state-chain/pallets/cf-validator/src/lib.rs b/state-chain/pallets/cf-validator/src/lib.rs index 98058b1417..405c9ad786 100644 --- a/state-chain/pallets/cf-validator/src/lib.rs +++ b/state-chain/pallets/cf-validator/src/lib.rs @@ -69,7 +69,7 @@ pub enum PalletConfigUpdate { type RuntimeRotationState = RotationState<::ValidatorId, ::Amount>; -pub const PALLET_VERSION: StorageVersion = StorageVersion::new(3); +pub const PALLET_VERSION: StorageVersion = StorageVersion::new(4); // Might be better to add the enum inside a struct rather than struct inside enum #[derive(Clone, PartialEq, Eq, Default, Encode, Decode, TypeInfo, RuntimeDebugNoBound)] @@ -1024,6 +1024,13 @@ impl Pallet { T::Bonder::update_bond(authority, EpochHistory::::active_bond(authority)); } T::EpochTransitionHandler::on_expired_epoch(epoch); + + let validators = HistoricalAuthorities::::take(epoch); + for validator in validators { + AuthorityIndex::::remove(epoch, validator); + } + HistoricalBonds::::remove(epoch); + T::ValidatorWeightInfo::expire_epoch(num_expired_authorities) } diff --git a/state-chain/pallets/cf-validator/src/migrations.rs b/state-chain/pallets/cf-validator/src/migrations.rs index 6bec8b3566..ea45a36cbf 100644 --- a/state-chain/pallets/cf-validator/src/migrations.rs +++ b/state-chain/pallets/cf-validator/src/migrations.rs @@ -1,4 +1,9 @@ use crate::Pallet; -use cf_runtime_upgrade_utilities::PlaceholderMigration; +use cf_runtime_upgrade_utilities::{PlaceholderMigration, VersionedMigration}; -pub type PalletMigration = (PlaceholderMigration, 3>,); +mod delete_old_epoch_data; + +pub type PalletMigration = ( + VersionedMigration, delete_old_epoch_data::Migration, 3, 4>, + PlaceholderMigration, 4>, +); diff --git a/state-chain/pallets/cf-validator/src/migrations/delete_old_epoch_data.rs b/state-chain/pallets/cf-validator/src/migrations/delete_old_epoch_data.rs new file mode 100644 index 0000000000..b2c62ced8f --- /dev/null +++ b/state-chain/pallets/cf-validator/src/migrations/delete_old_epoch_data.rs @@ -0,0 +1,118 @@ +use frame_support::{traits::OnRuntimeUpgrade, weights::Weight}; + +use crate::*; + +pub struct Migration(PhantomData); + +impl OnRuntimeUpgrade for Migration { + fn on_runtime_upgrade() -> Weight { + pub fn delete_all_old( + iter: Iter, + remove: Remove, + relevant: Relevant, + ) where + Iter: Fn() -> IterRes, + IterRes: Iterator, + Relevant: Fn(Item) -> Option, + Remove: Fn(Index), + { + let mut old_indices = Vec::new(); + for item in iter() { + if let Some(index) = relevant(item) { + old_indices.push(index); + } + } + for index in old_indices { + remove(index); + } + } + + let epoch = LastExpiredEpoch::::get(); + + delete_all_old( + HistoricalAuthorities::::iter, + HistoricalAuthorities::::remove, + |(e, _)| if e <= epoch { Some(e) } else { None }, + ); + delete_all_old(HistoricalBonds::::iter, HistoricalBonds::::remove, |(e, _)| { + if e <= epoch { + Some(e) + } else { + None + } + }); + delete_all_old( + AuthorityIndex::::iter, + |(e1, e2)| AuthorityIndex::::remove(e1, e2), + |(e, e2, _)| if e <= epoch { Some((e, e2)) } else { None }, + ); + + Weight::zero() + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, DispatchError> { + Ok(vec![]) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_state: Vec) -> Result<(), DispatchError> { + let epoch = LastExpiredEpoch::::get(); + + assert!(!HistoricalAuthorities::::iter().any(|(e, _)| e <= epoch)); + assert!(!HistoricalBonds::::iter().any(|(e, _)| e <= epoch)); + assert!(!AuthorityIndex::::iter().any(|(e, _, _)| e <= epoch)); + + Ok(()) + } +} + +#[cfg(test)] +mod migration_tests { + #[test] + fn test_migration() { + use super::*; + use crate::mock::*; + + new_test_ext().execute_with(|| { + let last_expired_epoch = 1000; + LastExpiredEpoch::::set(last_expired_epoch); + + // create some test values + HistoricalAuthorities::::set(last_expired_epoch - 2, vec![1, 2, 3]); + HistoricalAuthorities::::set(last_expired_epoch - 1, vec![4, 5]); + HistoricalAuthorities::::set(last_expired_epoch, vec![6, 7, 8, 9]); + HistoricalAuthorities::::set(last_expired_epoch + 1, vec![10, 11]); + + HistoricalBonds::::set(last_expired_epoch - 2, 100); + HistoricalBonds::::set(last_expired_epoch - 1, 101); + HistoricalBonds::::set(last_expired_epoch, 102); + HistoricalBonds::::set(last_expired_epoch + 1, 103); + + AuthorityIndex::::set(last_expired_epoch - 2, 1, Some(1)); + AuthorityIndex::::set(last_expired_epoch - 2, 2, Some(2)); + AuthorityIndex::::set(last_expired_epoch - 2, 3, Some(3)); + AuthorityIndex::::set(last_expired_epoch - 1, 1, Some(1)); + AuthorityIndex::::set(last_expired_epoch - 1, 2, Some(2)); + AuthorityIndex::::set(last_expired_epoch, 3, Some(1)); + AuthorityIndex::::set(last_expired_epoch, 1, Some(2)); + AuthorityIndex::::set(last_expired_epoch + 1, 2, Some(1)); + AuthorityIndex::::set(last_expired_epoch + 2, 3, Some(2)); + + #[cfg(feature = "try-runtime")] + let state = super::Migration::::pre_upgrade().unwrap(); + + // Perform runtime migration. + super::Migration::::on_runtime_upgrade(); + + #[cfg(feature = "try-runtime")] + super::Migration::::post_upgrade(state).unwrap(); + + // ensure that data which is not expired is kept + assert_eq!(HistoricalAuthorities::::get(last_expired_epoch + 1), vec![10, 11]); + assert_eq!(HistoricalBonds::::get(last_expired_epoch + 1), 103); + assert_eq!(AuthorityIndex::::get(last_expired_epoch + 1, 2), Some(1)); + assert_eq!(AuthorityIndex::::get(last_expired_epoch + 2, 3), Some(2)); + }); + } +} diff --git a/state-chain/pallets/cf-validator/src/tests.rs b/state-chain/pallets/cf-validator/src/tests.rs index d3d0aefc7c..9b6e9e794d 100644 --- a/state-chain/pallets/cf-validator/src/tests.rs +++ b/state-chain/pallets/cf-validator/src/tests.rs @@ -502,6 +502,39 @@ fn historical_epochs() { }); } +#[test] +fn expired_epoch_data_is_removed() { + new_test_ext().then_execute_with_checks(|| { + // Epoch 1 + EpochHistory::::activate_epoch(&ALICE, 1); + HistoricalAuthorities::::insert(1, Vec::from([ALICE])); + HistoricalBonds::::insert(1, 10); + // Epoch 2 + EpochHistory::::activate_epoch(&ALICE, 2); + HistoricalAuthorities::::insert(2, Vec::from([ALICE])); + HistoricalBonds::::insert(2, 30); + let authority_index = AuthorityIndex::::get(2, ALICE); + + // Expire + ValidatorPallet::expire_epoch(1); + + // Epoch 3 + EpochHistory::::activate_epoch(&ALICE, 3); + HistoricalAuthorities::::insert(3, Vec::from([ALICE])); + HistoricalBonds::::insert(3, 20); + + // Expect epoch 1's data to be deleted + assert!(AuthorityIndex::::try_get(1, ALICE).is_err()); + assert!(HistoricalAuthorities::::try_get(1).is_err()); + assert!(HistoricalBonds::::try_get(1).is_err()); + + // Expect epoch 2's data to be exist + assert_eq!(AuthorityIndex::::get(2, ALICE), authority_index); + assert_eq!(HistoricalAuthorities::::get(2), vec![ALICE]); + assert_eq!(HistoricalBonds::::get(2), 30); + }); +} + #[test] fn highest_bond() { new_test_ext().then_execute_with_checks(|| { diff --git a/state-chain/runtime/src/chainflip.rs b/state-chain/runtime/src/chainflip.rs index 6226784623..5f0e1dfcc0 100644 --- a/state-chain/runtime/src/chainflip.rs +++ b/state-chain/runtime/src/chainflip.rs @@ -31,7 +31,7 @@ use cf_chains::{ assets::any::ForeignChainAndAsset, btc::{ api::{BitcoinApi, SelectedUtxosAndChangeAmount, UtxoSelectionType}, - Bitcoin, BitcoinCrypto, BitcoinFeeInfo, BitcoinTransactionData, UtxoId, + Bitcoin, BitcoinCrypto, BitcoinFeeInfo, BitcoinTransactionData, BtcDepositDetails, UtxoId, }, dot::{ api::PolkadotApi, Polkadot, PolkadotAccountId, PolkadotCrypto, PolkadotReplayProtection, @@ -55,9 +55,9 @@ use cf_chains::{ SolAddress, SolAmount, SolApiEnvironment, SolanaCrypto, SolanaTransactionData, }, AnyChain, ApiCall, Arbitrum, CcmChannelMetadata, CcmDepositMetadata, Chain, ChainCrypto, - ChainEnvironment, ChainState, ChannelRefundParameters, DepositChannel, ForeignChain, - ReplayProtectionProvider, RequiresSignatureRefresh, SetCommKeyWithAggKey, SetGovKeyWithAggKey, - Solana, TransactionBuilder, + ChainEnvironment, ChainState, ChannelRefundParameters, ForeignChain, ReplayProtectionProvider, + RequiresSignatureRefresh, SetCommKeyWithAggKey, SetGovKeyWithAggKey, Solana, + TransactionBuilder, }; use cf_primitives::{ chains::assets, AccountRole, Asset, BasisPoints, Beneficiaries, ChannelId, DcaParameters, @@ -766,11 +766,14 @@ impl OnDeposit for DepositHandler {} impl OnDeposit for DepositHandler {} impl OnDeposit for DepositHandler { fn on_deposit_made( - utxo_id: ::DepositDetails, + deposit_details: BtcDepositDetails, amount: ::ChainAmount, - channel: &DepositChannel, ) { - Environment::add_bitcoin_utxo_to_list(amount, utxo_id, channel.state.clone()) + Environment::add_bitcoin_utxo_to_list( + amount, + deposit_details.utxo_id, + deposit_details.deposit_address, + ) } } impl OnDeposit for DepositHandler {} diff --git a/state-chain/runtime/src/chainflip/solana_elections.rs b/state-chain/runtime/src/chainflip/solana_elections.rs index ac26202983..df76a73893 100644 --- a/state-chain/runtime/src/chainflip/solana_elections.rs +++ b/state-chain/runtime/src/chainflip/solana_elections.rs @@ -1,10 +1,13 @@ use crate::{ Environment, Offence, Reputation, Runtime, SolanaBroadcaster, SolanaChainTracking, - SolanaThresholdSigner, + SolanaIngressEgress, SolanaThresholdSigner, }; use cf_chains::{ instances::ChainInstanceAlias, - sol::{SolAddress, SolAmount, SolHash, SolSignature, SolTrackedData, SolanaCrypto}, + sol::{ + api::SolanaTransactionType, SolAddress, SolAmount, SolHash, SolSignature, SolTrackedData, + SolanaCrypto, + }, Chain, FeeEstimationApi, ForeignChain, Solana, }; use cf_runtime_utilities::log_or_panic; @@ -143,6 +146,9 @@ impl OnCheckComplete<::ValidatorId> for OnCheckCompleteHoo #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, TypeInfo)] pub struct TransactionSuccessDetails { pub tx_fee: u64, + // It is possible for a contract call to be reverted due to contract's internal error. + // This field is set to `true` if the contract call executed successfully without error. + pub transaction_successful: bool, } pub struct SolanaEgressWitnessingHook; @@ -150,9 +156,22 @@ pub struct SolanaEgressWitnessingHook; impl OnEgressSuccess for SolanaEgressWitnessingHook { fn on_egress_success( signature: SolSignature, - TransactionSuccessDetails { tx_fee }: TransactionSuccessDetails, + TransactionSuccessDetails { tx_fee, transaction_successful }: TransactionSuccessDetails, ) { use cf_traits::KeyProvider; + if !transaction_successful { + // On CCM failure, we need to refund the user using their fallback info. + if let Some((broadcast_id, ccm_tx)) = + SolanaBroadcaster::pending_api_call_from_out_id(signature) + { + // Only Ccm calls support fallback. + if let SolanaTransactionType::CcmTransfer { fallback } = ccm_tx.call_type { + SolanaIngressEgress::do_ccm_fallback(broadcast_id, fallback); + } + } else { + log::error!("Ccm fallback failed: Reported Solana contract call revert, but the ApiCall does not exist in storage. Tx_out_id: : {:?}", signature); + } + } if let Err(err) = SolanaBroadcaster::egress_success( pallet_cf_witnesser::RawOrigin::CurrentEpochWitnessThreshold.into(), @@ -163,7 +182,11 @@ impl OnEgressSuccess for SolanaEgressWi (), signature, ) { - log::error!("Failed to execute egress success: {:?}", err); + log::error!( + "Failed to execute egress success: TxOutId: {:?}, Error: {:?}", + signature, + err + ) } } } diff --git a/state-chain/runtime/src/lib.rs b/state-chain/runtime/src/lib.rs index 81226256aa..20b321eac1 100644 --- a/state-chain/runtime/src/lib.rs +++ b/state-chain/runtime/src/lib.rs @@ -59,7 +59,10 @@ use codec::{alloc::string::ToString, Decode, Encode}; use core::ops::Range; use frame_support::{derive_impl, instances::*}; pub use frame_system::Call as SystemCall; -use migrations::add_liveness_electoral_system_solana::LivenessSettingsMigration; +use migrations::{ + add_liveness_electoral_system_solana::LivenessSettingsMigration, + solana_egress_success_witness::SolanaEgressSuccessWitnessMigration, +}; use pallet_cf_governance::GovCallHash; use pallet_cf_ingress_egress::{ ChannelAction, DepositWitness, IngressOrEgress, OwedAmount, TargetChainAsset, @@ -1277,10 +1280,18 @@ type MigrationsForV1_7 = ( 8, 9, >, - VersionedMigration, NoopUpgrade, 8, 9>, - VersionedMigration, NoopUpgrade, 8, 9>, - VersionedMigration, NoopUpgrade, 8, 9>, - VersionedMigration, NoopUpgrade, 8, 9>, + // For clearing all Solana Egress Success election votes, and migrating Solana ApiCall to the + // newer version. + VersionedMigration< + pallet_cf_broadcast::Pallet, + SolanaEgressSuccessWitnessMigration, + 9, + 10, + >, + VersionedMigration, NoopUpgrade, 8, 10>, + VersionedMigration, NoopUpgrade, 8, 10>, + VersionedMigration, NoopUpgrade, 8, 10>, + VersionedMigration, NoopUpgrade, 8, 10>, VersionedMigration< pallet_cf_elections::Pallet, LivenessSettingsMigration, @@ -2047,6 +2058,14 @@ impl_runtime_apis! { fn cf_pools() -> Vec> { LiquidityPools::pools() } + + fn cf_validate_dca_params(number_of_chunks: u32, chunk_interval: u32) -> Result<(), DispatchErrorWithMessage> { + pallet_cf_swapping::Pallet::::validate_dca_params(&cf_primitives::DcaParameters{number_of_chunks, chunk_interval}).map_err(Into::into) + } + + fn cf_validate_refund_params(retry_duration: u32) -> Result<(), DispatchErrorWithMessage> { + pallet_cf_swapping::Pallet::::validate_refund_params(retry_duration).map_err(Into::into) + } } impl monitoring_apis::MonitoringRuntimeApi for Runtime { diff --git a/state-chain/runtime/src/migrations.rs b/state-chain/runtime/src/migrations.rs index ba3424202f..03536d059b 100644 --- a/state-chain/runtime/src/migrations.rs +++ b/state-chain/runtime/src/migrations.rs @@ -4,3 +4,4 @@ pub mod add_liveness_electoral_system_solana; pub mod housekeeping; pub mod reap_old_accounts; pub mod serialize_solana_broadcast; +pub mod solana_egress_success_witness; diff --git a/state-chain/runtime/src/migrations/solana_egress_success_witness.rs b/state-chain/runtime/src/migrations/solana_egress_success_witness.rs new file mode 100644 index 0000000000..957b49b317 --- /dev/null +++ b/state-chain/runtime/src/migrations/solana_egress_success_witness.rs @@ -0,0 +1,150 @@ +use crate::{ + chainflip::solana_elections::SolanaElectoralSystem, Runtime, SolEnvironment, SolanaElections, + Weight, +}; +use cf_chains::{instances::SolanaInstance, sol::api::SolanaApi, TransferAssetParams}; +use frame_support::traits::OnRuntimeUpgrade; + +use pallet_cf_elections::{ + access_impls::ElectionAccess, electoral_system::ElectionWriteAccess, + electoral_systems::composite::tuple_6_impls::CompositeElectionIdentifierExtra, +}; + +use cf_chains::sol::{SolAddress, SolPubkey, SolTransaction}; + +#[allow(unused_imports)] +mod try_runtime_import { + pub use codec::{Decode, Encode}; + pub use sp_runtime::DispatchError; + pub use sp_std::vec::Vec; +} +#[cfg(feature = "try-runtime")] +use try_runtime_import::*; + +pub struct SolanaEgressSuccessWitnessMigration; + +mod old { + use super::*; + use frame_support::pallet_prelude::*; + + #[derive(Encode, Decode, TypeInfo)] + + pub struct SolanaApi { + pub call_type: SolanaTransactionType, + pub transaction: SolTransaction, + pub signer: Option, + pub _phantom: PhantomData, + } + #[derive(Encode, Decode, TypeInfo)] + pub enum SolanaTransactionType { + BatchFetch, + Transfer, + RotateAggKey, + CcmTransfer, + SetGovKeyWithAggKey, + } + + pub fn to_new_sol_transaction_type( + old: SolanaTransactionType, + ) -> cf_chains::sol::api::SolanaTransactionType { + // Use an invalid address and amount of 0 as fallback. + // Only CCMs submitted after the runtime upgrade support Fallback. + match old { + SolanaTransactionType::BatchFetch => + cf_chains::sol::api::SolanaTransactionType::BatchFetch, + SolanaTransactionType::Transfer => cf_chains::sol::api::SolanaTransactionType::Transfer, + SolanaTransactionType::RotateAggKey => + cf_chains::sol::api::SolanaTransactionType::RotateAggKey, + SolanaTransactionType::CcmTransfer => + cf_chains::sol::api::SolanaTransactionType::CcmTransfer { + fallback: TransferAssetParams { + asset: cf_chains::assets::sol::Asset::Sol, + to: SolPubkey([0x00; 32]).into(), + amount: Default::default(), + }, + }, + SolanaTransactionType::SetGovKeyWithAggKey => + cf_chains::sol::api::SolanaTransactionType::SetGovKeyWithAggKey, + } + } +} + +impl OnRuntimeUpgrade for SolanaEgressSuccessWitnessMigration { + fn on_runtime_upgrade() -> Weight { + log::info!("🥮 Running Solana Success witnessing migration."); + + // Clear Solana's egress-success votes. + let _ = + SolanaElections::with_electoral_access_and_identifiers(|_, election_identifiers| { + SolanaElectoralSystem::with_identifiers( + election_identifiers, + |election_identifiers| { + // Extract egress-success elections only. + let (_, _, _, _, egress_success_election_identifiers, ..) = + election_identifiers; + egress_success_election_identifiers.into_iter().for_each( + |election_identifier| { + ElectionAccess::::new( + election_identifier + .with_extra(CompositeElectionIdentifierExtra::< + (), + (), + u32, + (), + (), + (), + >::EE(())), + ) + .clear_votes() + }, + ); + Ok(()) + }, + ) + }); + + // Solana ApiCalls are stored in the broadcaster pallets. Add empty "fallback" info for + // existing Ccms. + pallet_cf_broadcast::PendingApiCalls::::translate_values::< + old::SolanaApi, + _, + >( + |old::SolanaApi:: { + call_type: old_call_type, + transaction, + signer, + _phantom, + }| { + Some(SolanaApi:: { + call_type: old::to_new_sol_transaction_type(old_call_type), + transaction, + signer, + _phantom, + }) + }, + ); + log::info!("Solana elections cleared, storage in Broadcaster pallet migrated."); + Weight::zero() + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { + Ok(Encode::encode( + &(pallet_cf_broadcast::PendingApiCalls::::iter_keys().count() + as u32), + )) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(state: Vec) -> Result<(), DispatchError> { + let pending_solana_calls = u32::decode(&mut &state[..]).unwrap_or_default(); + + assert_eq!( + pending_solana_calls, + pallet_cf_broadcast::PendingApiCalls::::iter_keys().count() + as u32 + ); + + Ok(()) + } +} diff --git a/state-chain/runtime/src/runtime_apis.rs b/state-chain/runtime/src/runtime_apis.rs index 509a99389a..5a29f5b22e 100644 --- a/state-chain/runtime/src/runtime_apis.rs +++ b/state-chain/runtime/src/runtime_apis.rs @@ -286,6 +286,11 @@ decl_runtime_apis!( fn cf_swap_limits() -> SwapLimits; fn cf_lp_events() -> Vec>; fn cf_minimum_chunk_size(asset: Asset) -> AssetAmount; + fn cf_validate_dca_params( + number_of_chunks: u32, + chunk_interval: u32, + ) -> Result<(), DispatchErrorWithMessage>; + fn cf_validate_refund_params(retry_duration: u32) -> Result<(), DispatchErrorWithMessage>; } ); diff --git a/state-chain/traits/src/lib.rs b/state-chain/traits/src/lib.rs index 8a8e73869f..67480a536b 100644 --- a/state-chain/traits/src/lib.rs +++ b/state-chain/traits/src/lib.rs @@ -23,7 +23,7 @@ use cf_chains::{ assets::any::AssetMap, sol::{SolAddress, SolHash}, ApiCall, CcmChannelMetadata, CcmDepositMetadata, Chain, ChainCrypto, ChannelRefundParameters, - DepositChannel, Ethereum, + Ethereum, }; use cf_primitives::{ AccountRole, Asset, AssetAmount, AuthorityCount, BasisPoints, Beneficiaries, BlockNumber, @@ -897,12 +897,7 @@ pub trait FlipBurnInfo { /// The trait implementation is intentionally no-op by default pub trait OnDeposit { - fn on_deposit_made( - _deposit_details: C::DepositDetails, - _amount: C::ChainAmount, - _channel: &DepositChannel, - ) { - } + fn on_deposit_made(_deposit_details: C::DepositDetails, _amount: C::ChainAmount) {} } pub trait NetworkEnvironmentProvider { @@ -994,6 +989,8 @@ pub struct SwapLimits { } pub trait SwapLimitsProvider { fn get_swap_limits() -> SwapLimits; + fn validate_dca_params(dca_params: &DcaParameters) -> Result<(), DispatchError>; + fn validate_refund_params(retry_duration: u32) -> Result<(), DispatchError>; } /// API for interacting with the asset-balance pallet. diff --git a/state-chain/traits/src/mocks/swap_limits_provider.rs b/state-chain/traits/src/mocks/swap_limits_provider.rs index 6058f30c2d..972f3decae 100644 --- a/state-chain/traits/src/mocks/swap_limits_provider.rs +++ b/state-chain/traits/src/mocks/swap_limits_provider.rs @@ -1,3 +1,5 @@ +use frame_support::sp_runtime::DispatchError; + use crate::{SwapLimits, SwapLimitsProvider}; pub struct MockSwapLimitsProvider; @@ -9,4 +11,35 @@ impl SwapLimitsProvider for MockSwapLimitsProvider { max_swap_request_duration_blocks: 14400_u32, } } + + fn validate_refund_params(retry_duration: u32) -> Result<(), DispatchError> { + let limits = Self::get_swap_limits(); + if retry_duration > limits.max_swap_retry_duration_blocks { + return Err(DispatchError::Other("Retry duration too high")); + } + Ok(()) + } + + fn validate_dca_params(params: &cf_primitives::DcaParameters) -> Result<(), DispatchError> { + let limits = Self::get_swap_limits(); + + if params.number_of_chunks != 1 { + if params.number_of_chunks == 0 { + return Err(DispatchError::Other("Zero number of chunks not allowed")); + } + if params.chunk_interval < cf_primitives::SWAP_DELAY_BLOCKS { + return Err(DispatchError::Other("Chunk interval too low")); + } + if let Some(total_swap_request_duration) = + params.number_of_chunks.saturating_sub(1).checked_mul(params.chunk_interval) + { + if total_swap_request_duration > limits.max_swap_request_duration_blocks { + return Err(DispatchError::Other("Swap request duration too long")); + } + } else { + return Err(DispatchError::Other("Invalid DCA parameters")); + } + } + Ok(()) + } }