From e7936d1b8226845549d0a4d2b31fa63c2a1e8623 Mon Sep 17 00:00:00 2001 From: Kevin Peters Date: Tue, 15 Oct 2024 09:21:03 -0500 Subject: [PATCH 1/5] portico: fetch swap event In addition to checking if the transfer was completed, we now check if the swap actually succeeded or failed. When the swap fails, the Wormhole-wrapped / highway token is received instead. If we can't find the swap event, then we assume it succeeded. --- connect/src/routes/portico/automatic.ts | 20 ++-- connect/src/types.ts | 16 +++- connect/src/warnings.ts | 6 ++ .../src/protocols/portico/portico.ts | 11 +++ platforms/evm/protocols/portico/src/abis.ts | 1 + platforms/evm/protocols/portico/src/bridge.ts | 93 +++++++++++++++++++ 6 files changed, 139 insertions(+), 8 deletions(-) diff --git a/connect/src/routes/portico/automatic.ts b/connect/src/routes/portico/automatic.ts index b8bf35404..3c717a5f6 100644 --- a/connect/src/routes/portico/automatic.ts +++ b/connect/src/routes/portico/automatic.ts @@ -20,6 +20,7 @@ import type { SourceInitiatedTransferReceipt, TokenId, TransactionId, + TransferWarning, } from "./../../index.js"; import { PorticoBridge, @@ -328,20 +329,27 @@ export class AutomaticPorticoRoute if (isAttested(receipt)) { const toChain = this.wh.getChain(receipt.to); const toPorticoBridge = await toChain.getPorticoBridge(); - const isCompleted = await toPorticoBridge.isTransferCompleted( + + const { swapResult, receivedToken } = await toPorticoBridge.getTransferResult( receipt.attestation.attestation, ); - if (isCompleted) { + + if (swapResult === "success" || swapResult === "failed") { + const warnings = + swapResult === "failed" + ? [{ type: "SwapFailedWarning" } satisfies TransferWarning] + : undefined; + const transferResult = receivedToken ? { receivedToken, warnings } : undefined; + receipt = { ...receipt, state: TransferState.DestinationFinalized, + transferResult, } satisfies CompletedTransferReceipt>; - - yield receipt; } - } - // TODO: handle swap failed case (highway token received) + yield receipt; + } yield receipt; } diff --git a/connect/src/types.ts b/connect/src/types.ts index f2cf45266..b82d058d7 100644 --- a/connect/src/types.ts +++ b/connect/src/types.ts @@ -6,7 +6,7 @@ import type { TokenId, TransactionId, } from "@wormhole-foundation/sdk-definitions"; -import type { QuoteWarning } from "./warnings.js"; +import type { TransferWarning, QuoteWarning } from "./warnings.js"; // Transfer state machine states export enum TransferState { @@ -105,6 +105,7 @@ export interface CompletedTransferReceipt[]; attestation: AT; destinationTxs?: TransactionId[]; + transferResult?: TransferResult; } export interface FailedTransferReceipt @@ -202,7 +203,7 @@ export interface TransferQuote { token: TokenId; amount: bigint; }; - // If the transfer being quoted asked for native gas dropoff + // If the transfer being quoted asked for native gas drop-off // this will contain the amount of native gas that is to be minted // on the destination chain given the current swap rates destinationNativeGas?: bigint; @@ -212,3 +213,14 @@ export interface TransferQuote { // Estimated time to completion in milliseconds eta?: number; } + +// Information about the result of a transfer +export interface TransferResult { + // How much of what token was received + receivedToken: { + token: TokenId; + amount: bigint; + } + // Any warnings that occurred (e.g. swap failed) + warnings?: TransferWarning[]; +} diff --git a/connect/src/warnings.ts b/connect/src/warnings.ts index c4c06efb5..33ec19bce 100644 --- a/connect/src/warnings.ts +++ b/connect/src/warnings.ts @@ -9,3 +9,9 @@ export type GovernorLimitWarning = { }; export type QuoteWarning = DestinationCapacityWarning | GovernorLimitWarning; + +export type SwapFailedWarning = { + type: "SwapFailedWarning"; +}; + +export type TransferWarning = SwapFailedWarning; diff --git a/core/definitions/src/protocols/portico/portico.ts b/core/definitions/src/protocols/portico/portico.ts index 7da0bdb94..75da8e392 100644 --- a/core/definitions/src/protocols/portico/portico.ts +++ b/core/definitions/src/protocols/portico/portico.ts @@ -34,6 +34,14 @@ export namespace PorticoBridge { relayerFee: bigint; }; + export type TransferResult = { + swapResult: "success" | "failed" | "pending"; + receivedToken?: { + token: TokenId; + amount: bigint; + }; + }; + const _transferPayloads = ["Transfer"] as const; const _payloads = [..._transferPayloads] as const; @@ -113,4 +121,7 @@ export interface PorticoBridge; } diff --git a/platforms/evm/protocols/portico/src/abis.ts b/platforms/evm/protocols/portico/src/abis.ts index 649eb019e..3ed1f1a30 100644 --- a/platforms/evm/protocols/portico/src/abis.ts +++ b/platforms/evm/protocols/portico/src/abis.ts @@ -3,6 +3,7 @@ import { ethers } from 'ethers'; export const porticoAbi = new ethers.Interface([ 'function start((bytes32,address,address,address,address,address,uint256,uint256,uint256,uint256)) returns (address,uint16,uint64)', 'function receiveMessageAndSwap(bytes)', + 'event PorticoSwapFinish(bool swapCompleted, uint256 finaluserAmount, uint256 relayerFeeAmount, tuple(bytes32 flags, address finalTokenAddress, address recipientAddress, uint256 canonAssetAmount, uint256 minAmountFinish, uint256 relayerFee) data)', ]); export const porticoSwapFinishedEvent = diff --git a/platforms/evm/protocols/portico/src/bridge.ts b/platforms/evm/protocols/portico/src/bridge.ts index 227f30a2e..ee22b0aed 100644 --- a/platforms/evm/protocols/portico/src/bridge.ts +++ b/platforms/evm/protocols/portico/src/bridge.ts @@ -10,7 +10,9 @@ import type { } from '@wormhole-foundation/sdk-connect'; import { PorticoBridge, + TokenTransfer, Wormhole, + amount, canonicalAddress, isEqualCaseInsensitive, nativeChainIds, @@ -349,4 +351,95 @@ export class EvmPorticoBridge< } return portico.uniswapQuoterV2; } + + async getTransferResult( + vaa: PorticoBridge.VAA, + ): Promise { + // First check if the transfer is completed + const isCompleted = await this.isTransferCompleted(vaa); + if (!isCompleted) return { swapResult: 'pending' }; + + const finalToken = Wormhole.tokenId( + this.chain, + vaa.payload.payload.flagSet.flags.shouldUnwrapNative + ? 'native' + : vaa.payload.payload.finalTokenAddress.toNative(this.chain).toString(), + ); + + const decimals = await EvmPlatform.getDecimals( + this.chain, + this.provider, + finalToken.address, + ); + + // This is a simplification since there is no swap on Ethereum + // since the highway token originates there + if (this.chain === 'Ethereum') { + const scaledAmount = amount.scale( + amount.fromBaseUnits( + vaa.payload.token.amount, + Math.min(decimals, TokenTransfer.MAX_DECIMALS), + ), + decimals, + ); + return { + swapResult: 'success', + receivedToken: { + token: finalToken, + amount: amount.units(scaledAmount), + }, + }; + } + + // Check if the swap succeeded or failed + const tokenBridge = this.tokenBridge.tokenBridge; + const filter = tokenBridge.filters.TransferRedeemed( + vaa.emitterChain, + vaa.emitterAddress.toString(), + vaa.sequence, + ); + + // NOTE: If we can't find the event, we assume the swap succeeded + // and minAmountFinish amount is received + const defaultResult: PorticoBridge.TransferResult = { + swapResult: 'success', + receivedToken: { + token: finalToken, + amount: vaa.payload.payload.minAmountFinish, + }, + }; + + const logs = await tokenBridge.queryFilter(filter); + if (logs.length === 0) return defaultResult; + + const txhash = logs[0]!.transactionHash; + const receipt = await this.provider.getTransactionReceipt(txhash); + if (!receipt) return defaultResult; + + const [event] = receipt.logs + .map((log) => porticoAbi.parseLog(log)) + .filter((log) => log); + if (!event) return defaultResult; + + const swapCompleted = event.args.swapCompleted; + const finaluserAmount = event.args.finaluserAmount; + + // If the swap failed, the highway / Wormhole-wrapped token is received instead + const token = swapCompleted + ? finalToken + : Wormhole.tokenId( + this.chain, + ( + await this.tokenBridge.getWrappedAsset(vaa.payload.token) + ).toString(), + ); + + return { + swapResult: swapCompleted ? 'success' : 'failed', + receivedToken: { + token, + amount: finaluserAmount, + }, + }; + } } From d5b8648332e3b4e9446f3a724cbbedaf80f3a444 Mon Sep 17 00:00:00 2001 From: Kevin Peters Date: Wed, 23 Oct 2024 15:29:37 -0500 Subject: [PATCH 2/5] move yield statement --- connect/src/routes/portico/automatic.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/connect/src/routes/portico/automatic.ts b/connect/src/routes/portico/automatic.ts index 3c717a5f6..426ff843e 100644 --- a/connect/src/routes/portico/automatic.ts +++ b/connect/src/routes/portico/automatic.ts @@ -346,9 +346,9 @@ export class AutomaticPorticoRoute state: TransferState.DestinationFinalized, transferResult, } satisfies CompletedTransferReceipt>; - } - yield receipt; + yield receipt; + } } yield receipt; From 4c6862279603bf467c5bb7f8ee0f5b0f9df17e6d Mon Sep 17 00:00:00 2001 From: Kevin Peters Date: Thu, 24 Oct 2024 11:31:03 -0500 Subject: [PATCH 3/5] limit log search range --- platforms/evm/protocols/portico/src/bridge.ts | 61 ++++++++----------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/platforms/evm/protocols/portico/src/bridge.ts b/platforms/evm/protocols/portico/src/bridge.ts index ee22b0aed..2530dbf28 100644 --- a/platforms/evm/protocols/portico/src/bridge.ts +++ b/platforms/evm/protocols/portico/src/bridge.ts @@ -10,9 +10,7 @@ import type { } from '@wormhole-foundation/sdk-connect'; import { PorticoBridge, - TokenTransfer, Wormhole, - amount, canonicalAddress, isEqualCaseInsensitive, nativeChainIds, @@ -39,6 +37,7 @@ import { EvmWormholeCore } from '@wormhole-foundation/sdk-evm-core'; import { EvmTokenBridge } from '@wormhole-foundation/sdk-evm-tokenbridge'; import '@wormhole-foundation/sdk-evm-tokenbridge'; +import { finality } from '@wormhole-foundation/sdk-connect'; export class EvmPorticoBridge< N extends Network, @@ -366,51 +365,42 @@ export class EvmPorticoBridge< : vaa.payload.payload.finalTokenAddress.toNative(this.chain).toString(), ); - const decimals = await EvmPlatform.getDecimals( - this.chain, - this.provider, - finalToken.address, - ); + const finalAmount = (() => { + const amountLessFee = + vaa.payload.payload.minAmountFinish - vaa.payload.payload.relayerFee; + return amountLessFee < 0n ? 0n : amountLessFee; + })(); + + const defaultResult: PorticoBridge.TransferResult = { + swapResult: 'success', + receivedToken: { + token: finalToken, + amount: finalAmount, + }, + }; // This is a simplification since there is no swap on Ethereum // since the highway token originates there - if (this.chain === 'Ethereum') { - const scaledAmount = amount.scale( - amount.fromBaseUnits( - vaa.payload.token.amount, - Math.min(decimals, TokenTransfer.MAX_DECIMALS), - ), - decimals, - ); - return { - swapResult: 'success', - receivedToken: { - token: finalToken, - amount: amount.units(scaledAmount), - }, - }; - } + if (this.chain === 'Ethereum') return defaultResult; // Check if the swap succeeded or failed + // NOTE: If we can't find the event, assume the swap succeeded const tokenBridge = this.tokenBridge.tokenBridge; const filter = tokenBridge.filters.TransferRedeemed( - vaa.emitterChain, + toChainId(vaa.emitterChain), vaa.emitterAddress.toString(), vaa.sequence, ); - // NOTE: If we can't find the event, we assume the swap succeeded - // and minAmountFinish amount is received - const defaultResult: PorticoBridge.TransferResult = { - swapResult: 'success', - receivedToken: { - token: finalToken, - amount: vaa.payload.payload.minAmountFinish, - }, - }; + // Search for the event in the last 15 minutes + const latestBlock = await EvmPlatform.getLatestBlock(this.provider); + const blockTime = finality.blockTime.get(this.chain)!; + const fromBlock = latestBlock - Math.floor((15 * 60 * 1000) / blockTime); - const logs = await tokenBridge.queryFilter(filter); - if (logs.length === 0) return defaultResult; + const logs = await tokenBridge + .queryFilter(filter, fromBlock, latestBlock) + .catch(() => null); + if (!logs || logs.length === 0) return defaultResult; const txhash = logs[0]!.transactionHash; const receipt = await this.provider.getTransactionReceipt(txhash); @@ -425,6 +415,7 @@ export class EvmPorticoBridge< const finaluserAmount = event.args.finaluserAmount; // If the swap failed, the highway / Wormhole-wrapped token is received instead + // of the finalToken const token = swapCompleted ? finalToken : Wormhole.tokenId( From 330e51dab77f2610b4a580990fa7c278819af434 Mon Sep 17 00:00:00 2001 From: Kevin Peters Date: Fri, 25 Oct 2024 16:14:32 -0500 Subject: [PATCH 4/5] export consts --- platforms/evm/protocols/portico/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/platforms/evm/protocols/portico/src/index.ts b/platforms/evm/protocols/portico/src/index.ts index 02ad5d833..759707d3d 100644 --- a/platforms/evm/protocols/portico/src/index.ts +++ b/platforms/evm/protocols/portico/src/index.ts @@ -5,3 +5,4 @@ import { EvmPorticoBridge } from './bridge.js'; registerProtocol(_platform, 'PorticoBridge', EvmPorticoBridge); export * from './bridge.js'; +export * from './consts.js'; From 2ecfd5d372c65a77ced02a3f3f59ed0ace128dbc Mon Sep 17 00:00:00 2001 From: Kevin Peters Date: Tue, 29 Oct 2024 09:16:09 -0500 Subject: [PATCH 5/5] cap search to 5k blocks --- platforms/evm/protocols/portico/src/bridge.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/platforms/evm/protocols/portico/src/bridge.ts b/platforms/evm/protocols/portico/src/bridge.ts index 2530dbf28..9a7235201 100644 --- a/platforms/evm/protocols/portico/src/bridge.ts +++ b/platforms/evm/protocols/portico/src/bridge.ts @@ -385,17 +385,18 @@ export class EvmPorticoBridge< // Check if the swap succeeded or failed // NOTE: If we can't find the event, assume the swap succeeded - const tokenBridge = this.tokenBridge.tokenBridge; + const { tokenBridge } = this.tokenBridge; const filter = tokenBridge.filters.TransferRedeemed( toChainId(vaa.emitterChain), vaa.emitterAddress.toString(), vaa.sequence, ); - // Search for the event in the last 15 minutes + // Search for the event in the last 15 minutes or 5000 blocks (whichever is smaller) const latestBlock = await EvmPlatform.getLatestBlock(this.provider); const blockTime = finality.blockTime.get(this.chain)!; - const fromBlock = latestBlock - Math.floor((15 * 60 * 1000) / blockTime); + const fromBlock = + latestBlock - Math.min(5000, Math.floor((15 * 60 * 1000) / blockTime)); const logs = await tokenBridge .queryFilter(filter, fromBlock, latestBlock)