Skip to content

Commit

Permalink
feat: improve Dot Existential Deposit Test (#4195)
Browse files Browse the repository at this point in the history
* added performAndTrackSwap:
which create and execute a swap tracking all the relative events

* added getSwapRate

* added chain to broadcastId:
we need to track the correct broadcaster, there can be different chain
with the same broadcastId

* add check before executing test

* address comments

* eslint

* added checks to make the test runnable concurrently

* added checks to make it run concurrently

* fix RpcAsset error

* addressed comments
  • Loading branch information
marcellorigotti authored Nov 8, 2023
1 parent b9a975e commit 9f336aa
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 109 deletions.
1 change: 0 additions & 1 deletion bouncer/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions bouncer/shared/perform_swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
observeCcmReceived,
assetToChain,
observeSwapScheduled,
observeSwapEvents,
observeBroadcastSuccess,
} from '../shared/utils';
import { CcmDepositMetadata } from '../shared/new_swap';

Expand Down Expand Up @@ -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!`);
}
6 changes: 5 additions & 1 deletion bouncer/shared/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
44 changes: 44 additions & 0 deletions bouncer/shared/swap_less_than_existential_deposit_dot.ts
Original file line number Diff line number Diff line change
@@ -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 ===');
}
140 changes: 139 additions & 1 deletion bouncer/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();

Expand Down Expand Up @@ -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<BroadcastId | undefined> {
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',
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
2 changes: 2 additions & 0 deletions bouncer/tests/all_concurrent_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ 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;
const broadcastAborted = observeBadEvents(':BroadcastAborted', () => stopObserving);
const feeDeficitRefused = observeBadEvents(':TransactionFeeDeficitRefused', () => stopObserving);

await Promise.all([
swapLessThanED(),
testAllSwaps(),
testEthereumDeposits(),
testFundRedeem('redeem'),
Expand Down
18 changes: 18 additions & 0 deletions bouncer/tests/swap_less_than_ED.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading

0 comments on commit 9f336aa

Please sign in to comment.