Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Extend Evm Vault swaps functionality #5344

Merged
merged 43 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
6f26b19
feat: witnessing btc smart contract swaps
msgmaxim Oct 15, 2024
a7fd6d4
Merge branch 'main' into feat/btc-smart-contract-witnessing
msgmaxim Oct 16, 2024
d0446ae
chore: address minor review comments
msgmaxim Oct 16, 2024
9c2e4c3
chore: start implementation
albert-llimos Oct 16, 2024
6557606
test: fix deposit witnessing tests
msgmaxim Oct 17, 2024
e682f16
chore: add intial scale encoding for cfParameters
albert-llimos Oct 17, 2024
115342e
chore: improve logic
albert-llimos Oct 17, 2024
4256a5b
fix: address RuntimeCall size limit
msgmaxim Oct 18, 2024
7f91a99
Merge branch 'main' into feat/btc-smart-contract-witnessing
msgmaxim Oct 18, 2024
4234046
chore: address clippy
msgmaxim Oct 18, 2024
bd67b36
chore: more cleanup and refactoring
albert-llimos Oct 18, 2024
b510b81
chore: add contract swaps to dca test
albert-llimos Oct 18, 2024
11d4fa9
chore: merge from base
albert-llimos Oct 18, 2024
f436393
chore: engine refactor
albert-llimos Oct 18, 2024
e27808c
chore: refactor createEvmWallet
albert-llimos Oct 21, 2024
3f90224
chore: merge from main
albert-llimos Oct 21, 2024
d14c40d
chore: cleanup
albert-llimos Oct 21, 2024
7a80f89
chore: add MAX_VAULT_SWAP_ATTRIBUTES_LENGTH
albert-llimos Oct 21, 2024
cd8fa68
chore: refactor bouncer
albert-llimos Oct 21, 2024
da74dbb
chore: refactor into common for reusal
albert-llimos Oct 21, 2024
1851094
chore: rename attributes to parameters
albert-llimos Oct 21, 2024
7305042
chore: pass extra parameters to sdk
albert-llimos Oct 21, 2024
f9e67ab
chore: fix issues
albert-llimos Oct 22, 2024
de1bf99
chore: engine renaming
albert-llimos Oct 22, 2024
5eba5c7
chore: update SDK with new encoding logic
albert-llimos Oct 23, 2024
b14cb63
chore: remove unnecessary bouncer ts-scale
albert-llimos Oct 23, 2024
c06e294
chore: lint
albert-llimos Oct 23, 2024
854cbaf
chore: fix merge conflicts
albert-llimos Oct 23, 2024
c621a7d
chore: update to right name
albert-llimos Oct 23, 2024
2381415
chore: fix missing rename
albert-llimos Oct 23, 2024
cfe63fd
chore: add beneficiares and make FoK mandatory
albert-llimos Oct 23, 2024
399846f
chore: lint
albert-llimos Oct 23, 2024
d77a0da
chore: update broker_fees and cli
albert-llimos Oct 23, 2024
ea61dce
chore: update with hardcoded cfParameters
albert-llimos Oct 24, 2024
2282405
chore: bump sdk
albert-llimos Oct 24, 2024
de719c5
fix: simplify cf params decoding
dandanlen Oct 24, 2024
e3b8314
chore: downgrade error -> warning
dandanlen Oct 24, 2024
1d9b88e
chore: merge form main and fix conflicts
albert-llimos Oct 25, 2024
785ab48
chore: bump sdk with new broker_fees type
albert-llimos Oct 25, 2024
2033df7
chore: fix failing test
albert-llimos Oct 25, 2024
99310e5
chore: bump sdk
albert-llimos Oct 25, 2024
2d070ea
chore: lint
albert-llimos Oct 25, 2024
3bd922a
Merge commit 'd4eff081774475a80b140dc554bb3ae6ebf121b3' into feat/evm…
albert-llimos Oct 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bouncer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"md5": "^2.3.0",
"minimist": "^1.2.8",
"rxjs": "^7.8.1",
"scale-ts": "1.6.0",
"tiny-secp256k1": "^2.2.1",
"toml": "^3.0.0",
"web3": "^1.9.0",
Expand Down
6 changes: 4 additions & 2 deletions bouncer/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

