import "dotenv/config"; import { describe, expect, jest, test } from "@jest/globals"; import { approveEth, attestFromEth, CHAIN_ID_BSC, CHAIN_ID_ETH, ChainId, CONTRACTS, createWrappedOnEth, getEmitterAddressEth, getForeignAssetEth, getSignedVAAWithRetry, hexToUint8Array, parseSequenceFromLogEth, parseTokenTransferVaa, redeemOnEth, serialiseVAA, sign, TokenBridgeTransfer, transferFromEth, tryNativeToHexString, tryNativeToUint8Array, uint8ArrayToHex, VAA, } from "@certusone/wormhole-sdk"; import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport"; import { ethers } from "ethers"; import * as devnetConsts from "../devnet-consts.json"; import { parseUnits } from "ethers/lib/utils"; import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; jest.setTimeout(60000); if (process.env.INIT_SIGNERS_KEYS_CSV === "undefined") { let msg = `.env is missing. run "make contracts-tools-deps" to fetch.`; console.error(msg); throw msg; } /* * Goals: * 1. Ensure a token can be sent from its origin chain * 2. Ensure a token can be sent back from a foreign chain * 3. Ensure spoofed tokens for more than the outstanding amount rejects successfully * 4. Validate the guardian metrics for each of these cases * 5. Bonus: Validate the on chain contract state via queries */ const ci = !!process.env.CI; const GUARDIAN_HOST = ci ? "guardian" : "localhost"; const GUARDIAN_RPCS = [`http://${GUARDIAN_HOST}:7071`]; const GUARDIAN_METRICS = `http://${GUARDIAN_HOST}:6060/metrics`; const ETH_NODE_URL = ci ? "ws://eth-devnet:8545" : "ws://localhost:8545"; const BSC_NODE_URL = ci ? "ws://eth-devnet2:8545" : "ws://localhost:8546"; const ETH_PRIVATE_KEY9 = "0xb0057716d5917badaf911b193b12b910811c1497b5bada8d7711f758981c3773"; const ETH_GA_TEST_TOKEN = devnetConsts.chains[CHAIN_ID_ETH].addresses.testGA.address; const DECIMALS = devnetConsts.chains[CHAIN_ID_ETH].addresses.testGA.decimals; const VAA_SIGNERS = process.env.INIT_SIGNERS_KEYS_CSV.split(","); const GOVERNANCE_CHAIN = Number(devnetConsts.global.governanceChainId); const GOVERNANCE_EMITTER = devnetConsts.global.governanceEmitterAddress; const TENDERMINT_URL = ci ? "http://wormchain:26657" : "http://localhost:26659"; const GA_ADDRESS = "wormhole14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9srrg465"; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } let ethProvider: ethers.providers.WebSocketProvider; let ethSigner: ethers.Wallet; let bscProvider: ethers.providers.WebSocketProvider; let bscSigner: ethers.Wallet; let cosmWasmClient: CosmWasmClient; beforeAll(async () => { // create a signer for Eth ethProvider = new ethers.providers.WebSocketProvider(ETH_NODE_URL); ethSigner = new ethers.Wallet(ETH_PRIVATE_KEY9, ethProvider); // create a signer for BSC bscProvider = new ethers.providers.WebSocketProvider(BSC_NODE_URL); bscSigner = new ethers.Wallet(ETH_PRIVATE_KEY9, bscProvider); cosmWasmClient = await CosmWasmClient.connect(TENDERMINT_URL); }); afterAll(async () => { await ethProvider.destroy(); await bscProvider.destroy(); cosmWasmClient.disconnect(); }); // Guardian metrics are prometheus data const fetchGlobalAccountantMetrics = async (): Promise<{ global_accountant_connection_errors_total: number; global_accountant_error_events_received: number; global_accountant_events_received: number; global_accountant_submit_failures: number; global_accountant_total_balance_errors: number; global_accountant_total_digest_mismatches: number; global_accountant_transfer_vaas_outstanding: number; global_accountant_transfer_vaas_submitted: number; global_accountant_transfer_vaas_submitted_and_approved: number; }> => (await (await fetch(GUARDIAN_METRICS)).text()) .split("\n") .filter((m) => m.startsWith("global_accountant")) .reduce((p, m) => { const [k, v] = m.split(" "); p[k] = Number(v); return p; }, {} as any); const fetchGlobalAccountantBalance = async ( tokenAddress: string, chainId: ChainId, tokenChain: ChainId ): Promise => { try { return BigInt( await cosmWasmClient.queryContractSmart(GA_ADDRESS, { balance: { token_address: tokenAddress, chain_id: chainId, token_chain: tokenChain, }, }) ); } catch (e) { if (e.message?.includes("accountant::state::account::Balance not found")) { // account not created yet return BigInt(0); } throw e; } }; const fetchGlobalAccountantTransferStatus = async ( emitterChain: ChainId, emitterAddress: string, sequence: string ): Promise => { return await cosmWasmClient.queryContractSmart(GA_ADDRESS, { transfer_status: { emitter_chain: emitterChain, emitter_address: emitterAddress, sequence: Number(sequence), }, }); }; describe("Global Accountant Tests", () => { test("Metrics and Contract Queries", async () => { let attestedAddress = ""; // // STEP 0 - attest the token // { attestedAddress = await getForeignAssetEth( CONTRACTS.DEVNET.bsc.token_bridge, bscProvider, CHAIN_ID_ETH, tryNativeToUint8Array(ETH_GA_TEST_TOKEN, CHAIN_ID_ETH) ); if (attestedAddress && attestedAddress !== ethers.constants.AddressZero) { console.log("already attested"); } else { console.log("attesting..."); // attest the test token const receipt = await attestFromEth( CONTRACTS.DEVNET.ethereum.token_bridge, ethSigner, ETH_GA_TEST_TOKEN ); // get the sequence from the logs (needed to fetch the vaa) const sequence = parseSequenceFromLogEth( receipt, CONTRACTS.DEVNET.ethereum.core ); const emitterAddress = getEmitterAddressEth( CONTRACTS.DEVNET.ethereum.token_bridge ); console.log(`fetching vaa ${sequence}...`); // poll until the guardian(s) witness and sign the vaa const { vaaBytes: signedVAA } = await getSignedVAAWithRetry( GUARDIAN_RPCS, CHAIN_ID_ETH, emitterAddress, sequence, { transport: NodeHttpTransport(), } ); console.log("creating..."); await createWrappedOnEth( CONTRACTS.DEVNET.bsc.token_bridge, bscSigner, signedVAA ); attestedAddress = await getForeignAssetEth( CONTRACTS.DEVNET.bsc.token_bridge, bscProvider, CHAIN_ID_ETH, tryNativeToUint8Array(ETH_GA_TEST_TOKEN, CHAIN_ID_ETH) ); } } // // STEP 1 - send the token out // { const beforeMetrics = await fetchGlobalAccountantMetrics(); const beforeEthBalance = await fetchGlobalAccountantBalance( tryNativeToHexString(ETH_GA_TEST_TOKEN, CHAIN_ID_ETH), CHAIN_ID_ETH, CHAIN_ID_ETH ); const beforeBscBalance = await fetchGlobalAccountantBalance( tryNativeToHexString(ETH_GA_TEST_TOKEN, CHAIN_ID_ETH), CHAIN_ID_BSC, CHAIN_ID_ETH ); const amount = parseUnits("1", DECIMALS); // approve the bridge to spend tokens console.log("approving..."); await approveEth( CONTRACTS.DEVNET.ethereum.token_bridge, ETH_GA_TEST_TOKEN, ethSigner, amount ); // transfer tokens out console.log("transferring..."); const receipt = await transferFromEth( CONTRACTS.DEVNET.ethereum.token_bridge, ethSigner, ETH_GA_TEST_TOKEN, amount, CHAIN_ID_BSC, tryNativeToUint8Array(await bscSigner.getAddress(), CHAIN_ID_BSC) ); // get the sequence from the logs (needed to fetch the vaa) const sequence = parseSequenceFromLogEth( receipt, CONTRACTS.DEVNET.ethereum.core ); const emitterAddress = getEmitterAddressEth( CONTRACTS.DEVNET.ethereum.token_bridge ); console.log(`fetching vaa ${sequence}...`); // poll until the guardian(s) witness and sign the vaa const { vaaBytes: signedVAA } = await getSignedVAAWithRetry( GUARDIAN_RPCS, CHAIN_ID_ETH, emitterAddress, sequence, { transport: NodeHttpTransport(), } ); console.log("redeeming..."); await redeemOnEth( CONTRACTS.DEVNET.bsc.token_bridge, bscSigner, signedVAA ); const afterMetrics = await fetchGlobalAccountantMetrics(); console.log( "approved b/a:", beforeMetrics.global_accountant_transfer_vaas_submitted_and_approved, afterMetrics.global_accountant_transfer_vaas_submitted_and_approved ); if ( afterMetrics.global_accountant_events_received <= beforeMetrics.global_accountant_events_received || afterMetrics.global_accountant_transfer_vaas_submitted <= beforeMetrics.global_accountant_transfer_vaas_submitted || afterMetrics.global_accountant_transfer_vaas_submitted_and_approved <= beforeMetrics.global_accountant_transfer_vaas_submitted_and_approved ) { throw new Error("Expected metrics change did not occur"); } const parsedVAA = parseTokenTransferVaa(signedVAA); const transferStatus = await fetchGlobalAccountantTransferStatus( CHAIN_ID_ETH, emitterAddress, sequence ); expect(transferStatus).toMatchObject({ committed: { data: { amount: parsedVAA.amount.toString(), token_chain: CHAIN_ID_ETH, token_address: tryNativeToHexString( ETH_GA_TEST_TOKEN, CHAIN_ID_ETH ), recipient_chain: CHAIN_ID_BSC, }, }, }); const afterEthBalance = await fetchGlobalAccountantBalance( tryNativeToHexString(ETH_GA_TEST_TOKEN, CHAIN_ID_ETH), CHAIN_ID_ETH, CHAIN_ID_ETH ); expect(afterEthBalance).toBeGreaterThan(beforeEthBalance.valueOf()); const afterBscBalance = await fetchGlobalAccountantBalance( tryNativeToHexString(ETH_GA_TEST_TOKEN, CHAIN_ID_ETH), CHAIN_ID_BSC, CHAIN_ID_ETH ); expect(afterBscBalance).toBeGreaterThan(beforeBscBalance.valueOf()); } // // STEP 2 - send the token back // { const beforeMetrics = await fetchGlobalAccountantMetrics(); const beforeEthBalance = await fetchGlobalAccountantBalance( tryNativeToHexString(ETH_GA_TEST_TOKEN, CHAIN_ID_ETH), CHAIN_ID_ETH, CHAIN_ID_ETH ); const beforeBscBalance = await fetchGlobalAccountantBalance( tryNativeToHexString(ETH_GA_TEST_TOKEN, CHAIN_ID_ETH), CHAIN_ID_BSC, CHAIN_ID_ETH ); const amount = parseUnits("1", DECIMALS); // approve the bridge to spend tokens console.log("approving..."); await approveEth( CONTRACTS.DEVNET.bsc.token_bridge, attestedAddress, bscSigner, amount ); // transfer tokens out console.log("transferring..."); const receipt = await transferFromEth( CONTRACTS.DEVNET.bsc.token_bridge, bscSigner, attestedAddress, amount, CHAIN_ID_ETH, tryNativeToUint8Array(await ethSigner.getAddress(), CHAIN_ID_ETH) ); // get the sequence from the logs (needed to fetch the vaa) const sequence = parseSequenceFromLogEth( receipt, CONTRACTS.DEVNET.bsc.core ); const emitterAddress = getEmitterAddressEth( CONTRACTS.DEVNET.bsc.token_bridge ); console.log(`fetching vaa ${sequence}...`); // poll until the guardian(s) witness and sign the vaa const { vaaBytes: signedVAA } = await getSignedVAAWithRetry( GUARDIAN_RPCS, CHAIN_ID_BSC, emitterAddress, sequence, { transport: NodeHttpTransport(), } ); console.log("redeeming..."); await redeemOnEth( CONTRACTS.DEVNET.ethereum.token_bridge, ethSigner, signedVAA ); const afterMetrics = await fetchGlobalAccountantMetrics(); console.log( "approved b/a:", beforeMetrics.global_accountant_transfer_vaas_submitted_and_approved, afterMetrics.global_accountant_transfer_vaas_submitted_and_approved ); if ( afterMetrics.global_accountant_events_received <= beforeMetrics.global_accountant_events_received || afterMetrics.global_accountant_transfer_vaas_submitted <= beforeMetrics.global_accountant_transfer_vaas_submitted || afterMetrics.global_accountant_transfer_vaas_submitted_and_approved <= beforeMetrics.global_accountant_transfer_vaas_submitted_and_approved ) { throw new Error("Expected metrics change did not occur"); } const parsedVAA = parseTokenTransferVaa(signedVAA); const transferStatus = await fetchGlobalAccountantTransferStatus( CHAIN_ID_BSC, emitterAddress, sequence ); expect(transferStatus).toMatchObject({ committed: { data: { amount: parsedVAA.amount.toString(), token_chain: CHAIN_ID_ETH, token_address: tryNativeToHexString( ETH_GA_TEST_TOKEN, CHAIN_ID_ETH ), recipient_chain: CHAIN_ID_ETH, }, }, }); const afterEthBalance = await fetchGlobalAccountantBalance( tryNativeToHexString(ETH_GA_TEST_TOKEN, CHAIN_ID_ETH), CHAIN_ID_ETH, CHAIN_ID_ETH ); expect(afterEthBalance).toBeLessThan(beforeEthBalance.valueOf()); const afterBscBalance = await fetchGlobalAccountantBalance( tryNativeToHexString(ETH_GA_TEST_TOKEN, CHAIN_ID_ETH), CHAIN_ID_BSC, CHAIN_ID_ETH ); expect(afterBscBalance).toBeLessThan(beforeBscBalance.valueOf()); } // // STEP 3a - redeem spoofed tokens // { console.log("redeeming spoofed tokens"); let vaa: VAA = { version: 1, guardianSetIndex: 0, signatures: [], timestamp: 0, nonce: 0, emitterChain: CHAIN_ID_ETH, emitterAddress: getEmitterAddressEth( CONTRACTS.DEVNET.ethereum.token_bridge ), sequence: BigInt(979999116 + Math.floor(Math.random() * 100000000)), consistencyLevel: 0, payload: { module: "TokenBridge", type: "Transfer", tokenChain: CHAIN_ID_ETH, tokenAddress: uint8ArrayToHex( tryNativeToUint8Array(ETH_GA_TEST_TOKEN, CHAIN_ID_ETH) ), amount: parseUnits("9000", DECIMALS).toBigInt(), toAddress: uint8ArrayToHex( tryNativeToUint8Array(await bscSigner.getAddress(), CHAIN_ID_BSC) ), chain: CHAIN_ID_BSC, fee: BigInt(0), }, }; vaa.signatures = sign(VAA_SIGNERS, vaa); await redeemOnEth( CONTRACTS.DEVNET.bsc.token_bridge, bscSigner, hexToUint8Array(serialiseVAA(vaa)) ); } // // STEP 3b - send the spoofed tokens back // { const beforeMetrics = await fetchGlobalAccountantMetrics(); const amount = parseUnits("9000", DECIMALS); // approve the bridge to spend tokens console.log("approving..."); await approveEth( CONTRACTS.DEVNET.bsc.token_bridge, attestedAddress, bscSigner, amount ); // transfer tokens out console.log("transferring..."); const receipt = await transferFromEth( CONTRACTS.DEVNET.bsc.token_bridge, bscSigner, attestedAddress, amount, CHAIN_ID_ETH, tryNativeToUint8Array(await ethSigner.getAddress(), CHAIN_ID_ETH) ); const sequence = parseSequenceFromLogEth( receipt, CONTRACTS.DEVNET.bsc.core ); console.log("waiting 30s to fetch metrics..."); await sleep(30 * 1000); // give the guardian a few seconds to pick up the transfers and attempt to submit them const afterMetrics = await fetchGlobalAccountantMetrics(); console.log( "balance errors b/a:", beforeMetrics.global_accountant_total_balance_errors, afterMetrics.global_accountant_total_balance_errors ); if ( afterMetrics.global_accountant_error_events_received <= beforeMetrics.global_accountant_error_events_received || afterMetrics.global_accountant_transfer_vaas_submitted <= beforeMetrics.global_accountant_transfer_vaas_submitted || afterMetrics.global_accountant_total_balance_errors <= beforeMetrics.global_accountant_total_balance_errors ) { throw new Error("Expected metrics change did not occur"); } // the transfer should fail, because there's an insufficient source balance await expect( fetchGlobalAccountantTransferStatus( CHAIN_ID_BSC, getEmitterAddressEth(CONTRACTS.DEVNET.bsc.core), sequence ) ).rejects.toThrow(); } console.log("success!"); }); });