Skip to content

Commit

Permalink
Feat: Extend Evm Vault swaps functionality (#5344)
Browse files Browse the repository at this point in the history
* feat: witnessing btc smart contract swaps

* chore: address minor review comments

* chore: start implementation

* test: fix deposit witnessing tests

* chore: add intial scale encoding for cfParameters

* chore: improve logic

* fix: address RuntimeCall size limit

* chore: address clippy

* chore: more cleanup and refactoring

* chore: add contract swaps to dca test

* chore: engine refactor

* chore: refactor createEvmWallet

* chore: cleanup

* chore: add MAX_VAULT_SWAP_ATTRIBUTES_LENGTH

* chore: refactor bouncer

* chore: refactor into common for reusal

* chore: rename attributes to parameters

* chore: pass extra parameters to sdk

* chore: fix issues

* chore: engine renaming

* chore: update SDK with new encoding logic

* chore: remove unnecessary bouncer ts-scale

* chore: lint

* chore: update to right name

* chore: fix missing rename

* chore: add beneficiares and make FoK mandatory

* chore: lint

* chore: update broker_fees and cli

* chore: update with hardcoded cfParameters

* chore: bump sdk

* fix: simplify cf params decoding

* chore: downgrade error -> warning

* chore: bump sdk with new broker_fees type

* chore: fix failing test

* chore: bump sdk

* chore: lint

---------

Co-authored-by: Maxim Shishmarev <maxim@chainflip.io>
Co-authored-by: Daniel <daniel@chainflip.io>
  • Loading branch information
3 people authored Oct 29, 2024
1 parent 5baa3dd commit 267b2a3
Show file tree
Hide file tree
Showing 36 changed files with 487 additions and 277 deletions.
2 changes: 1 addition & 1 deletion bouncer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"prettier:write": "prettier --write ."
},
"dependencies": {
"@chainflip/cli": "1.6.3",
"@chainflip/cli": "1.8.0-cf-parameters-rename.5",
"@chainflip/utils": "^0.4.0",
"@coral-xyz/anchor": "^0.30.1",
"@iarna/toml": "^2.2.5",
Expand Down
18 changes: 9 additions & 9 deletions bouncer/pnpm-lock.yaml

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

110 changes: 62 additions & 48 deletions bouncer/shared/contract_swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,67 @@ import {
approveVault,
Asset as SCAsset,
Chains,
InternalAsset,
Chain,
} from '@chainflip/cli';
import { HDNodeWallet, Wallet, getDefaultProvider } from 'ethers';
import { HDNodeWallet } from 'ethers';
import { randomBytes } from 'crypto';
import {
observeBalanceIncrease,
getContractAddress,
observeCcmReceived,
amountToFineAmount,
defaultAssetAmounts,
chainFromAsset,
getEvmEndpoint,
assetDecimals,
stateChainAssetFromAsset,
chainGasAsset,
createEvmWalletAndFund,
newAddress,
} 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 async function executeContractSwap(
srcAsset: Asset,
sourceAsset: Asset,
destAsset: Asset,
destAddress: string,
wallet: HDNodeWallet,
messageMetadata?: CcmDepositMetadata,
amount?: string,
boostFeeBps?: number,
fillOrKillParams?: FillOrKillParamsX128,
dcaParams?: DcaParams,
wallet?: HDNodeWallet,
): ReturnType<typeof executeSwap> {
const srcChain = chainFromAsset(srcAsset);
const srcChain = chainFromAsset(sourceAsset);
const destChain = chainFromAsset(destAsset);
const amountToSwap = amount ?? defaultAssetAmounts(sourceAsset);

const refundAddress = await newAddress(sourceAsset, randomBytes(32).toString('hex'));
const fokParams = fillOrKillParams ?? {
retryDurationBlocks: 0,
refundAddress,
minPriceX128: '0',
};

const evmWallet = wallet ?? (await createEvmWalletAndFund(sourceAsset));

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(amountToSwap, assetDecimals(sourceAsset))) * 100n).toString(),
evmWallet,
);
}

const networkOptions = {
signer: wallet,
signer: evmWallet,
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.
Expand All @@ -55,15 +78,21 @@ export async function executeContractSwap(
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,
ccmParams: messageMetadata && {
gasBudget: messageMetadata.gasBudget.toString(),
message: messageMetadata.message,
cfParameters: messageMetadata.cfParameters,
ccmAdditionalData: messageMetadata.ccmAdditionalData,
},
// The SDK will encode these parameters and the ccmAdditionalData
// into the `cfParameters` field for the vault swap.
boostFeeBps,
fillOrKillParams: fokParams,
dcaParams,
beneficiaries: undefined,
} as ExecuteSwapParams,
networkOptions,
txOptions,
Expand All @@ -86,37 +115,18 @@ 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);

