Skip to content

Commit

Permalink
Added Portico Bridge USDT support (#552)
Browse files Browse the repository at this point in the history
* Added Portico Bridge USDT support

USDT uses PancakeSwap if available on the chain

* Added native USDT tokens

* removed unused portico api

* add missing wstETHbsc token

* removed wstETHbsc token (no portico pool)

* progress

* more progress

* working

* revert router example changes

* added celo usdt

* Revert "added celo usdt"

This reverts commit 24a386e.

* Reapply "added celo usdt"

This reverts commit a0ad1e8.

* added comment on decimals scaling

* bump version to 0.12.0
  • Loading branch information
kev1n-peters authored Oct 16, 2024
1 parent 0e7e158 commit 3f4c8e9
Show file tree
Hide file tree
Showing 41 changed files with 594 additions and 630 deletions.
6 changes: 3 additions & 3 deletions connect/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@wormhole-foundation/sdk-connect",
"version": "0.11.0",
"version": "0.12.0",
"repository": {
"type": "git",
"url": "git+https://github.com/wormhole-foundation/connect-sdk.git"
Expand Down Expand Up @@ -98,8 +98,8 @@
},
"dependencies": {
"axios": "^1.4.0",
"@wormhole-foundation/sdk-base": "0.11.0",
"@wormhole-foundation/sdk-definitions": "0.11.0"
"@wormhole-foundation/sdk-base": "0.12.0",
"@wormhole-foundation/sdk-definitions": "0.12.0"
},
"type": "module"
}
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 |
```
206 changes: 118 additions & 88 deletions connect/src/routes/portico/automatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import type {
} from "../types.js";
import type {
AttestationReceipt,
AttestedTransferReceipt,
Chain,
ChainContext,
CompletedTransferReceipt,
Network,
Signer,
SourceInitiatedTransferReceipt,
Expand All @@ -26,19 +28,19 @@ import {
Wormhole,
amount,
canonicalAddress,
chainToPlatform,
contracts,
isAttested,
isNative,
isSourceFinalized,
isSourceInitiated,
resolveWrappedToken,
signSendWait,
} from "./../../index.js";
import type { ChainAddress } from "@wormhole-foundation/sdk-definitions";
import type { ChainAddress, WormholeMessageId } from "@wormhole-foundation/sdk-definitions";
import type { RouteTransferRequest } from "../request.js";

export const SLIPPAGE_BPS = 15n; // 0.15%
export const BPS_PER_HUNDRED_PERCENT = 10000n;
export const MAX_SLIPPAGE_BPS = 100n; // 1%
export const BPS_PER_HUNDRED_PERCENT = 10_000n;

export namespace PorticoRoute {
export type Options = {};
Expand Down Expand Up @@ -78,8 +80,6 @@ export class AutomaticPorticoRoute<N extends Network>
name: "AutomaticPortico",
};

private static _supportedTokens = ["WETH", "WSTETH"];

static supportedNetworks(): Network[] {
return ["Mainnet"];
}
Expand All @@ -92,19 +92,12 @@ export class AutomaticPorticoRoute<N extends Network>
}

static async supportedSourceTokens(fromChain: ChainContext<Network>): Promise<TokenId[]> {
const { chain } = fromChain;
const supported = this._supportedTokens
.map((symbol) => {
return filters.bySymbol(fromChain.config.tokenMap!, symbol) ?? [];
})
.flat()
.filter((td) => {
const localOrEth = !td.original || td.original === "Ethereum";
const isAvax = chain === "Avalanche" && isNative(td.address);
return localOrEth && !isAvax;
});

return supported.map((td) => Wormhole.tokenId(chain, td.address));
const pb = await fromChain.getPorticoBridge();
const { tokenMap } = fromChain.config;
return pb
.supportedTokens()
.filter((t) => !tokenMap || filters.byAddress(tokenMap, canonicalAddress(t.token)))
.map((t) => t.token);
}

static async supportedDestinationTokens<N extends Network>(
Expand All @@ -119,51 +112,39 @@ export class AutomaticPorticoRoute<N extends Network>
);
const tokenAddress = canonicalAddress(srcTokenAddress);

// The token that will be used to bridge
const pb = await fromChain.getPorticoBridge();
const transferrableToken = pb.getTransferrableToken(tokenAddress);

// The tokens that _will_ be received on redemption
const redeemToken = await TokenTransfer.lookupDestinationToken(
fromChain,
toChain,
transferrableToken,
);
try {
// The highway token that will be used to bridge
const transferrableToken = await pb.getTransferrableToken(tokenAddress);
// Make sure it exists on the destination chain
await TokenTransfer.lookupDestinationToken(fromChain, toChain, transferrableToken);
} catch {
return [];
}

// Grab the symbol for the token that gets redeemed
const redeemTokenDetails = filters.byAddress(
toChain.config.tokenMap!,
canonicalAddress(redeemToken),
)!;

// Find the local/native version of the same token by symbol
const locallyRedeemable = (
filters.bySymbol(toChain.config.tokenMap!, redeemTokenDetails.symbol) ?? []
)
.filter((td) => {
return !td.original;
})
.map((td) => {
switch (td.symbol) {
case "ETH":
case "WETH":
return Wormhole.tokenId(toChain.chain, td.address);
case "WSTETH":
return Wormhole.tokenId(toChain.chain, td.address);
default:
throw new Error("Unknown symbol: " + redeemTokenDetails.symbol);
}
});

return locallyRedeemable;
// Find the destination token(s) in the same group
const toPb = await toChain.getPorticoBridge();
const tokens = toPb.supportedTokens();
const { tokenMap } = toChain.config;
const group = pb.getTokenGroup(tokenAddress);
return tokens
.filter(
(t) =>
(t.group === group ||
// ETH/WETH supports wrapping/unwrapping
(t.group === "ETH" && group === "WETH") ||
(t.group === "WETH" && group === "ETH")) &&
(!tokenMap || filters.byAddress(tokenMap, canonicalAddress(t.token))),
)
.map((t) => t.token);
}

static isProtocolSupported<N extends Network>(chain: ChainContext<N>): boolean {
return chain.supportsPorticoBridge();
}

async isAvailable(): Promise<boolean> {
// TODO:
return true;
}

Expand All @@ -174,10 +155,10 @@ export class AutomaticPorticoRoute<N extends Network>
async validate(request: RouteTransferRequest<N>, params: TP): Promise<VR> {
try {
if (
chainToPlatform(request.fromChain.chain) !== "Evm" ||
chainToPlatform(request.toChain.chain) !== "Evm"
!AutomaticPorticoRoute.isProtocolSupported(request.fromChain) ||
!AutomaticPorticoRoute.isProtocolSupported(request.toChain)
) {
throw new Error("Only EVM chains are supported");
throw new Error("Protocol not supported");
}

const { fromChain, toChain, source, destination } = request;
Expand All @@ -190,9 +171,11 @@ export class AutomaticPorticoRoute<N extends Network>
const fromPb = await fromChain.getPorticoBridge();
const toPb = await toChain.getPorticoBridge();

const canonicalSourceToken = fromPb.getTransferrableToken(canonicalAddress(sourceToken));
const canonicalSourceToken = await fromPb.getTransferrableToken(
canonicalAddress(sourceToken),
);

const canonicalDestinationToken = toPb.getTransferrableToken(
const canonicalDestinationToken = await toPb.getTransferrableToken(
canonicalAddress(destinationToken),
);

Expand All @@ -216,7 +199,25 @@ export class AutomaticPorticoRoute<N extends Network>

async quote(request: RouteTransferRequest<N>, params: VP): Promise<QR> {
try {
const swapAmounts = await this.quoteUniswap(request, params);
const swapAmounts = await this.fetchSwapQuote(request, params);

// destination token may have a different number of decimals than the source token
// so we need to scale the amounts to the token with the most decimals
// before comparing them
const maxDecimals = Math.max(request.source.decimals, request.destination.decimals);
const scaledAmount = amount.units(amount.scale(params.normalizedParams.amount, maxDecimals));
const scaledMinAmountFinish = amount.units(
amount.scale(
amount.fromBaseUnits(swapAmounts.minAmountFinish, request.destination.decimals),
maxDecimals,
),
);
// if the slippage is more than 100bps, this likely means that the pools are unbalanced
if (
scaledMinAmountFinish <
scaledAmount - (scaledAmount * MAX_SLIPPAGE_BPS) / BPS_PER_HUNDRED_PERCENT
)
throw new Error("Slippage too high");

const pb = await request.toChain.getPorticoBridge();

Expand All @@ -230,9 +231,9 @@ export class AutomaticPorticoRoute<N extends Network>
relayerFee: fee,
};

let destinationAmount = details.swapAmounts.minAmountFinish - fee;
const destinationAmount = details.swapAmounts.minAmountFinish - fee;

if (Number(destinationAmount) < 0) {
if (destinationAmount < 0n) {
return {
success: false,
error: new Error(
Expand Down Expand Up @@ -275,13 +276,17 @@ export class AutomaticPorticoRoute<N extends Network>
const destToken = request.destination!.id;

const fromPorticoBridge = await request.fromChain.getPorticoBridge();
const tokenGroup = fromPorticoBridge.getTokenGroup(sourceToken.toString());
const toPorticoBridge = await request.toChain.getPorticoBridge();
const destPorticoAddress = toPorticoBridge.getPorticoAddress(tokenGroup);

const xfer = fromPorticoBridge.transfer(
Wormhole.parseAddress(sender.chain(), sender.address()),
to,
sourceToken,
amount.units(params.normalizedParams.amount),
destToken!,
destPorticoAddress,
details!,
);

Expand All @@ -296,14 +301,49 @@ export class AutomaticPorticoRoute<N extends Network>
}

async *track(receipt: R, timeout?: number) {
if (!isSourceInitiated(receipt)) throw new Error("Source must be initiated");
if (isSourceInitiated(receipt) || isSourceFinalized(receipt)) {
const { txid } = receipt.originTxs[receipt.originTxs.length - 1]!;

const vaa = await this.wh.getVaa(txid, "PorticoBridge:Transfer", timeout);
if (!vaa) throw new Error("No VAA found for transaction: " + txid);

const msgId: WormholeMessageId = {
chain: vaa.emitterChain,
emitter: vaa.emitterAddress,
sequence: vaa.sequence,
};

receipt = {
...receipt,
state: TransferState.Attested,
attestation: {
id: msgId,
attestation: vaa,
},
} satisfies AttestedTransferReceipt<AttestationReceipt<"PorticoBridge">>;

const { txid } = receipt.originTxs[receipt.originTxs.length - 1]!;
const vaa = await this.wh.getVaa(txid, "TokenBridge:TransferWithPayload", timeout);
if (!vaa) throw new Error("No VAA found for transaction: " + txid);
yield receipt;
}

if (isAttested(receipt)) {
const toChain = this.wh.getChain(receipt.to);
const toPorticoBridge = await toChain.getPorticoBridge();
const isCompleted = await toPorticoBridge.isTransferCompleted(
receipt.attestation.attestation,
);
if (isCompleted) {
receipt = {
...receipt,
state: TransferState.DestinationFinalized,
} satisfies CompletedTransferReceipt<AttestationReceipt<"PorticoBridge">>;

yield receipt;
}
}

// TODO: handle swap failed case (highway token received)

const parsed = PorticoBridge.deserializePayload(vaa.payload.payload);
yield { ...receipt, vaa, parsed };
yield receipt;
}

async complete(signer: Signer<N>, receipt: R): Promise<TransactionId[]> {
Expand All @@ -316,47 +356,37 @@ export class AutomaticPorticoRoute<N extends Network>
return await signSendWait(toChain, xfer, signer);
}

private async quoteUniswap(request: RouteTransferRequest<N>, params: VP) {
const fromPorticoBridge = await request.fromChain.getPorticoBridge();
const startQuote = await fromPorticoBridge.quoteSwap(
private async fetchSwapQuote(request: RouteTransferRequest<N>, params: VP) {
const fromPb = await request.fromChain.getPorticoBridge();
const xferAmount = amount.units(params.normalizedParams.amount);
const tokenGroup = fromPb.getTokenGroup(canonicalAddress(params.normalizedParams.sourceToken));
const startQuote = await fromPb.quoteSwap(
params.normalizedParams.sourceToken.address,
params.normalizedParams.canonicalSourceToken.address,
amount.units(params.normalizedParams.amount),
tokenGroup,
xferAmount,
);
const startSlippage = (startQuote * SLIPPAGE_BPS) / BPS_PER_HUNDRED_PERCENT;

if (startSlippage >= startQuote) throw new Error("Start slippage too high");

const toPorticoBridge = await request.toChain.getPorticoBridge();
const toPb = await request.toChain.getPorticoBridge();
const minAmountStart = startQuote - startSlippage;
const finishQuote = await toPorticoBridge.quoteSwap(
const finishQuote = await toPb.quoteSwap(
params.normalizedParams.canonicalDestinationToken.address,
params.normalizedParams.destinationToken.address,
tokenGroup,
minAmountStart,
);
const finishSlippage = (finishQuote * SLIPPAGE_BPS) / BPS_PER_HUNDRED_PERCENT;

if (finishSlippage >= finishQuote) throw new Error("Finish slippage too high");

const minAmountFinish = finishQuote - finishSlippage;
const amountFinishQuote = await toPorticoBridge.quoteSwap(
params.normalizedParams.canonicalDestinationToken.address,
params.normalizedParams.destinationToken.address,
startQuote, // no slippage
);
// the expected receive amount is the amount out from the swap
// minus 5bps slippage
const amountFinishSlippage = (amountFinishQuote * 5n) / BPS_PER_HUNDRED_PERCENT;
if (amountFinishSlippage >= amountFinishQuote)
throw new Error("Amount finish slippage too high");

const amountFinish = amountFinishQuote - amountFinishSlippage;
if (amountFinish <= minAmountFinish) throw new Error("Amount finish too low");

return {
minAmountStart: minAmountStart,
minAmountFinish: minAmountFinish,
amountFinish: amountFinish,
};
}
}
6 changes: 5 additions & 1 deletion connect/src/routes/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,13 @@ export class RouteTransferRequest<N extends Network> {
};

if (quote.relayFee) {
const relayFeeChain =
quote.relayFee.token.chain === this.fromChain.chain ? this.fromChain : this.toChain;
const relayFeeDecimals = await relayFeeChain.getDecimals(quote.relayFee.token.address);

dq.relayFee = {
token: quote.relayFee.token,
amount: amount.fromBaseUnits(quote.relayFee.amount, this.source.decimals),
amount: amount.fromBaseUnits(quote.relayFee.amount, relayFeeDecimals),
};
}

Expand Down
Loading

0 comments on commit 3f4c8e9

Please sign in to comment.