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 manual TBTC route #816

Merged
merged 9 commits into from
Mar 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions connect/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from "./common.js";
export * from "./tokenBridge/index.js";
export * from "./portico/index.js";
export * from "./cctp/index.js";
export * from "./tbtc/index.js";
5 changes: 5 additions & 0 deletions connect/src/routes/tbtc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
The TBTCRoute enables the transfer of both native and Wormhole-wrapped Ethereum TBTC.

Arbitrum, Base, Optimism, Polygon, and Solana have a gateway contract. This contract mints native TBTC when it receives a payload3 transfer of Wormhole-wrapped TBTC from the token bridge. Conversely, it burns native TBTC, unlocks and bridges Wormhole-wrapped TBTC through the token bridge when bridging out. Wormhole-wrapped TBTC serves as the "highway" asset for bridging TBTC between chains. Transfers of TBTC to chains without a gateway contract are regular token bridge transfers.

You can view the EVM L2WormholeGateway contract code [here](https://github.com/keep-network/tbtc-v2/blob/main/solidity/contracts/l2/L2WormholeGateway.sol). The Solana program is [here](https://github.com/keep-network/tbtc-v2/tree/main/cross-chain/solana).
1 change: 1 addition & 0 deletions connect/src/routes/tbtc/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./tbtc.js";
350 changes: 350 additions & 0 deletions connect/src/routes/tbtc/tbtc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,350 @@
import {
amount,
Chain,
contracts,
finality,
guardians,
Network,
} from "@wormhole-foundation/sdk-base";
import { ManualRoute, StaticRouteMethods } from "../route.js";
import {
ChainAddress,
ChainContext,
deserialize,
isNative,
isSameToken,
serialize,
Signer,
TBTCBridge,
TokenId,
TransactionId,
WormholeMessageId,
} from "@wormhole-foundation/sdk-definitions";
import {
Options,
Quote,
QuoteResult,
Receipt,
TransferParams,
ValidatedTransferParams,
ValidationResult,
} from "../types.js";
import {
AttestedTransferReceipt,
CompletedTransferReceipt,
isAttested,
isSourceFinalized,
isSourceInitiated,
TransferState,
type AttestationReceipt,
type SourceInitiatedTransferReceipt,
type TransferReceipt,
} from "../../types.js";
import { RouteTransferRequest } from "../request.js";
import { Wormhole } from "../../wormhole.js";
import { signSendWait } from "../../common.js";

export namespace TBTCRoute {
export type NormalizedParams = {
amount: amount.Amount;
};

export interface ValidatedParams extends ValidatedTransferParams<Options> {
normalizedParams: NormalizedParams;
}
}

type Op = Options;
type Vp = TBTCRoute.ValidatedParams;

type Tp = TransferParams<Op>;
type Vr = ValidationResult<Op>;

type QR = QuoteResult<Op, Vp>;
type Q = Quote<Op, Vp>;
type R = TransferReceipt<AttestationReceipt<"TBTCBridge">>;

export class TBTCRoute<N extends Network>
extends ManualRoute<N>
implements StaticRouteMethods<typeof TBTCRoute>
{
static meta = {
name: "ManualTBTC",
};

static supportedNetworks(): Network[] {
return ["Mainnet"];
}

static supportedChains(network: Network): Chain[] {
return contracts.tokenBridgeChains(network);
}

static async supportedDestinationTokens<N extends Network>(
sourceToken: TokenId,
fromChain: ChainContext<N>,
toChain: ChainContext<N>,
): Promise<TokenId[]> {
if (!(await this.isSourceTokenSupported(sourceToken, fromChain))) {
return [];
}

const tbtcToken = TBTCBridge.getNativeTbtcToken(toChain.chain);
if (tbtcToken) {
return [tbtcToken];
}

const tb = await toChain.getTokenBridge();
const ethTbtc = TBTCBridge.getNativeTbtcToken("Ethereum")!;
try {
const wrappedTbtc = await tb.getWrappedAsset(ethTbtc);
return [Wormhole.tokenId(toChain.chain, wrappedTbtc.toString())];
} catch (e: any) {
if (e.message.includes("not a wrapped asset")) return [];
throw e;
}
}

getDefaultOptions(): Op {
return {};
}

async validate(request: RouteTransferRequest<N>, params: Tp): Promise<Vr> {
const amount = request.parseAmount(params.amount);

const validatedParams: Vp = {
normalizedParams: {
amount,
},
options: params.options ?? this.getDefaultOptions(),
...params,
};

return { valid: true, params: validatedParams };
}

async quote(request: RouteTransferRequest<N>, params: Vp): Promise<QR> {
const eta =
finality.estimateFinalityTime(request.fromChain.chain) + guardians.guardianAttestationEta;

return {
success: true,
params,
sourceToken: {
token: request.source.id,
amount: params.normalizedParams.amount,
},
destinationToken: {
token: request.destination.id,
amount: params.normalizedParams.amount,
},
eta,
};
}

async initiate(
request: RouteTransferRequest<N>,
signer: Signer,
quote: Q,
to: ChainAddress,
): Promise<R> {
const amt = amount.units(quote.params.normalizedParams.amount);
const isEthereum = request.fromChain.chain === "Ethereum";
const nativeTbtc = TBTCBridge.getNativeTbtcToken(request.fromChain.chain);
const isNativeTbtc = nativeTbtc && isSameToken(quote.sourceToken.token, nativeTbtc);

if (isNativeTbtc && !isEthereum) {
return await this.transferNative(request, signer, to, amt);
}

if (!isNativeTbtc && isEthereum) {
throw new Error("Only tbtc can be transferred on Ethereum");
}

if (!isNativeTbtc) {
const tb = await request.fromChain.getTokenBridge();
const originalAsset = await tb.getOriginalAsset(quote.sourceToken.token.address);
const ethTbtc = TBTCBridge.getNativeTbtcToken("Ethereum")!;
if (!isSameToken(originalAsset, ethTbtc)) {
throw new Error("Can only transfer wrapped tbtc");
}
}

return await this.transferWrapped(request, signer, to, amt);
}

private async transferNative(
request: RouteTransferRequest<N>,
signer: Signer,
to: ChainAddress,
amt: bigint,
): Promise<R> {
const sender = Wormhole.parseAddress(signer.chain(), signer.address());
const bridge = await request.fromChain.getTBTCBridge();
const xfer = bridge.transfer(sender, to, amt);
const txIds = await signSendWait(request.fromChain, xfer, signer);

const receipt: SourceInitiatedTransferReceipt = {
originTxs: txIds,
state: TransferState.SourceInitiated,
from: request.fromChain.chain,
to: request.toChain.chain,
};

return receipt;
}

private async transferWrapped(
request: RouteTransferRequest<N>,
signer: Signer,
to: ChainAddress,
amt: bigint,
): Promise<R> {
const sender = Wormhole.parseAddress(signer.chain(), signer.address());
const toGateway = contracts.tbtc.get(request.fromChain.network, to.chain);
const tb = await request.fromChain.getTokenBridge();
let xfer;

if (toGateway) {
// payload3 transfer to gateway contract
xfer = tb.transfer(
sender,
Wormhole.chainAddress(request.toChain.chain, toGateway),
request.source.id.address,
amt,
// payload is the recipient address
to.address.toUniversalAddress().toUint8Array(),
);
} else {
xfer = tb.transfer(sender, to, request.source.id.address, amt);
}

const txIds = await signSendWait(request.fromChain, xfer, signer);

const receipt: SourceInitiatedTransferReceipt = {
originTxs: txIds,
state: TransferState.SourceInitiated,
from: request.fromChain.chain,
to: request.toChain.chain,
};

return receipt;
}

async complete(signer: Signer, receipt: R): Promise<R> {
if (!isAttested(receipt)) {
throw new Error("The source must be finalized in order to complete the transfer");
}

const sender = Wormhole.parseAddress(signer.chain(), signer.address());
const vaa = receipt.attestation.attestation;
const toChain = this.wh.getChain(receipt.to);
let xfer;

if (vaa.payloadLiteral === "TBTCBridge:GatewayTransfer") {
const bridge = await toChain.getTBTCBridge();
xfer = bridge.redeem(sender, vaa);
} else {
const tb = await toChain.getTokenBridge();
// This is really a TokenBridge:Transfer VAA
const serialized = serialize(vaa);
const tbVaa = deserialize("TokenBridge:Transfer", serialized);
xfer = tb.redeem(sender, tbVaa);
}

const dstTxIds = await signSendWait(toChain, xfer, signer);

return {
...receipt,
state: TransferState.DestinationInitiated,
destinationTxs: dstTxIds,
};
}

async resume(txid: TransactionId): Promise<R> {
const vaa = await this.wh.getVaa(txid.txid, TBTCBridge.getTransferDiscriminator());
if (!vaa) throw new Error("No VAA found for transaction: " + txid);

return {
originTxs: [txid],
state: TransferState.Attested,
from: vaa.emitterChain,
to: vaa.payload.to.chain,
attestation: {
id: {
chain: vaa.emitterChain,
emitter: vaa.emitterAddress,
sequence: vaa.sequence,
},
attestation: vaa,
},
} satisfies AttestedTransferReceipt<AttestationReceipt<"TBTCBridge">>;
}

async *track(receipt: Receipt, timeout?: number) {
if (isSourceInitiated(receipt) || isSourceFinalized(receipt)) {
const { txid } = receipt.originTxs[receipt.originTxs.length - 1]!;

const vaa = await this.wh.getVaa(txid, TBTCBridge.getTransferDiscriminator(), 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<"TBTCBridge">>;

yield receipt;
}

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

yield receipt;
}
}

yield receipt;
}

static async isSourceTokenSupported<N extends Network>(
sourceToken: TokenId,
fromChain: ChainContext<N>,
): Promise<boolean> {
if (isNative(sourceToken.address)) {
return false;
}

// Native tbtc is supported
const nativeTbtc = TBTCBridge.getNativeTbtcToken(fromChain.chain);
if (nativeTbtc && isSameToken(sourceToken, nativeTbtc)) {
return true;
}

// Wormhole-wrapped Ethereum tbtc is supported
const tb = await fromChain.getTokenBridge();
try {
const originalAsset = await tb.getOriginalAsset(sourceToken.address);
return isSameToken(originalAsset, TBTCBridge.getNativeTbtcToken("Ethereum")!);
} catch (e: any) {
if (e.message.includes("not a wrapped asset")) return false;
throw e;
}
}
}
Loading