Skip to content

Commit

Permalink
feat: Solana Vault swaps (base PR) (#5309)
Browse files Browse the repository at this point in the history
* chore: adding api call

* chore: add swap endpoint accounts to solEnvApi

* chore: add swap endpoint contract

* chore: add swap endpoint initialization

* chore: fix lint and initialization

* chore: update swap endpoint data account

* chore: remove initialization as its already done in image

* chore: cleanup

* chore: restore ci

* chore: remove unnecessary TODOs

* Added storage migration for Environment pallet.

* chore: address comment

* chore: bouncer solana program swaps (#5318)

* chore: first iteration adding anchor

* chore: small cleanup

* chore: syntax improvement

* chore: add comment

* chore: update compute units

* Moved Environment pallet's migration from Runtime into Pallet

* Fixed bouncer

* Corrected placeholder migration version for the broadcaster pallet

* chore: fix lint

* chore: run upgrade test

* chore: use Parity VersionedMigration

* Fixed try-runtime error

* chore: not run upgrade test

---------

Co-authored-by: Roy Yang <[email protected]>
  • Loading branch information
albert-llimos and syan095 authored Nov 12, 2024
1 parent 973990b commit 2d727e6
Show file tree
Hide file tree
Showing 26 changed files with 3,876 additions and 37 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion bouncer/commands/setup_vaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ async function main(): Promise<void> {

// Confirmation
console.log('Waiting for new epoch...');
await observeEvent('validator:NewEpoch');
await observeEvent('validator:NewEpoch').event;

console.log('=== New Epoch ===');
console.log('=== Vault Setup completed ===');
Expand Down
3 changes: 3 additions & 0 deletions bouncer/shared/contract_interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ export const getSolanaVaultIdl = loadContractCached(
export const getCfTesterIdl = loadContractCached(
`../contract-interfaces/sol-program-idls/${CF_SOL_PROGRAM_IDL_TAG}/cf_tester.json`,
);
export const getSolanaSwapEndpointIdl = loadContractCached(
`../contract-interfaces/sol-program-idls/${CF_SOL_PROGRAM_IDL_TAG}/swap_endpoint.json`,
);
193 changes: 168 additions & 25 deletions bouncer/shared/evm_vault_swap.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import * as anchor from '@coral-xyz/anchor';
// import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet';

import {
InternalAsset as Asset,
executeSwap,
Expand All @@ -9,6 +12,8 @@ import {
} from '@chainflip/cli';
import { HDNodeWallet } from 'ethers';
import { randomBytes } from 'crypto';
import { PublicKey, sendAndConfirmTransaction, Keypair } from '@solana/web3.js';
import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } from '@solana/spl-token';
import {
observeBalanceIncrease,
getContractAddress,
Expand All @@ -20,11 +25,23 @@ import {
stateChainAssetFromAsset,
createEvmWalletAndFund,
newAddress,
evmChains,
getSolWhaleKeyPair,
getSolConnection,
} from './utils';
import { getBalance } from './get_balance';
import { CcmDepositMetadata, DcaParams, FillOrKillParamsX128 } from './new_swap';
import { SwapContext, SwapStatus } from './swap_context';

import VaultIdl from '../../contract-interfaces/sol-program-idls/v1.0.0/vault.json';
import SwapEndpointIdl from '../../contract-interfaces/sol-program-idls/v1.0.0/swap_endpoint.json';

import { SwapEndpoint } from '../../contract-interfaces/sol-program-idls/v1.0.0/types/swap_endpoint';
import { Vault } from '../../contract-interfaces/sol-program-idls/v1.0.0/types/vault';

// Workaround because of anchor issue
const { BN } = anchor;

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

export async function executeVaultSwap(
Expand Down Expand Up @@ -100,6 +117,120 @@ export async function executeVaultSwap(

return receipt;
}
// Temporary before the SDK implements this.
export async function executeSolContractSwap(
srcAsset: Asset,
destAsset: Asset,
destAddress: string,
messageMetadata?: CcmDepositMetadata,
) {
const destChain = chainFromAsset(destAsset);

// const solanaSwapEndpointId = new PublicKey(getContractAddress('Solana', 'SWAP_ENDPOINT'));
const solanaVaultDataAccount = new PublicKey(getContractAddress('Solana', 'DATA_ACCOUNT'));
const swapEndpointDataAccount = new PublicKey(
getContractAddress('Solana', 'SWAP_ENDPOINT_DATA_ACCOUNT'),
);
const whaleKeypair = getSolWhaleKeyPair();

// We should just be able to do this instead but it's not working...
// const wallet = new NodeWallet(whaleKeypair);
// const provider = new anchor.AnchorProvider(connection, wallet, {
// commitment: 'processed',
// });
// const cfSwapEndpointProgram = new anchor.Program<SwapEndpoint>(SwapEndpointIdl, provider);
// const vaultProgram = new anchor.Program<Vault>(VaultIdl, provider);

// The current workaround requires having the wallet in a id.json and then set the ANCHOR_WALLET env.
// TODO: Depending on how the SDK is implemented we can remove this.
process.env.ANCHOR_WALLET = 'shared/solana_keypair.json';

const connection = getSolConnection();
const cfSwapEndpointProgram = new anchor.Program<SwapEndpoint>(SwapEndpointIdl as SwapEndpoint);
const vaultProgram = new anchor.Program<Vault>(VaultIdl as Vault);

const newEventAccountKeypair = Keypair.generate();
const fetchedDataAccount = await vaultProgram.account.dataAccount.fetch(solanaVaultDataAccount);
const aggKey = fetchedDataAccount.aggKey;

const tx =
srcAsset === 'Sol'
? await cfSwapEndpointProgram.methods
.xSwapNative({
amount: new BN(
amountToFineAmount(defaultAssetAmounts(srcAsset), assetDecimals(srcAsset)),
),
dstChain: Number(destChain),
dstAddress: Buffer.from(destAddress),
dstToken: Number(stateChainAssetFromAsset(destAsset)),
ccmParameters: messageMetadata
? {
message: Buffer.from(messageMetadata.message.slice(2), 'hex'),
gasAmount: new BN(messageMetadata.gasBudget),
}
: null,
// TODO: Encode cfParameters from ccmAdditionalData and other vault swap parameters
cfParameters: Buffer.from(messageMetadata?.ccmAdditionalData?.slice(2) ?? '', 'hex'),
})
.accountsPartial({
dataAccount: solanaVaultDataAccount,
aggKey,
from: whaleKeypair.publicKey,
eventDataAccount: newEventAccountKeypair.publicKey,
swapEndpointDataAccount,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([whaleKeypair, newEventAccountKeypair])
.transaction()
: await cfSwapEndpointProgram.methods
.xSwapToken({
amount: new BN(
amountToFineAmount(defaultAssetAmounts(srcAsset), assetDecimals(srcAsset)),
),
dstChain: Number(destChain),
dstAddress: Buffer.from(destAddress),
dstToken: Number(stateChainAssetFromAsset(destAsset)),
ccmParameters: messageMetadata
? {
message: Buffer.from(messageMetadata.message.slice(2), 'hex'),
gasAmount: new BN(messageMetadata.gasBudget),
}
: null,
// TODO: Encode cfParameters from ccmAdditionalData and other vault swap parameters
cfParameters: Buffer.from(messageMetadata?.ccmAdditionalData?.slice(2) ?? '', 'hex'),
decimals: assetDecimals(srcAsset),
})
.accountsPartial({
dataAccount: solanaVaultDataAccount,
tokenVaultAssociatedTokenAccount: new PublicKey(
getContractAddress('Solana', 'TOKEN_VAULT_ATA'),
),
from: whaleKeypair.publicKey,
fromTokenAccount: getAssociatedTokenAddressSync(
new PublicKey(getContractAddress('Solana', 'SolUsdc')),
whaleKeypair.publicKey,
false,
),
eventDataAccount: newEventAccountKeypair.publicKey,
swapEndpointDataAccount,
tokenSupportedAccount: new PublicKey(
getContractAddress('Solana', 'SolUsdcTokenSupport'),
),
tokenProgram: TOKEN_PROGRAM_ID,
mint: new PublicKey(getContractAddress('Solana', 'SolUsdc')),
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([whaleKeypair, newEventAccountKeypair])
.transaction();
const txHash = await sendAndConfirmTransaction(connection, tx, [
whaleKeypair,
newEventAccountKeypair,
]);

console.log('tx', txHash);
return txHash;
}

export type VaultSwapParams = {
sourceAsset: Asset;
destAsset: Asset;
Expand All @@ -122,11 +253,21 @@ export async function performVaultSwap(
): Promise<VaultSwapParams> {
const tag = swapTag ?? '';
const amountToSwap = amount ?? defaultAssetAmounts(sourceAsset);
const srcChain = chainFromAsset(sourceAsset);

try {
// Generate a new wallet for each vault swap to prevent nonce issues when running in parallel
// with other swaps via deposit channels.
const wallet = await createEvmWalletAndFund(sourceAsset);
let wallet;
let txHash: string;
let sourceAddress: string;

if (evmChains.includes(srcChain)) {
// Generate a new wallet for each vault swap to prevent nonce issues when running in parallel
// with other swaps via deposit channels.
wallet = await createEvmWalletAndFund(sourceAsset);
sourceAddress = wallet!.address.toLowerCase();
} else {
sourceAddress = getSolWhaleKeyPair().publicKey.toBase58();
}

const oldBalance = await getBalance(destAsset, destAddress);
if (log) {
Expand All @@ -136,30 +277,32 @@ export async function performVaultSwap(
);
}

// To uniquely identify the VaultSwap, we need to use the TX hash. This is only known
// after sending the transaction, so we send it first and observe the events afterwards.
// There are still multiple blocks of safety margin inbetween before the event is emitted
const receipt = await executeVaultSwap(
sourceAsset,
destAsset,
destAddress,
messageMetadata,
amountToSwap,
boostFeeBps,
fillOrKillParams,
dcaParams,
wallet,
);
// TODO: Temporary before the SDK implements this.
if (evmChains.includes(srcChain)) {
// To uniquely identify the VaultSwap, we need to use the TX hash. This is only known
// after sending the transaction, so we send it first and observe the events afterwards.
// There are still multiple blocks of safety margin inbetween before the event is emitted
const receipt = await executeVaultSwap(
sourceAsset,
destAsset,
destAddress,
messageMetadata,
amountToSwap,
boostFeeBps,
fillOrKillParams,
dcaParams,
wallet,
);
txHash = receipt.hash;
sourceAddress = wallet!.address.toLowerCase();
} else {
txHash = await executeSolContractSwap(sourceAsset, destAsset, destAddress, messageMetadata);
sourceAddress = getSolWhaleKeyPair().publicKey.toBase58();
}
swapContext?.updateStatus(swapTag, SwapStatus.VaultContractExecuted);

const ccmEventEmitted = messageMetadata
? observeCcmReceived(
sourceAsset,
destAsset,
destAddress,
messageMetadata,
wallet.address.toLowerCase(),
)
? observeCcmReceived(sourceAsset, destAsset, destAddress, messageMetadata, sourceAddress)
: Promise.resolve();

const [newBalance] = await Promise.all([
Expand All @@ -174,7 +317,7 @@ export async function performVaultSwap(
sourceAsset,
destAsset,
destAddress,
txHash: receipt.hash,
txHash,
};
} catch (err) {
console.error('err:', err);
Expand Down
5 changes: 1 addition & 4 deletions bouncer/shared/initialize_new_chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,7 @@ export async function initializeSolanaPrograms(solClient: Connection, solKey: st

const solUsdcMintPubkey = new PublicKey(getContractAddress('Solana', 'SolUsdc'));

const [tokenSupportedAccount] = PublicKey.findProgramAddressSync(
[Buffer.from('supported_token'), solUsdcMintPubkey.toBuffer()],
solanaVaultProgramId,
);
const tokenSupportedAccount = new PublicKey(getContractAddress('Solana', 'SolUsdcTokenSupport'));

tx.add(
new TransactionInstruction({
Expand Down
6 changes: 6 additions & 0 deletions bouncer/shared/solana_keypair.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
6, 151, 150, 20, 145, 210, 176, 113, 98, 200, 192, 80, 73, 63, 133, 232, 208, 124, 81, 213, 117,
199, 196, 243, 219, 33, 79, 217, 157, 69, 205, 140, 247, 157, 94, 2, 111, 18, 237, 198, 68, 58,
83, 75, 44, 221, 80, 114, 35, 57, 137, 180, 21, 215, 89, 101, 115, 231, 67, 243, 229, 179, 134,
251
]
13 changes: 13 additions & 0 deletions bouncer/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const brokerMutex = new Mutex();
export const snowWhiteMutex = new Mutex();

export const ccmSupportedChains = ['Ethereum', 'Arbitrum', 'Solana'] as Chain[];
export const evmChains = ['Ethereum', 'Arbitrum'] as Chain[];

export type Asset = SDKAsset;
export type Chain = SDKChain;
Expand Down Expand Up @@ -114,14 +115,26 @@ export function getContractAddress(chain: Chain, contract: string): string {
return '8inHGLHXegST3EPLcpisQe9D1hDT9r7DJjS395L3yuYf';
case 'TOKEN_VAULT_PDA':
return '7B13iu7bUbBX88eVBqTZkQqrErnTMazPmGLdE5RqdyKZ';
case 'TOKEN_VAULT_ATA':
return '9CGLwcPknpYs3atgwtjMX7RhgvBgaqK8wwCvXnmjEoL9';
case 'DATA_ACCOUNT':
return 'BttvFNSRKrkHugwDP6SpnBejCKKskHowJif1HGgBtTfG';
case 'SolUsdc':
return process.env.SOL_USDC_ADDRESS ?? '24PNhTaNtomHhoy3fTRaMhAFCRj4uHqhZEEoWrKDbR5p';
case 'SolUsdcTokenSupport':
return PublicKey.findProgramAddressSync(
[
Buffer.from('supported_token'),
new PublicKey(getContractAddress('Solana', 'SolUsdc')).toBuffer(),
],
new PublicKey(getContractAddress('Solana', 'VAULT')),
)[0].toBase58();
case 'CFTESTER':
return '8pBPaVfTAcjLeNfC187Fkvi9b1XEFhRNJ95BQXXVksmH';
case 'SWAP_ENDPOINT':
return '35uYgHdfZQT4kHkaaXQ6ZdCkK5LFrsk43btTLbGCRCNT';
case 'SWAP_ENDPOINT_DATA_ACCOUNT':
return '2tmtGLQcBd11BMiE9B1tAkQXwmPNgR79Meki2Eme4Ec9';
default:
throw new Error(`Unsupported contract: ${contract}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ gh release download \
unzip -u ${ZIP_FILE} \
'vault.json' \
'cf_tester.json' \
'swap_endpoint.json' \
-d $TARGET_DIR

rm ${ZIP_FILE}
Loading

0 comments on commit 2d727e6

Please sign in to comment.