Skip to content

Commit

Permalink
Added Portico Bridge USDT support
Browse files Browse the repository at this point in the history
USDT uses PancakeSwap if available on the chain
  • Loading branch information
kev1n-peters committed May 15, 2024
1 parent d180aca commit 8d2fd18
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 58 deletions.
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';
}
}

0 comments on commit 8d2fd18

Please sign in to comment.