diff --git a/bouncer/run.sh b/bouncer/run.sh index f531bcb904..dde62dacc8 100755 --- a/bouncer/run.sh +++ b/bouncer/run.sh @@ -2,6 +2,7 @@ set -e ./commands/observe_block.ts 5 ./commands/setup_vaults.ts ./commands/setup_swaps.ts +./tests/swap_less_than_existential_deposit_dot.ts ./tests/gaslimit_ccm.ts ./tests/all_concurrent_tests.ts ./tests/rotates_through_btc_swap.ts diff --git a/bouncer/tests/swap_less_than_existential_deposit_dot.ts b/bouncer/tests/swap_less_than_existential_deposit_dot.ts new file mode 100755 index 0000000000..6eed76f173 --- /dev/null +++ b/bouncer/tests/swap_less_than_existential_deposit_dot.ts @@ -0,0 +1,106 @@ +#!/usr/bin/env -S pnpm tsx +import assert from 'assert'; +import { getBalance } from '../shared/get_balance'; +import { CcmDepositMetadata } from '../shared/new_swap'; +import { SwapParams, requestNewSwap } from '../shared/perform_swap'; +import { sendDot } from '../shared/send_dot'; +import { sendErc20 } from '../shared/send_erc20'; +import { + newAddress, + getChainflipApi, + observeEvent, + observeSwapScheduled, + observeCcmReceived, + observeBalanceIncrease, + getEthContractAddress, + observeBadEvents, + runWithTimeout, +} from '../shared/utils'; + +// This code is duplicated to allow us to specify a specific amount we want to swap +// and to wait for some specific events +export async function doPerformSwap( + { sourceAsset, destAsset, destAddress, depositAddress, channelId }: SwapParams, + amount: string, + balanceIncrease: boolean, + tag = '', + messageMetadata?: CcmDepositMetadata, +) { + const oldBalance = await getBalance(destAsset, destAddress); + + console.log(`${tag} Old balance: ${oldBalance}`); + + const swapScheduledHandle = observeSwapScheduled(sourceAsset, destAsset, channelId); + + const ccmEventEmitted = messageMetadata + ? observeCcmReceived(sourceAsset, destAsset, destAddress, messageMetadata) + : Promise.resolve(); + + const contractAddress = getEthContractAddress('USDC'); + await sendErc20(depositAddress, contractAddress, amount); + + console.log(`${tag} Funded the address`); + + await swapScheduledHandle; + + console.log(`${tag} Waiting for balance to update`); + + if (!balanceIncrease) { + const api = await getChainflipApi(); + await observeEvent('polkadotBroadcaster:BroadcastSuccess', api); + + const newBalance = await getBalance(destAsset, destAddress); + + assert.strictEqual(newBalance, oldBalance, 'Balance should not have changed'); + console.log(`${tag} Swap success! Balance (Same as before): ${newBalance}!`); + } else { + try { + const [newBalance] = await Promise.all([ + observeBalanceIncrease(destAsset, destAddress, oldBalance), + ccmEventEmitted, + ]); + + console.log(`${tag} Swap success! New balance: ${newBalance}!`); + } catch (err) { + throw new Error(`${tag} ${err}`); + } + } +} + +export async function swapLessThanED() { + console.log('=== Testing USDC -> DOT swaps obtaining less than ED ==='); + + let stopObserving = false; + const observingBadEvents = observeBadEvents(':BroadcastAborted', () => stopObserving); + + // The initial price is 10USDC = 1DOT, + // we will swap only 5 USDC and check that the swap is completed successfully + const tag = `USDC -> DOT (less than ED)`; + const address = await newAddress('DOT', 'random seed'); + + console.log('Generated DOT address: ' + address); + const swapParams = await requestNewSwap('USDC', 'DOT', address, tag); + await doPerformSwap(swapParams, '5', false, tag); + + await sendDot(address, '50'); + console.log('Account funded, new balance: ' + (await getBalance('DOT', address))); + + // We will then send some dot to the address and perform another swap with less than ED + const tag2 = `USDC -> DOT (to active account)`; + const swapParams2 = await requestNewSwap('USDC', 'DOT', address, tag2); + await doPerformSwap(swapParams2, '5', true, tag2); + + stopObserving = true; + await observingBadEvents; + + console.log('=== Test complete ==='); +} + +runWithTimeout(swapLessThanED(), 500000) + .then(() => { + process.exit(0); + }) + .catch((error) => { + console.error(error); + process.exit(-1); + }); diff --git a/engine/metadata.polkadot.scale b/engine/metadata.polkadot.scale index bceb884c29..5a121627e4 100644 Binary files a/engine/metadata.polkadot.scale and b/engine/metadata.polkadot.scale differ diff --git a/engine/src/witness/dot.rs b/engine/src/witness/dot.rs index 08d093c700..8446ede524 100644 --- a/engine/src/witness/dot.rs +++ b/engine/src/witness/dot.rs @@ -39,7 +39,7 @@ use super::common::{ }; // To generate the metadata file, use the subxt-cli tool (`cargo install subxt-cli`): -// subxt metadata --format=json --pallets Proxy,Balances,TransactionPayment --url +// subxt metadata --format=json --pallets Proxy,Balances,TransactionPayment,System --url // wss://polkadot-rpc.dwellir.com:443 > metadata.polkadot.json.scale #[subxt::subxt(runtime_metadata_path = "metadata.polkadot.scale")] pub mod polkadot {} @@ -49,10 +49,11 @@ pub enum EventWrapper { ProxyAdded { delegator: AccountId32, delegatee: AccountId32 }, Transfer { to: AccountId32, from: AccountId32, amount: PolkadotBalance }, TransactionFeePaid { actual_fee: PolkadotBalance, tip: PolkadotBalance }, + ExtrinsicSuccess, } use polkadot::{ - balances::events::Transfer, proxy::events::ProxyAdded, + balances::events::Transfer, proxy::events::ProxyAdded, system::events::ExtrinsicSuccess, transaction_payment::events::TransactionFeePaid, }; @@ -76,6 +77,11 @@ pub fn filter_map_events( event_details.as_event::().unwrap().unwrap(); Some(EventWrapper::TransactionFeePaid { actual_fee, tip }) }, + (ExtrinsicSuccess::PALLET, ExtrinsicSuccess::EVENT) => { + let ExtrinsicSuccess { .. } = + event_details.as_event::().unwrap().unwrap(); + Some(EventWrapper::ExtrinsicSuccess) + }, _ => None, } .map(|event| (event_details.phase(), event)), @@ -88,7 +94,7 @@ pub fn filter_map_events( pub async fn proxy_added_witnessing( epoch: Vault, - header: Header, BTreeSet)>, + header: Header>, process_call: ProcessCall, ) -> (Vec<(Phase, EventWrapper)>, BTreeSet) where @@ -99,17 +105,16 @@ where + 'static, ProcessingFut: Future + Send + 'static, { - let (events, mut broadcast_indices) = header.data; + let events = header.data; - let (vault_key_rotated_calls, mut proxy_added_broadcasts) = + let (vault_key_rotated_calls, proxy_added_broadcasts) = proxy_addeds(header.index, &events, &epoch.info.1); - broadcast_indices.append(&mut proxy_added_broadcasts); for call in vault_key_rotated_calls { process_call(call, epoch.index).await; } - (events, broadcast_indices) + (events, proxy_added_broadcasts) } #[allow(clippy::type_complexity)] @@ -130,11 +135,15 @@ pub async fn process_egress( + 'static, ProcessingFut: Future + Send + 'static, { - let ((events, broadcast_indices), monitored_egress_ids) = header.data; + let ((events, mut extrinsic_indices), monitored_egress_ids) = header.data; + + // To guarantee witnessing egress, we are interested in all extrinsics that were successful + extrinsic_indices.extend(extrinsic_success_indices(&events)); - let extrinsics = dot_client.extrinsics(header.hash).await; + let extrinsics: Vec = + dot_client.extrinsics(header.hash).await; - for (extrinsic_index, tx_fee) in transaction_fee_paids(&broadcast_indices, &events) { + for (extrinsic_index, tx_fee) in transaction_fee_paids(&extrinsic_indices, &events) { let xt = extrinsics.get(extrinsic_index as usize).expect( "We know this exists since we got this index from the event, from the block we are querying.", @@ -275,19 +284,29 @@ where fn transaction_fee_paids( indices: &BTreeSet, - events: &Vec<(Phase, EventWrapper)>, + events: &[(Phase, EventWrapper)], ) -> BTreeSet<(PolkadotExtrinsicIndex, PolkadotBalance)> { - let mut indices_with_fees = BTreeSet::new(); - for (phase, wrapped_event) in events { - if let Phase::ApplyExtrinsic(extrinsic_index) = phase { - if indices.contains(extrinsic_index) { - if let EventWrapper::TransactionFeePaid { actual_fee, .. } = wrapped_event { - indices_with_fees.insert((*extrinsic_index, *actual_fee)); - } - } - } - } - indices_with_fees + events + .iter() + .filter_map(|(phase, wrapped_event)| match (phase, wrapped_event) { + ( + Phase::ApplyExtrinsic(extrinsic_index), + EventWrapper::TransactionFeePaid { actual_fee, .. }, + ) if indices.contains(extrinsic_index) => Some((*extrinsic_index, *actual_fee)), + _ => None, + }) + .collect() +} + +fn extrinsic_success_indices(events: &[(Phase, EventWrapper)]) -> BTreeSet { + events + .iter() + .filter_map(|(phase, wrapped_event)| match (phase, wrapped_event) { + (Phase::ApplyExtrinsic(extrinsic_index), EventWrapper::ExtrinsicSuccess) => + Some(*extrinsic_index), + _ => None, + }) + .collect() } fn proxy_addeds( @@ -363,11 +382,23 @@ pub mod test { (3u32, mock_tx_fee_paid(20000)), ]); - let (vault_key_rotated_calls, broadcast_indices) = + let (vault_key_rotated_calls, extrinsic_indices) = proxy_addeds(20, &block_event_details, &our_vault); assert_eq!(vault_key_rotated_calls.len(), 1); - assert_eq!(broadcast_indices.len(), 1); - assert!(broadcast_indices.contains(&our_proxy_added_index)); + assert_eq!(extrinsic_indices.len(), 1); + assert!(extrinsic_indices.contains(&our_proxy_added_index)); + } + + #[tokio::test] + async fn test_extrinsic_success_filtering() { + let events = phase_and_events(vec![ + (1u32, EventWrapper::ExtrinsicSuccess), + (2u32, mock_tx_fee_paid(20000)), + (2u32, EventWrapper::ExtrinsicSuccess), + (3u32, mock_tx_fee_paid(20000)), + ]); + + assert_eq!(extrinsic_success_indices(&events), BTreeSet::from([1, 2])); } } diff --git a/engine/src/witness/dot/dot_deposits.rs b/engine/src/witness/dot/dot_deposits.rs index 6cd7a45350..71a63a7e9b 100644 --- a/engine/src/witness/dot/dot_deposits.rs +++ b/engine/src/witness/dot/dot_deposits.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeSet; - use cf_primitives::{EpochIndex, PolkadotBlockNumber}; use futures_core::Future; use pallet_cf_ingress_egress::{DepositChannelDetails, DepositWitness}; @@ -17,7 +15,7 @@ use crate::witness::{ }; use cf_chains::{ assets::dot::Asset, - dot::{PolkadotAccountId, PolkadotExtrinsicIndex, PolkadotHash}, + dot::{PolkadotAccountId, PolkadotHash}, Polkadot, }; use subxt::events::Phase; @@ -30,7 +28,7 @@ impl ChunkedByVaultBuilder { impl ChunkedByVault< Index = PolkadotBlockNumber, Hash = PolkadotHash, - Data = (Vec<(Phase, EventWrapper)>, BTreeSet), + Data = Vec<(Phase, EventWrapper)>, Chain = Polkadot, ExtraInfo = PolkadotAccountId, ExtraHistoricInfo = (), @@ -62,8 +60,7 @@ impl ChunkedByVaultBuilder { let addresses = address_and_details_to_addresses(addresses_and_details); - let (deposit_witnesses, broadcast_indices) = - deposit_witnesses(header.index, addresses, &events, &epoch.info.1); + let deposit_witnesses = deposit_witnesses(addresses, &events); if !deposit_witnesses.is_empty() { process_call( @@ -77,7 +74,7 @@ impl ChunkedByVaultBuilder { .await } - (events, broadcast_indices) + events } }) } @@ -98,16 +95,13 @@ fn address_and_details_to_addresses( // Return the deposit witnesses and the extrinsic indices of transfers we want // to confirm the broadcast of. fn deposit_witnesses( - block_number: PolkadotBlockNumber, monitored_addresses: Vec, events: &Vec<(Phase, EventWrapper)>, - our_vault: &PolkadotAccountId, -) -> (Vec>, BTreeSet) { +) -> Vec> { let mut deposit_witnesses = vec![]; - let mut extrinsic_indices = BTreeSet::new(); for (phase, wrapped_event) in events { - if let Phase::ApplyExtrinsic(extrinsic_index) = phase { - if let EventWrapper::Transfer { to, amount, from } = wrapped_event { + if let Phase::ApplyExtrinsic(_extrinsic_index) = phase { + if let EventWrapper::Transfer { to, amount, from: _ } = wrapped_event { let deposit_address = PolkadotAccountId::from_aliased(to.0); if monitored_addresses.contains(&deposit_address) { deposit_witnesses.push(DepositWitness { @@ -117,20 +111,10 @@ fn deposit_witnesses( deposit_details: (), }); } - // It's possible a transfer to one of the monitored addresses comes from our_vault, - // so this cannot be an else if - if &PolkadotAccountId::from_aliased(from.0) == our_vault || - &deposit_address == our_vault - { - tracing::info!( - "Interesting transfer at block: {block_number}, extrinsic index: {extrinsic_index} from: {from:?} to: {to:?}", - ); - extrinsic_indices.insert(*extrinsic_index); - } } } } - (deposit_witnesses, extrinsic_indices) + deposit_witnesses } #[cfg(test)] @@ -157,7 +141,7 @@ mod test { fn witness_deposits_for_addresses_we_monitor() { let our_vault = PolkadotAccountId::from_aliased([0; 32]); - // we want two monitors, one sent through at start, and one sent through channel + // We want two monitors, one sent through at start, and one sent through channel const TRANSFER_1_INDEX: u32 = 1; let transfer_1_deposit_address = PolkadotAccountId::from_aliased([1; 32]); const TRANSFER_1_AMOUNT: PolkadotBalance = 10000; @@ -167,7 +151,7 @@ mod test { const TRANSFER_2_AMOUNT: PolkadotBalance = 20000; const TRANSFER_FROM_OUR_VAULT_INDEX: u32 = 7; - const TRANFER_TO_OUR_VAULT_INDEX: u32 = 8; + const TRANSFER_TO_OUR_VAULT_INDEX: u32 = 8; const TRANSFER_TO_SELF_INDEX: u32 = 9; const TRANSFER_TO_SELF_AMOUNT: PolkadotBalance = 30000; @@ -205,35 +189,27 @@ mod test { mock_transfer(&our_vault, &PolkadotAccountId::from_aliased([9; 32]), 93232), ), ( - TRANFER_TO_OUR_VAULT_INDEX, + TRANSFER_TO_OUR_VAULT_INDEX, mock_transfer(&PolkadotAccountId::from_aliased([9; 32]), &our_vault, 93232), ), // Example: Someone generates a DOT -> ETH swap, getting the DOT address that we're now // monitoring for inputs. They now generate a BTC -> DOT swap, and set the destination // address of the DOT to the address they generated earlier. - // Now our Polakdot vault is sending to an address we're monitoring for deposits. + // Now our Polkadot vault is sending to an address we're monitoring for deposits. ( TRANSFER_TO_SELF_INDEX, mock_transfer(&our_vault, &transfer_2_deposit_address, TRANSFER_TO_SELF_AMOUNT), ), ]); - let (deposit_witnesses, broadcast_indices) = deposit_witnesses( - 32, + let deposit_witnesses = deposit_witnesses( vec![transfer_1_deposit_address, transfer_2_deposit_address], &block_event_details, - &our_vault, ); assert_eq!(deposit_witnesses.len(), 3); assert_eq!(deposit_witnesses.get(0).unwrap().amount, TRANSFER_1_AMOUNT); assert_eq!(deposit_witnesses.get(1).unwrap().amount, TRANSFER_2_AMOUNT); assert_eq!(deposit_witnesses.get(2).unwrap().amount, TRANSFER_TO_SELF_AMOUNT); - - // Check the egress and ingress fetch - assert_eq!(broadcast_indices.len(), 3); - assert!(broadcast_indices.contains(&TRANSFER_FROM_OUR_VAULT_INDEX)); - assert!(broadcast_indices.contains(&TRANFER_TO_OUR_VAULT_INDEX)); - assert!(broadcast_indices.contains(&TRANSFER_TO_SELF_INDEX)); } }