129 changes: 100 additions & 29 deletions bouncer/shared/contract_swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,135 @@ import {
approveVault,
Asset as SCAsset,
Chains,
InternalAsset,
Chain,
} from '@chainflip/cli';
import { HDNodeWallet, Wallet, getDefaultProvider } from 'ethers';
import { u32, Struct, Option, u16, u256, Bytes as TsBytes, Enum } from 'scale-ts';
import { u8aToHex, hexToU8a } from '@polkadot/util';
import { HDNodeWallet, Wallet } from 'ethers';
import {
observeBalanceIncrease,
getContractAddress,
observeCcmReceived,
amountToFineAmount,
defaultAssetAmounts,
chainFromAsset,
getEvmEndpoint,
assetDecimals,
stateChainAssetFromAsset,
chainGasAsset,
shortChainFromAsset,
createEvmWalletAndFund,
} from './utils';
import { getBalance } from './get_balance';
import { CcmDepositMetadata } from '../shared/new_swap';
import { send } from './send';
import { CcmDepositMetadata, DcaParams, FillOrKillParamsX128 } from '../shared/new_swap';
import { SwapContext, SwapStatus } from './swap_context';

const erc20Assets: Asset[] = ['Flip', 'Usdc', 'Usdt', 'ArbUsdc'];

export const vaultSwapCfParametersCodec = Struct({
ccmAdditionalData: Option(TsBytes()),
vaultSwapParameters: Option(
Struct({
refundParams: Option(
Struct({
retryDurationBlocks: u32,
refundAddress: Enum({
Eth: TsBytes(20),
Dot: TsBytes(32),
Btc: TsBytes(),
Arb: TsBytes(20),
Sol: TsBytes(32),
}),
minPriceX128: u256,
}),
),
dcaParams: Option(Struct({ numberOfChunks: u32, chunkIntervalBlocks: u32 })),
boostFee: Option(u16),
}),
),
});

export function encodeCfParameters(
sourceAsset: Asset,
ccmAdditionalData?: string | undefined,
boostFeeBps?: number,
fillOrKillParams?: FillOrKillParamsX128,
dcaParams?: DcaParams,
): string | undefined {
return ccmAdditionalData || fillOrKillParams || dcaParams || boostFeeBps
? u8aToHex(
vaultSwapCfParametersCodec.enc({
ccmAdditionalData: ccmAdditionalData ? hexToU8a(ccmAdditionalData) : undefined,
vaultSwapParameters:
fillOrKillParams || dcaParams || boostFeeBps
? {
refundParams: fillOrKillParams && {
retryDurationBlocks: fillOrKillParams.retryDurationBlocks,
refundAddress: {
tag: shortChainFromAsset(sourceAsset),
value: hexToU8a(fillOrKillParams.refundAddress),
},
minPriceX128: BigInt(fillOrKillParams.minPriceX128),
},
dcaParams,
boostFee: boostFeeBps,
}
: undefined,
}),
)
: undefined;
}

