diff --git a/bouncer/run.sh b/bouncer/run.sh index dde62dacc8..f531bcb904 100755 --- a/bouncer/run.sh +++ b/bouncer/run.sh @@ -2,7 +2,6 @@ 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/shared/perform_swap.ts b/bouncer/shared/perform_swap.ts index 3109731229..dc788066c8 100644 --- a/bouncer/shared/perform_swap.ts +++ b/bouncer/shared/perform_swap.ts @@ -10,6 +10,8 @@ import { observeCcmReceived, assetToChain, observeSwapScheduled, + observeSwapEvents, + observeBroadcastSuccess, } from '../shared/utils'; import { CcmDepositMetadata } from '../shared/new_swap'; @@ -161,3 +163,26 @@ export async function performSwap( return swapParams; } + +// function to create a swap and track it until we detect the corresponding broadcast success +export async function performAndTrackSwap( + sourceAsset: Asset, + destAsset: Asset, + destAddress: string, + amount?: string, + tag?: string, +) { + const chainflipApi = await getChainflipApi(); + + const swapParams = await requestNewSwap(sourceAsset, destAsset, destAddress, tag); + + await send(sourceAsset, swapParams.depositAddress, amount); + console.log(`${tag} fund sent, waiting for the deposit to be witnessed..`); + + // SwapScheduled, SwapExecuted, SwapEgressScheduled, BatchBroadcastRequested + const broadcastId = await observeSwapEvents(swapParams, chainflipApi, tag); + + if (broadcastId) await observeBroadcastSuccess(broadcastId); + else throw new Error('Failed to retrieve broadcastId!'); + console.log(`${tag} broadcast executed succesfully, swap is complete!`); +} diff --git a/bouncer/shared/send.ts b/bouncer/shared/send.ts index f5dab323b5..e92487d90c 100644 --- a/bouncer/shared/send.ts +++ b/bouncer/shared/send.ts @@ -21,7 +21,11 @@ export async function send(asset: Asset, address: string, amount?: string) { case 'DOT': await sendDot(address, amount ?? defaultAssetAmounts(asset)); break; - case 'USDC': + case 'USDC': { + const contractAddress = getEthContractAddress(asset); + await sendErc20(address, contractAddress, amount ?? defaultAssetAmounts(asset)); + break; + } case 'FLIP': { const contractAddress = getEthContractAddress(asset); await sendErc20(address, contractAddress, amount ?? defaultAssetAmounts(asset)); diff --git a/bouncer/shared/swap_less_than_existential_deposit_dot.ts b/bouncer/shared/swap_less_than_existential_deposit_dot.ts new file mode 100755 index 0000000000..f14e8049c0 --- /dev/null +++ b/bouncer/shared/swap_less_than_existential_deposit_dot.ts @@ -0,0 +1,44 @@ +#!/usr/bin/env -S pnpm tsx +import { getDotBalance } from './get_dot_balance'; +import { performAndTrackSwap } from './perform_swap'; +import { getSwapRate, newAddress } from './utils'; + +const DOT_EXISTENTIAL_DEPOSIT = 1; + +export async function swapLessThanED() { + console.log('=== Testing USDC -> DOT swaps obtaining less than ED ==='); + const tag = `USDC -> DOT (less than ED)`; + + // we will try to swap with 5 USDC and check if the expected output is low enough + // otherwise we'll keep reducing the amount + let retry = true; + let inputAmount = '5'; + while (retry) { + let outputAmount = await getSwapRate('USDC', 'DOT', inputAmount); + + while (parseFloat(outputAmount) >= DOT_EXISTENTIAL_DEPOSIT) { + inputAmount = (parseFloat(inputAmount) / 2).toString(); + outputAmount = await getSwapRate('USDC', 'DOT', inputAmount); + } + console.log(`${tag} Input amount: ${inputAmount} USDC`); + console.log(`${tag} Approximate expected output amount: ${outputAmount} DOT`); + + // we want to be sure to have an address with 0 balance, hence we create a new one every time + const address = await newAddress( + 'DOT', + '!testing less than ED output for dot swaps!' + inputAmount + outputAmount, + ); + console.log(`${tag} Generated DOT address: ${address}`); + + await performAndTrackSwap('USDC', 'DOT', address, inputAmount, tag); + // if for some reason the balance after swapping is > 0 it means that the output was larger than + // ED, so we'll retry the test with a lower input + if (parseFloat(await getDotBalance(address)) > 0) { + console.log(`${tag}, swap output was more than ED, retrying with less...`); + inputAmount = (parseFloat(inputAmount) / 3).toString(); + } else { + retry = false; + } + } + console.log('=== Test USDC -> DOT swaps obtaining less than ED complete ==='); +} diff --git a/bouncer/shared/utils.ts b/bouncer/shared/utils.ts index 8377e741bf..8daae9b53c 100644 --- a/bouncer/shared/utils.ts +++ b/bouncer/shared/utils.ts @@ -3,7 +3,7 @@ import { setTimeout as sleep } from 'timers/promises'; import Client from 'bitcoin-core'; import { ApiPromise, WsProvider, Keyring } from '@polkadot/api'; import { Mutex } from 'async-mutex'; -import { Chain, Asset, assetChains, chainContractIds } from '@chainflip-io/cli'; +import { Chain, Asset, assetChains, chainContractIds, assetDecimals } from '@chainflip-io/cli'; import Web3 from 'web3'; import { u8aToHex } from '@polkadot/util'; import { newDotAddress } from './new_dot_address'; @@ -12,6 +12,7 @@ import { getBalance } from './get_balance'; import { newEthAddress } from './new_eth_address'; import { CcmDepositMetadata } from './new_swap'; import { getCFTesterAbi } from './eth_abis'; +import { SwapParams } from './perform_swap'; const cfTesterAbi = await getCFTesterAbi(); @@ -203,6 +204,98 @@ export async function observeEvent( return result as Event; } +type EgressId = [Chain, number]; +type BroadcastId = [Chain, number]; +// Observe multiple events related to the same swap that could be emitted in the same block +export async function observeSwapEvents( + { sourceAsset, destAsset, depositAddress, channelId }: SwapParams, + api: ApiPromise, + tag?: string, + swapType?: SwapType, + finalized = false, +): Promise { + let eventFound = false; + const subscribeMethod = finalized + ? api.rpc.chain.subscribeFinalizedHeads + : api.rpc.chain.subscribeNewHeads; + + const swapScheduledEvent = 'SwapScheduled'; + const swapExecutedEvent = 'SwapExecuted'; + const swapEgressScheduled = 'SwapEgressScheduled'; + const batchBroadcastRequested = 'BatchBroadcastRequested'; + let expectedMethod = swapScheduledEvent; + + let swapId = 0; + let egressId: EgressId; + let broadcastId; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const unsubscribe: any = await subscribeMethod(async (header) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const events: any[] = await api.query.system.events.at(header.hash); + events.forEach((record) => { + const { event } = record; + if (!eventFound && event.method.includes(expectedMethod)) { + const expectedEvent = { + data: event.toHuman().data, + }; + + switch (expectedMethod) { + case swapScheduledEvent: + if ('DepositChannel' in expectedEvent.data.origin) { + if ( + Number(expectedEvent.data.origin.DepositChannel.channelId) === channelId && + sourceAsset === (expectedEvent.data.sourceAsset.toUpperCase() as Asset) && + destAsset === (expectedEvent.data.destinationAsset.toUpperCase() as Asset) && + swapType + ? expectedEvent.data.swapType[swapType] !== undefined + : true && + depositAddress === + (Object.values( + expectedEvent.data.origin.DepositChannel.depositAddress, + )[0] as string) + ) { + expectedMethod = swapExecutedEvent; + swapId = expectedEvent.data.swapId; + console.log(`${tag} swap scheduled with swapId: ${swapId}`); + } + } + break; + case swapExecutedEvent: + if (Number(expectedEvent.data.swapId) === Number(swapId)) { + expectedMethod = swapEgressScheduled; + console.log(`${tag} swap executed, with id: ${swapId}`); + } + break; + case swapEgressScheduled: + if (Number(expectedEvent.data.swapId) === Number(swapId)) { + expectedMethod = batchBroadcastRequested; + egressId = expectedEvent.data.egressId as EgressId; + console.log(`${tag} swap egress scheduled with id: (${egressId[0]}, ${egressId[1]})`); + } + break; + case batchBroadcastRequested: + expectedEvent.data.egressIds.forEach((eventEgressId: EgressId) => { + if (egressId[0] === eventEgressId[0] && egressId[1] === eventEgressId[1]) { + broadcastId = [egressId[0], Number(expectedEvent.data.broadcastId)] as BroadcastId; + console.log(`${tag} broadcast requested, with id: (${broadcastId})`); + eventFound = true; + unsubscribe(); + } + }); + break; + default: + break; + } + } + }); + }); + while (!eventFound) { + await sleep(1000); + } + return broadcastId; +} + // TODO: To import from the SDK once it's exported export enum SwapType { Swap = 'Swap', @@ -250,6 +343,30 @@ export async function observeBadEvents( } } +export async function observeBroadcastSuccess(broadcastId: BroadcastId) { + const chainflipApi = await getChainflipApi(); + const broadcaster = broadcastId[0].toLowerCase() + 'Broadcaster'; + const broadcastIdNumber = broadcastId[1]; + + let stopObserving = false; + const observeBroadcastFailure = observeBadEvents( + broadcaster + ':BroadcastAborted', + () => stopObserving, + (event) => { + if (broadcastIdNumber === Number(event.data.broadcastId)) return true; + return false; + }, + ); + + await observeEvent(broadcaster + ':BroadcastSuccess', chainflipApi, (event) => { + if (broadcastIdNumber === Number(event.data.broadcastId)) return true; + return false; + }); + + stopObserving = true; + await observeBroadcastFailure; +} + export async function newAddress( asset: Asset, seed: string, @@ -479,3 +596,24 @@ export function compareSemVer(version1: string, version2: string) { return 'equal'; } + +type SwapRate = { + intermediary: string; + output: string; +}; +export async function getSwapRate(from: Asset, to: Asset, fromAmount: string) { + const chainflipApi = await getChainflipApi(); + + const fineFromAmount = amountToFineAmount(fromAmount, assetDecimals[from]); + const hexPrice = (await chainflipApi.rpc( + 'cf_swap_rate', + from, + to, + Number(fineFromAmount).toString(16), + )) as SwapRate; + + const finePriceOutput = parseInt(hexPrice.output); + const outputPrice = fineAmountToAmount(finePriceOutput.toString(), assetDecimals[to]); + + return outputPrice; +} diff --git a/bouncer/tests/all_concurrent_tests.ts b/bouncer/tests/all_concurrent_tests.ts index 7b37db9f5d..9f189ec8ca 100755 --- a/bouncer/tests/all_concurrent_tests.ts +++ b/bouncer/tests/all_concurrent_tests.ts @@ -5,6 +5,7 @@ import { runWithTimeout, observeBadEvents } from '../shared/utils'; import { testFundRedeem } from '../shared/fund_redeem'; import { testMultipleMembersGovernance } from '../shared/multiple_members_governance'; import { testLpApi } from '../shared/lp_api_test'; +import { swapLessThanED } from '../shared/swap_less_than_existential_deposit_dot'; async function runAllConcurrentTests() { let stopObserving = false; @@ -12,6 +13,7 @@ async function runAllConcurrentTests() { const feeDeficitRefused = observeBadEvents(':TransactionFeeDeficitRefused', () => stopObserving); await Promise.all([ + swapLessThanED(), testAllSwaps(), testEthereumDeposits(), testFundRedeem('redeem'), diff --git a/bouncer/tests/swap_less_than_ED.ts b/bouncer/tests/swap_less_than_ED.ts new file mode 100755 index 0000000000..96f85446b7 --- /dev/null +++ b/bouncer/tests/swap_less_than_ED.ts @@ -0,0 +1,18 @@ +#!/usr/bin/env -S pnpm tsx +import { swapLessThanED } from '../shared/swap_less_than_existential_deposit_dot'; +import { runWithTimeout } from '../shared/utils'; + +async function main() { + await swapLessThanED(); + process.exit(0); +} + +runWithTimeout(main(), 300000) + .then(() => { + // there are some dangling resources that prevent the process from exiting + process.exit(0); + }) + .catch((error) => { + console.error(error); + process.exit(-1); + }); diff --git a/bouncer/tests/swap_less_than_existential_deposit_dot.ts b/bouncer/tests/swap_less_than_existential_deposit_dot.ts deleted file mode 100755 index 6eed76f173..0000000000 --- a/bouncer/tests/swap_less_than_existential_deposit_dot.ts +++ /dev/null @@ -1,106 +0,0 @@ -#!/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); - });