// 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 amountToSwap = amount ?? defaultAssetAmounts(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(),
wallet,
);
}
swapContext?.updateStatus(swapTag, SwapStatus.ContractApproved);
// Generate a new wallet for each contract swap to prevent nonce issues when running in parallel
// with other swaps via deposit channels.
const wallet = await createEvmWalletAndFund(sourceAsset);

const oldBalance = await getBalance(destAsset, destAddress);
if (log) {
Expand All @@ -133,8 +143,12 @@ export async function performSwapViaContract(
sourceAsset,
destAsset,
destAddress,
wallet,
messageMetadata,
amountToSwap,
boostFeeBps,
fillOrKillParams,
dcaParams,
wallet,
);
swapContext?.updateStatus(swapTag, SwapStatus.ContractExecuted);

Expand Down Expand Up @@ -171,24 +185,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
2 changes: 1 addition & 1 deletion bouncer/shared/new_swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export async function newSwap(
ccmParams: messageMetadata && {
message: messageMetadata.message as `0x${string}`,
gasBudget: messageMetadata.gasBudget.toString(),
cfParameters: messageMetadata.cfParameters as `0x${string}`,
ccmAdditionalData: messageMetadata.ccmAdditionalData as `0x${string}`,
},
commissionBps: brokerCommissionBps,
maxBoostFeeBps: boostFeeBps,
Expand Down
2 changes: 1 addition & 1 deletion bouncer/shared/perform_swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export async function requestNewSwap(
? event.data.channelMetadata !== null &&
event.data.channelMetadata.message === messageMetadata.message &&
event.data.channelMetadata.gasBudget.replace(/,/g, '') === messageMetadata.gasBudget &&
event.data.channelMetadata.cfParameters === messageMetadata.cfParameters
event.data.channelMetadata.ccmAdditionalData === messageMetadata.ccmAdditionalData
: event.data.channelMetadata === null;

return destAddressMatches && destAssetMatches && sourceAssetMatches && ccmMetadataMatches;
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: 1 addition & 10 deletions bouncer/shared/swap_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import assert from 'assert';
export enum SwapStatus {
Initiated,
Funded,
// Contract swap specific statuses
ContractApproved,
ContractExecuted,
SwapScheduled,
Success,
Expand Down Expand Up @@ -37,16 +35,9 @@ export class SwapContext {
);
break;
}
case SwapStatus.ContractApproved: {
assert(
currentStatus === SwapStatus.Initiated,
`Unexpected status transition for ${tag}. Transitioning from ${currentStatus} to ${status}`,
);
break;
}
case SwapStatus.ContractExecuted: {
assert(
currentStatus === SwapStatus.ContractApproved,
currentStatus === SwapStatus.Initiated,
`Unexpected status transition for ${tag}. Transitioning from ${currentStatus} to ${status}`,
);
break;
Expand Down
16 changes: 8 additions & 8 deletions bouncer/shared/swapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
defaultAssetAmounts,
ccmSupportedChains,
assetDecimals,
solCfParamsCodec,
solCcmAdditionalDataCodec,
} from '../shared/utils';
import { BtcAddressType } from '../shared/new_btc_address';
import { CcmDepositMetadata } from '../shared/new_swap';
Expand Down Expand Up @@ -69,7 +69,7 @@ function newAbiEncodedMessage(types?: SolidityType[]): string {
return web3.eth.abi.encodeParameters(typesArray, variables);
}

export function newSolanaCfParameters(maxAccounts: number) {
export function newSolanaCcmAdditionalData(maxAccounts: number) {
const cfReceiverAddress = getContractAddress('Solana', 'CFTESTER');

const fallbackAddress = Keypair.generate().publicKey.toBytes();
Expand All @@ -93,7 +93,7 @@ export function newSolanaCfParameters(maxAccounts: number) {
fallback_address: fallbackAddress,
};

return u8aToHex(solCfParamsCodec.enc(cfParameters));
return u8aToHex(solCcmAdditionalDataCodec.enc(cfParameters));
}

// Solana CCM-related parameters. These are values in the protocol.
Expand All @@ -107,7 +107,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 @@ -123,7 +123,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 @@ -151,7 +151,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 @@ -162,7 +162,7 @@ export function newCcmMetadata(
return {
message,
gasBudget,
cfParameters,
ccmAdditionalData,
};
}

Expand Down Expand Up @@ -250,7 +250,7 @@ export async function testSwapViaContract(
destAsset,
addressType,
messageMetadata,
(tagSuffix ?? '') + ' Contract',
(tagSuffix ?? '') + 'Contract',
log,
swapContext,
);
Expand Down
Loading

0 comments on commit 267b2a3

Please sign in to comment.