diff --git a/connect/src/routes/portico/README.md b/connect/src/routes/portico/README.md index 7c26fcb11..ffeaeedbb 100644 --- a/connect/src/routes/portico/README.md +++ b/connect/src/routes/portico/README.md @@ -41,7 +41,7 @@ The current table of input tokens, to bridging tokens, to final tokens is as follows ``` -| inputs | 'native' | ETH | wETH | wstETH -| bridging token | xETH | wstETH -| outputs | 'native' | ETH | wETH | wstETH +| inputs | 'native' | ETH | wETH | wstETH | USDT | +| bridging token | xETH | xwstETH | xUSDT | +| outputs | 'native' | ETH | wETH | wstETH | USDT | ``` diff --git a/connect/src/routes/portico/automatic.ts b/connect/src/routes/portico/automatic.ts index 1b4cfb7e2..c22082fde 100644 --- a/connect/src/routes/portico/automatic.ts +++ b/connect/src/routes/portico/automatic.ts @@ -29,7 +29,6 @@ import { chainToPlatform, contracts, isAttested, - isNative, isSourceInitiated, resolveWrappedToken, signSendWait, @@ -76,7 +75,8 @@ export class AutomaticPorticoRoute name: "AutomaticPortico", }; - private static _supportedTokens = ["WETH", "WSTETH"]; + // TODO: "ETH"? + private static _supportedTokens = ["WETH", "wstETH", "USDT"]; static supportedNetworks(): Network[] { return ["Mainnet"]; @@ -97,9 +97,8 @@ export class AutomaticPorticoRoute }) .flat() .filter((td) => { - const localOrEth = !td.original || td.original === "Ethereum"; - const isAvax = chain === "Avalanche" && isNative(td.address); - return localOrEth && !isAvax; + // Only tokens native to the chain are supported + return td.chain === chain; }); return supported.map((td) => Wormhole.tokenId(chain, td.address)); @@ -145,8 +144,8 @@ export class AutomaticPorticoRoute switch (td.symbol) { case "ETH": case "WETH": - return Wormhole.tokenId(toChain.chain, td.address); - case "WSTETH": + case "wstETH": + case "USDT": return Wormhole.tokenId(toChain.chain, td.address); default: throw new Error("Unknown symbol: " + redeemTokenDetails.symbol); @@ -314,10 +313,11 @@ export class AutomaticPorticoRoute private async quoteUniswap(params: VP) { const fromPorticoBridge = await this.request.fromChain.getPorticoBridge(); + const xferAmount = amount.units(params.normalizedParams.amount); const startQuote = await fromPorticoBridge.quoteSwap( params.normalizedParams.sourceToken.address, params.normalizedParams.canonicalSourceToken.address, - amount.units(params.normalizedParams.amount), + xferAmount, ); const startSlippage = (startQuote * SLIPPAGE_BPS) / BPS_PER_HUNDRED_PERCENT; @@ -349,6 +349,11 @@ export class AutomaticPorticoRoute const amountFinish = amountFinishQuote - amountFinishSlippage; if (amountFinish <= minAmountFinish) throw new Error("Amount finish too low"); + // if the slippage is more than 50bps, we should throw an error + // this likely means that the pools are unbalanced + if (minAmountFinish < xferAmount - (xferAmount * 50n) / BPS_PER_HUNDRED_PERCENT) + throw new Error("Slippage too high"); + return { minAmountStart: minAmountStart, minAmountFinish: minAmountFinish, diff --git a/connect/src/wormhole.ts b/connect/src/wormhole.ts index bee28e475..f9b88df3a 100644 --- a/connect/src/wormhole.ts +++ b/connect/src/wormhole.ts @@ -369,7 +369,7 @@ export class Wormhole { } /** - * Parse an address from its canonincal string format to a NativeAddress + * Parse an address from its canonical string format to a NativeAddress * * @param chain The chain the address is for * @param address The native address in canonical string format @@ -380,7 +380,7 @@ export class Wormhole { } /** - * Parse an address from its canonincal string format to a NativeAddress + * Parse an address from its canonical string format to a NativeAddress * * @param chain The chain the address is for * @param address The native address in canonical string format or the string "native" diff --git a/core/base/src/constants/contracts/portico.ts b/core/base/src/constants/contracts/portico.ts index e8fde82bd..f7b9eafd0 100644 --- a/core/base/src/constants/contracts/portico.ts +++ b/core/base/src/constants/contracts/portico.ts @@ -1,10 +1,12 @@ -import type { MapLevels } from './../../utils/index.js'; -import type { Chain } from '../chains.js'; -import type { Network } from '../networks.js'; +import type { MapLevels } from "./../../utils/index.js"; +import type { Chain } from "../chains.js"; +import type { Network } from "../networks.js"; export type PorticoContracts = { - portico: string; + porticoUniswap: string; uniswapQuoterV2: string; + porticoPancakeSwap?: string; + pancakeSwapQuoterV2?: string; }; // prettier-ignore @@ -13,32 +15,46 @@ export const porticoContracts = [ "Mainnet", [ ["Ethereum", { - portico: '0x48b6101128C0ed1E208b7C910e60542A2ee6f476', + porticoUniswap: '0x48b6101128C0ed1E208b7C910e60542A2ee6f476', uniswapQuoterV2: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e', + porticoPancakeSwap: '0x4db1683d60e0a933A9A477a19FA32F472bB9d06e', + pancakeSwapQuoterV2: '0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997', }], ["Polygon", { - portico: '0x227bABe533fa9a1085f5261210E0B7137E44437B', + porticoUniswap: '0x227bABe533fa9a1085f5261210E0B7137E44437B', uniswapQuoterV2: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e', + porticoPancakeSwap: undefined, + pancakeSwapQuoterV2: undefined, }], ["Bsc", { - portico: '0x05498574BD0Fa99eeCB01e1241661E7eE58F8a85', + porticoUniswap: '0x05498574BD0Fa99eeCB01e1241661E7eE58F8a85', uniswapQuoterV2: '0x78D78E420Da98ad378D7799bE8f4AF69033EB077', + porticoPancakeSwap: '0xF352DC165783538A26e38A536e76DceF227d90F2', + pancakeSwapQuoterV2: '0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997', }], ["Avalanche", { - portico: '0xE565E118e75304dD3cF83dff409c90034b7EA18a', + porticoUniswap: '0xE565E118e75304dD3cF83dff409c90034b7EA18a', uniswapQuoterV2: '0xbe0F5544EC67e9B3b2D979aaA43f18Fd87E6257F', + porticoPancakeSwap: undefined, + pancakeSwapQuoterV2: undefined, }], ["Arbitrum", { - portico: '0x48fa7528bFD6164DdF09dF0Ed22451cF59c84130', + porticoUniswap: '0x48fa7528bFD6164DdF09dF0Ed22451cF59c84130', uniswapQuoterV2: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e', + porticoPancakeSwap: '0xE70946692E2e56ae47BfAe2d93d31bd60952B090', + pancakeSwapQuoterV2: '0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997', }], ["Optimism", { - portico: '0x9ae506cDDd27DEe1275fd1fe6627E5dc65257061', + porticoUniswap: '0x9ae506cDDd27DEe1275fd1fe6627E5dc65257061', uniswapQuoterV2: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e', + porticoPancakeSwap: undefined, + pancakeSwapQuoterV2: undefined, }], ["Base", { - portico: '0x610d4DFAC3EC32e0be98D18DDb280DACD76A1889', + porticoUniswap: '0x610d4DFAC3EC32e0be98D18DDb280DACD76A1889', uniswapQuoterV2: '0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a', + porticoPancakeSwap: '0x4568aa1eA0ED54db666c58B4526B3FC9BD9be9bf', + pancakeSwapQuoterV2: '0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997', }], ] ]] as const satisfies MapLevels<[Network, Chain, PorticoContracts]> diff --git a/core/definitions/src/address.ts b/core/definitions/src/address.ts index b93401d88..c0126d282 100644 --- a/core/definitions/src/address.ts +++ b/core/definitions/src/address.ts @@ -43,7 +43,7 @@ type GetNativeAddress