export async function executeContractSwap(
srcAsset: Asset,
sourceAsset: Asset,
destAsset: Asset,
destAddress: string,
wallet: HDNodeWallet,
messageMetadata?: CcmDepositMetadata,
amount?: string,
boostFeeBps?: number,
fillOrKillParams?: FillOrKillParamsX128,
dcaParams?: DcaParams,
): ReturnType<typeof executeSwap> {
const srcChain = chainFromAsset(srcAsset);
const srcChain = chainFromAsset(sourceAsset);
const destChain = chainFromAsset(destAsset);
const amountToSwap = amount ?? defaultAssetAmounts(sourceAsset);

const networkOptions = {
signer: wallet,
network: 'localnet',
vaultContractAddress: getContractAddress(srcChain, 'VAULT'),
srcTokenContractAddress: getContractAddress(srcChain, srcAsset),
srcTokenContractAddress: getContractAddress(srcChain, sourceAsset),
} as const;
const txOptions = {
// This is run with fresh addresses to prevent nonce issues. Will be 1 for ERC20s.
gasLimit: srcChain === Chains.Arbitrum ? 10000000n : 200000n,
} as const;

const cfParameters = encodeCfParameters(
sourceAsset,
messageMetadata?.cfParameters,
boostFeeBps,
fillOrKillParams,
dcaParams,
);

const receipt = await executeSwap(
{
destChain,
destAsset: stateChainAssetFromAsset(destAsset),
// It is important that this is large enough to result in
// an amount larger than existential (e.g. on Polkadot):
amount: amountToFineAmount(defaultAssetAmounts(srcAsset), assetDecimals(srcAsset)),
amount: amountToFineAmount(amountToSwap, assetDecimals(sourceAsset)),
destAddress,
srcAsset: stateChainAssetFromAsset(srcAsset),
srcAsset: stateChainAssetFromAsset(sourceAsset),
srcChain,
// TODO: This will need some refactoring either putting the cfParameters outside the
// ccmParams and the user should encode it as done here or, probably better, we just
// add the SwapParameters support (Fok/Dca/Boost) as separate parameters, rename
// cfParameters to ccmAdditionalData and do the encoding within the SDK.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll move the encodeCfParameters into the SDK and then pass the DCA/Boost/Fok parameters separately here, as for deposit channels, so we abstract that layer of complexity from the end user.

ccmParams: messageMetadata && {
gasBudget: messageMetadata.gasBudget.toString(),
message: messageMetadata.message,
cfParameters: messageMetadata.cfParameters,
cfParameters,
},
} as ExecuteSwapParams,
networkOptions,
Expand All @@ -86,33 +157,29 @@ export async function performSwapViaContract(
messageMetadata?: CcmDepositMetadata,
swapContext?: SwapContext,
log = true,
amount?: string,
boostFeeBps?: number,
fillOrKillParams?: FillOrKillParamsX128,
dcaParams?: DcaParams,
): Promise<ContractSwapParams> {
const tag = swapTag ?? '';

const srcChain = chainFromAsset(sourceAsset);
const amountToSwap = amount ?? defaultAssetAmounts(sourceAsset);

// Generate a new wallet for each contract swap to prevent nonce issues when running in parallel
// with other swaps via deposit channels.
const mnemonic = Wallet.createRandom().mnemonic?.phrase ?? '';
if (mnemonic === '') {
throw new Error('Failed to create random mnemonic');
}
const wallet = Wallet.fromPhrase(mnemonic).connect(getDefaultProvider(getEvmEndpoint(srcChain)));
const wallet = await createEvmWalletAndFund(sourceAsset);

try {
// Fund new key with native asset and asset to swap.
await send(chainGasAsset(srcChain) as InternalAsset, wallet.address);
await send(sourceAsset, wallet.address);

if (erc20Assets.includes(sourceAsset)) {
// Doing effectively infinite approvals to make sure it doesn't fail.
// eslint-disable-next-line @typescript-eslint/no-use-before-define
await approveTokenVault(
sourceAsset,
(
BigInt(amountToFineAmount(defaultAssetAmounts(sourceAsset), assetDecimals(sourceAsset))) *
100n
).toString(),
(BigInt(amountToFineAmount(amountToSwap, assetDecimals(sourceAsset))) * 100n).toString(),
wallet,
);
}
Expand All @@ -135,6 +202,10 @@ export async function performSwapViaContract(
destAddress,
wallet,
messageMetadata,
amountToSwap,
boostFeeBps,
fillOrKillParams,
dcaParams,
);
swapContext?.updateStatus(swapTag, SwapStatus.ContractExecuted);

Expand Down Expand Up @@ -171,24 +242,24 @@ export async function performSwapViaContract(
throw new Error(`${tag} ${err}`);
}
}
export async function approveTokenVault(srcAsset: Asset, amount: string, wallet: HDNodeWallet) {
if (!erc20Assets.includes(srcAsset)) {
throw new Error(`Unsupported asset, not an ERC20: ${srcAsset}`);
export async function approveTokenVault(sourceAsset: Asset, amount: string, wallet: HDNodeWallet) {
if (!erc20Assets.includes(sourceAsset)) {
throw new Error(`Unsupported asset, not an ERC20: ${sourceAsset}`);
}

const chain = chainFromAsset(srcAsset as Asset);
const chain = chainFromAsset(sourceAsset as Asset);

await approveVault(
{
amount,
srcChain: chain as Chain,
srcAsset: stateChainAssetFromAsset(srcAsset) as SCAsset,
srcAsset: stateChainAssetFromAsset(sourceAsset) as SCAsset,
},
{
signer: wallet,
network: 'localnet',
vaultContractAddress: getContractAddress(chain, 'VAULT'),
srcTokenContractAddress: getContractAddress(chain, srcAsset),
srcTokenContractAddress: getContractAddress(chain, sourceAsset),
},
// This is run with fresh addresses to prevent nonce issues
{
Expand Down
4 changes: 1 addition & 3 deletions bouncer/shared/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ import { sendSolUsdc } from './send_solusdc';
const cfTesterAbi = await getCFTesterAbi();

export async function send(asset: Asset, address: string, amount?: string, log = true) {
// TODO: Remove this any when we have Sol assets in the Asset type.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
switch (asset as any) {
switch (asset) {
case 'Btc':
await sendBtc(address, amount ?? defaultAssetAmounts(asset));
break;
Expand Down
11 changes: 6 additions & 5 deletions bouncer/shared/swapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function newAbiEncodedMessage(types?: SolidityType[]): string {
return web3.eth.abi.encodeParameters(typesArray, variables);
}

function newSolanaCfParameters(maxAccounts: number) {
function newSolanaCcmAdditionalData(maxAccounts: number) {
function arrayToHexString(byteArray: Uint8Array): string {
return (
'0x' +
Expand Down Expand Up @@ -123,7 +123,7 @@ function newCcmArbitraryBytes(maxLength: number): string {
return randomAsHex(Math.floor(Math.random() * Math.max(0, maxLength - 10)) + 10);
}

function newCfParameters(destAsset: Asset, message?: string): string {
function newCcmAdditionalData(destAsset: Asset, message?: string): string {
const destChain = chainFromAsset(destAsset);
switch (destChain) {
case 'Ethereum':
Expand All @@ -139,7 +139,7 @@ function newCfParameters(destAsset: Asset, message?: string): string {

// The maximum number of extra accounts that can be passed is limited by the tx size
// and therefore also depends on the message length.
return newSolanaCfParameters(maxAccounts);
return newSolanaCcmAdditionalData(maxAccounts);
}
default:
throw new Error(`Unsupported chain: ${destChain}`);
Expand Down Expand Up @@ -167,7 +167,7 @@ export function newCcmMetadata(
cfParamsArray?: string,
): CcmDepositMetadata {
const message = ccmMessage ?? newCcmMessage(destAsset);
const cfParameters = cfParamsArray ?? newCfParameters(destAsset, message);
const ccmAdditionalData = cfParamsArray ?? newCcmAdditionalData(destAsset, message);
const gasDiv = gasBudgetFraction ?? 2;

const gasBudget = Math.floor(
Expand All @@ -178,7 +178,8 @@ export function newCcmMetadata(
return {
message,
gasBudget,
cfParameters,
// TODO: To rename to ccmAdditionalData
cfParameters: ccmAdditionalData,
};
}

Expand Down
24 changes: 21 additions & 3 deletions bouncer/shared/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { execSync } from 'child_process';
import * as crypto from 'crypto';
import { HDNodeWallet, Wallet, getDefaultProvider } from 'ethers';
import { setTimeout as sleep } from 'timers/promises';
import Client from 'bitcoin-core';
import { ApiPromise, Keyring } from '@polkadot/api';
Expand Down Expand Up @@ -30,6 +31,7 @@ import { SwapParams } from './perform_swap';
import { newSolAddress } from './new_sol_address';
import { getChainflipApi, observeBadEvent, observeEvent } from './utils/substrate';
import { execWithLog } from './utils/exec_with_log';
import { send } from './send';

const cfTesterAbi = await getCFTesterAbi();
const cfTesterIdl = await getCfTesterIdl();
Expand Down Expand Up @@ -472,17 +474,20 @@ function checkRequestTypeMatches(actual: object | string, expected: SwapRequestT
export async function observeSwapRequested(
sourceAsset: Asset,
destAsset: Asset,
channelId: number,
id: number | string,
swapRequestType: SwapRequestType,
) {
// need to await this to prevent the chainflip api from being disposed prematurely
return observeEvent('swapping:SwapRequested', {
test: (event) => {
const data = event.data;

if (typeof data.origin === 'object' && 'DepositChannel' in data.origin) {
if (typeof data.origin === 'object') {
const channelMatches =
Number(data.origin.DepositChannel.channelId.replaceAll(',', '')) === channelId;
(typeof id === 'number' &&
'DepositChannel' in data.origin &&
Number(data.origin.DepositChannel.channelId.replaceAll(',', '')) === id) ||
(typeof id === 'string' && 'Vault' in data.origin && data.origin.Vault.txHash === id);
const sourceAssetMatches = sourceAsset === (data.inputAsset as Asset);
const destAssetMatches = destAsset === (data.outputAsset as Asset);
const requestTypeMatches = checkRequestTypeMatches(data.requestType, swapRequestType);
Expand Down Expand Up @@ -1164,3 +1169,16 @@ export function getTimeStamp(): string {
const seconds = now.getSeconds().toString().padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
}

export async function createEvmWalletAndFund(asset: Asset): Promise<HDNodeWallet> {
const chain = chainFromAsset(asset);

const mnemonic = Wallet.createRandom().mnemonic?.phrase ?? '';
if (mnemonic === '') {
throw new Error('Failed to create random mnemonic');
}
const wallet = Wallet.fromPhrase(mnemonic).connect(getDefaultProvider(getEvmEndpoint(chain)));
await send(chainGasAsset(chain) as SDKAsset, wallet.address);
await send(asset, wallet.address);
return wallet;
}
Loading
Loading