Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Portico Bridge USDT support #552

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions connect/src/routes/portico/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
```
21 changes: 13 additions & 8 deletions connect/src/routes/portico/automatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
chainToPlatform,
contracts,
isAttested,
isNative,
isSourceInitiated,
resolveWrappedToken,
signSendWait,
Expand Down Expand Up @@ -76,7 +75,8 @@ export class AutomaticPorticoRoute<N extends Network>
name: "AutomaticPortico",
};

private static _supportedTokens = ["WETH", "WSTETH"];
// TODO: "ETH"?
private static _supportedTokens = ["WETH", "wstETH", "USDT"];

static supportedNetworks(): Network[] {
return ["Mainnet"];
Expand All @@ -97,9 +97,8 @@ export class AutomaticPorticoRoute<N extends Network>
})
.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));
Expand Down Expand Up @@ -145,8 +144,8 @@ export class AutomaticPorticoRoute<N extends Network>
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);
Expand Down Expand Up @@ -314,10 +313,11 @@ export class AutomaticPorticoRoute<N extends Network>

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;

Expand Down Expand Up @@ -349,6 +349,11 @@ export class AutomaticPorticoRoute<N extends Network>
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,
Expand Down
4 changes: 2 additions & 2 deletions connect/src/wormhole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ export class Wormhole<N extends Network> {
}

/**
* 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
Expand All @@ -380,7 +380,7 @@ export class Wormhole<N extends Network> {
}

/**
* 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"
Expand Down
38 changes: 27 additions & 11 deletions core/base/src/constants/contracts/portico.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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]>
2 changes: 1 addition & 1 deletion core/definitions/src/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type GetNativeAddress<P extends Platform> = 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<C extends Chain> = GetNativeAddress<ChainToPlatform<C>>;

/** A union type representing a parsed address */
Expand Down
2 changes: 1 addition & 1 deletion core/definitions/src/protocols/portico/porticoLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
4 changes: 2 additions & 2 deletions platforms/evm/protocols/portico/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
90 changes: 60 additions & 30 deletions platforms/evm/protocols/portico/src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
nativeChainIds,
resolveWrappedToken,
serialize,
toChain,
toChainId,
} from '@wormhole-foundation/sdk-connect';
import type { EvmChains } from '@wormhole-foundation/sdk-evm';
Expand All @@ -37,18 +38,16 @@ 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,
C extends EvmChains = EvmChains,
> implements PorticoBridge<N, C>
{
chainId: bigint;
porticoAddress: string;
uniswapAddress: string;

porticoContract: ethers.Contract;
uniswapContract: ethers.Contract;
core: EvmWormholeCore<N, C>;

constructor(
Expand All @@ -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<N extends Network>(
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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),
};
Expand All @@ -191,7 +173,12 @@ export class EvmPorticoBridge<
}

async *redeem(sender: AccountAddress<C>, 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));

Expand Down Expand Up @@ -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]);

Expand Down Expand Up @@ -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';
}
}
Loading