= P extends MappedPlatforms export type NativeAddressCtr = new (ua: UniversalAddress | string | Uint8Array) => Address; -/** An address that has been parsed into its Nativfe Address type */ +/** An address that has been parsed into its Native Address type */ export type NativeAddress = GetNativeAddress>; /** A union type representing a parsed address */ diff --git a/core/definitions/src/protocols/portico/porticoLayout.ts b/core/definitions/src/protocols/portico/porticoLayout.ts index 760adcc4e..2286d6a26 100644 --- a/core/definitions/src/protocols/portico/porticoLayout.ts +++ b/core/definitions/src/protocols/portico/porticoLayout.ts @@ -38,7 +38,7 @@ export const porticoPayloadLayout = [ { name: "relayerFee", ...amountItem }, ] as const satisfies Layout; -export const namedPayloads = [["Transfer", porticoFlagSetLayout]] as const satisfies NamedPayloads; +export const namedPayloads = [["Transfer", porticoPayloadLayout]] as const satisfies NamedPayloads; // factory registration: import "../../registry.js"; diff --git a/platforms/evm/protocols/portico/src/api.ts b/platforms/evm/protocols/portico/src/api.ts index 847f79736..3b01be46b 100644 --- a/platforms/evm/protocols/portico/src/api.ts +++ b/platforms/evm/protocols/portico/src/api.ts @@ -96,12 +96,12 @@ export class PorticoApi { const sourcePorticoAddress = contracts.portico.get( network, chain, - )!.portico; + )!.porticoUniswap; const destinationPorticoAddress = contracts.portico.get( network, receiver.chain, - )!.portico; + )!.porticoUniswap; const startingChainId = nativeChainIds.networkChainToNativeChainId.get( network, diff --git a/platforms/evm/protocols/portico/src/bridge.ts b/platforms/evm/protocols/portico/src/bridge.ts index 530d905a7..7694cb933 100644 --- a/platforms/evm/protocols/portico/src/bridge.ts +++ b/platforms/evm/protocols/portico/src/bridge.ts @@ -17,6 +17,7 @@ import { nativeChainIds, resolveWrappedToken, serialize, + toChain, toChainId, } from '@wormhole-foundation/sdk-connect'; import type { EvmChains } from '@wormhole-foundation/sdk-evm'; @@ -37,6 +38,8 @@ import * as tokens from '@wormhole-foundation/sdk-connect/tokens'; import { EvmWormholeCore } from '@wormhole-foundation/sdk-evm-core'; import '@wormhole-foundation/sdk-evm-tokenbridge'; +import { getTokenByAddress } from '@wormhole-foundation/sdk-connect/tokens'; +import { isNative } from '@wormhole-foundation/sdk-connect'; export class EvmPorticoBridge< N extends Network, @@ -44,11 +47,7 @@ export class EvmPorticoBridge< > implements PorticoBridge { chainId: bigint; - porticoAddress: string; - uniswapAddress: string; - porticoContract: ethers.Contract; - uniswapContract: ethers.Contract; core: EvmWormholeCore; constructor( @@ -62,28 +61,10 @@ export class EvmPorticoBridge< this.core = new EvmWormholeCore(network, chain, provider, contracts); - const { portico: porticoAddress, uniswapQuoterV2: uniswapAddress } = - contracts.portico; - - this.porticoAddress = porticoAddress; - this.uniswapAddress = uniswapAddress; - this.chainId = nativeChainIds.networkChainToNativeChainId.get( network, chain, ) as bigint; - - this.porticoContract = new ethers.Contract( - this.porticoAddress, - porticoAbi.fragments, - this.provider, - ); - - this.uniswapContract = new ethers.Contract( - this.uniswapAddress, - uniswapQuoterV2Abi.fragments, - this.provider, - ); } static async fromRpc( @@ -135,10 +116,7 @@ export class EvmPorticoBridge< const finalTokenAddress = canonicalAddress(finalToken); - const destinationPorticoAddress = contracts.portico.get( - this.network, - receiver.chain, - )!.portico; + const destinationPorticoAddress = this.getPorticoAddress(destToken); const nonce = new Date().valueOf() % 2 ** 4; const flags = PorticoBridge.serializeFlagSet({ @@ -168,19 +146,23 @@ export class EvmPorticoBridge< ], ]); + const porticoAddress = this.getPorticoAddress( + Wormhole.tokenId(this.chain, startTokenAddress), + ); + // Approve the token if necessary if (!isStartTokenNative) yield* this.approve( startTokenAddress, senderAddress, amount, - this.porticoAddress, + porticoAddress, ); const messageFee = await this.core.getMessageFee(); const tx = { - to: this.porticoAddress, + to: porticoAddress, data: transactionData, value: messageFee + (isStartTokenNative ? amount : 0n), }; @@ -191,7 +173,12 @@ export class EvmPorticoBridge< } async *redeem(sender: AccountAddress, vaa: PorticoBridge.VAA) { - const txReq = await this.porticoContract + const recipientChain = toChain(vaa.payload.flagSet.recipientChain); + const tokenAddress = vaa.payload.finalTokenAddress + .toNative(recipientChain) + .toString(); + const tokenId = Wormhole.tokenId(recipientChain, tokenAddress); + const txReq = await this.getPorticoContract(tokenId) .getFunction('receiveMessageAndSwap') .populateTransaction(serialize(vaa)); @@ -225,7 +212,7 @@ export class EvmPorticoBridge< if (isEqualCaseInsensitive(inputAddress, outputAddress)) return amount; - const result = await this.uniswapContract + const result = await this.getQuoterContract(inputTokenId) .getFunction('quoteExactInputSingle') .staticCall([inputAddress, outputAddress, amount, FEE_TIER, 0]); @@ -293,4 +280,47 @@ export class EvmPorticoBridge< false, ); } + + // The address of the Portico contract depends on the token being transferred + // USDT uses PancakeSwap if available + private getPorticoAddress(tokenId: TokenId) { + const portico = contracts.portico.get(this.network, tokenId.chain); + if (!portico) throw new Error('Unsupported chain: ' + tokenId.chain); + if (this.isUSDT(tokenId)) { + return portico.porticoPancakeSwap || portico.porticoUniswap; + } + return portico.porticoUniswap; + } + + private getPorticoContract(tokenId: TokenId) { + const address = this.getPorticoAddress(tokenId); + return new ethers.Contract(address, porticoAbi.fragments, this.provider); + } + + // The address of the Quoter contract depends on the token being transferred + // USDT uses PancakeSwap if available + private getQuoterAddress(tokenId: TokenId) { + const portico = contracts.portico.get(this.network, tokenId.chain); + if (!portico) throw new Error('Unsupported chain: ' + tokenId.chain); + if (this.isUSDT(tokenId)) { + return portico.pancakeSwapQuoterV2 || portico.uniswapQuoterV2; + } + return portico.uniswapQuoterV2; + } + + private getQuoterContract(tokenId: TokenId) { + const address = this.getQuoterAddress(tokenId); + return new ethers.Contract( + address, + uniswapQuoterV2Abi.fragments, + this.provider, + ); + } + + private isUSDT(tokenId: TokenId) { + const tokenAddress = canonicalAddress(tokenId); + if (isNative(tokenAddress)) return false; + const token = getTokenByAddress(this.network, tokenId.chain, tokenAddress); + return token && token.symbol === 'USDT'; + } }