From 4d01fbbc9375b1324fa69124ce31c3ac6410805e Mon Sep 17 00:00:00 2001 From: Kevin Peters Date: Wed, 31 Jul 2024 15:23:56 -0500 Subject: [PATCH] tokenBridge protocol: add token address conversion methods - Added getTokenUniversalAddress and getTokenNativeAddress methods to each platform's token bridge protocol implementation. - Some chains like Aptos and Sui require fetching on-chain data for token address conversions (universal to native and vice versa). - Fixed issue where lookupDestinationToken would return a universal address, causing issues in functions expecting a native address. - lookupDestinationToken now consistently returns a native address. - Resolved issues with transferring native Sui and Aptos tokens back to their origin chains. --- .../protocols/tokenBridge/tokenTransfer.ts | 79 +++++++++++-------- connect/src/wormhole.ts | 33 ++++++++ .../src/protocols/tokenBridge/tokenBridge.ts | 19 +++-- .../src/testing/mocks/tokenBridge.ts | 7 +- .../protocols/tokenBridge/src/tokenBridge.ts | 10 ++- platforms/algorand/src/address.ts | 2 +- .../protocols/tokenBridge/src/tokenBridge.ts | 18 ++++- .../protocols/tokenBridge/src/tokenBridge.ts | 10 ++- platforms/cosmwasm/src/gateway.ts | 2 + .../protocols/tokenBridge/src/tokenBridge.ts | 9 ++- .../protocols/tokenBridge/src/tokenBridge.ts | 13 ++- .../protocols/tokenBridge/src/tokenBridge.ts | 22 ++++-- 12 files changed, 172 insertions(+), 52 deletions(-) diff --git a/connect/src/protocols/tokenBridge/tokenTransfer.ts b/connect/src/protocols/tokenBridge/tokenTransfer.ts index abdb611a7..7e5ef7c77 100644 --- a/connect/src/protocols/tokenBridge/tokenTransfer.ts +++ b/connect/src/protocols/tokenBridge/tokenTransfer.ts @@ -5,6 +5,7 @@ import type { AutomaticTokenBridge, ChainContext, Signer, + NativeAddress, TokenId, TokenTransferDetails, TransactionId, @@ -14,6 +15,7 @@ import type { } from "@wormhole-foundation/sdk-definitions"; import { TokenBridge, + UniversalAddress, deserialize, isNative, isTokenId, @@ -501,7 +503,6 @@ export namespace TokenTransfer { dstChain: ChainContext, token: TokenId, ): Promise> { - // that will be minted when the transfer is redeemed let lookup: TokenId; const tb = await srcChain.getTokenBridge(); if (isNative(token.address)) { @@ -514,20 +515,36 @@ export namespace TokenTransfer { } else { try { // otherwise, check to see if it is a wrapped token locally - lookup = await tb.getOriginalAsset(token.address); + let address: NativeAddress; + if (UniversalAddress.instanceof(token.address)) { + address = (await tb.getWrappedAsset(token)) as NativeAddress; + } else { + address = token.address; + } + lookup = await tb.getOriginalAsset(address); } catch (e) { // not a from-chain native wormhole-wrapped one - lookup = { chain: token.chain, address: await tb.getTokenUniversalAddress(token.address) }; + let address: NativeAddress; + if (UniversalAddress.instanceof(token.address)) { + address = await tb.getTokenNativeAddress(srcChain.chain, token.address); + } else { + address = token.address; + } + lookup = { chain: token.chain, address: await tb.getTokenUniversalAddress(address) }; } } // if the token id is actually native to the destination, return it + const dstTb = await dstChain.getTokenBridge(); if (lookup.chain === dstChain.chain) { - return lookup as TokenId; + const nativeAddress = await dstTb.getTokenNativeAddress( + lookup.chain, + lookup.address as UniversalAddress, + ); + return { chain: dstChain.chain, address: nativeAddress }; } // otherwise, figure out what the token address representing the wormhole-wrapped token we're transferring - const dstTb = await dstChain.getTokenBridge(); const dstAddress = await dstTb.getWrappedAsset(lookup); return { chain: dstChain.chain, address: dstAddress }; } @@ -627,14 +644,26 @@ export namespace TokenTransfer { dstChain: ChainContext, transfer: Omit, ): Promise { - const srcDecimals = await srcChain.getDecimals(transfer.token.address); + const srcTb = await srcChain.getTokenBridge(); + let srcToken: NativeAddress; + if (isNative(transfer.token.address)) { + srcToken = await srcTb.getWrappedNative(); + } else if (UniversalAddress.instanceof(transfer.token.address)) { + try { + srcToken = (await srcTb.getWrappedAsset(transfer.token)) as NativeAddress; + } catch (e) { + srcToken = await srcTb.getTokenNativeAddress(srcChain.chain, transfer.token.address); + } + } else { + srcToken = transfer.token.address; + } + // @ts-ignore: TS2339 + const srcTokenId = Wormhole.tokenId(srcChain.chain, srcToken.toString()); + + const srcDecimals = await srcChain.getDecimals(srcToken); const srcAmount = amount.fromBaseUnits(transfer.amount, srcDecimals); const srcAmountTruncated = amount.truncate(srcAmount, TokenTransfer.MAX_DECIMALS); - const srcToken = isNative(transfer.token.address) - ? await srcChain.getNativeWrappedTokenId() - : transfer.token; - // Ensure the transfer would not violate governor transfer limits const [tokens, limits] = await Promise.all([ getGovernedTokens(wh.config.api), @@ -643,13 +672,11 @@ export namespace TokenTransfer { const warnings: QuoteWarning[] = []; if (limits !== null && srcChain.chain in limits && tokens !== null) { - const srcTb = await srcChain.getTokenBridge(); - let origAsset: TokenId; if (isNative(transfer.token.address)) { origAsset = { chain: srcChain.chain, - address: await srcTb.getTokenUniversalAddress(srcToken.address), + address: await srcTb.getTokenUniversalAddress(srcToken), }; } else { try { @@ -658,7 +685,7 @@ export namespace TokenTransfer { if (!e.message.includes("not a wrapped asset")) throw e; origAsset = { chain: srcChain.chain, - address: await srcTb.getTokenUniversalAddress(srcToken.address), + address: await srcTb.getTokenUniversalAddress(srcToken), }; } } @@ -685,26 +712,16 @@ export namespace TokenTransfer { } const dstToken = await TokenTransfer.lookupDestinationToken(srcChain, dstChain, transfer.token); - // TODO: this is a hack to get the aptos native gas token decimals - // which requires us to pass in a token address in canonical form - // but the `dstToken.address` here is in universal form - if (dstChain.chain === "Aptos" && dstToken.chain === "Aptos") { - const dstTb = await dstChain.getTokenBridge(); - const wrappedNative = await dstTb.getWrappedNative(); - if ( - dstToken.address.toString() === - (await dstTb.getTokenUniversalAddress(wrappedNative)).toString() - ) { - dstToken.address = wrappedNative; - } - } const dstDecimals = await dstChain.getDecimals(dstToken.address); const dstAmountReceivable = amount.scale(srcAmountTruncated, dstDecimals); const eta = finality.estimateFinalityTime(srcChain.chain); if (!transfer.automatic) { return { - sourceToken: { token: srcToken, amount: amount.units(srcAmountTruncated) }, + sourceToken: { + token: srcTokenId, + amount: amount.units(srcAmountTruncated), + }, destinationToken: { token: dstToken, amount: amount.units(dstAmountReceivable) }, warnings: warnings.length > 0 ? warnings : undefined, eta, @@ -716,7 +733,7 @@ export namespace TokenTransfer { // The fee is removed from the amount transferred // quoted on the source chain const stb = await srcChain.getAutomaticTokenBridge(); - const fee = await stb.getRelayerFee(dstChain.chain, srcToken.address); + const fee = await stb.getRelayerFee(dstChain.chain, srcToken); const feeAmountDest = amount.scale( amount.truncate(amount.fromBaseUnits(fee, srcDecimals), TokenTransfer.MAX_DECIMALS), dstDecimals, @@ -791,11 +808,11 @@ export namespace TokenTransfer { return { sourceToken: { - token: srcToken, + token: srcTokenId, amount: amount.units(srcAmountTruncated), }, destinationToken: { token: dstToken, amount: destAmountLessFee }, - relayFee: { token: srcToken, amount: fee }, + relayFee: { token: srcTokenId, amount: fee }, destinationNativeGas, warnings: warnings.length > 0 ? warnings : undefined, eta, diff --git a/connect/src/wormhole.ts b/connect/src/wormhole.ts index b66f15792..5352da378 100644 --- a/connect/src/wormhole.ts +++ b/connect/src/wormhole.ts @@ -39,6 +39,7 @@ import { getVaaBytesWithRetry, getVaaWithRetry, } from "./whscan-api.js"; +import { UniversalAddress } from "@wormhole-foundation/sdk-definitions"; type PlatformMap = Map>; type ChainMap = Map>; @@ -222,6 +223,38 @@ export class Wormhole { return await tb.getOriginalAsset(token.address); } + /** + * Returns the UniversalAddress of the token. This may require fetching on-chain data. + * @param chain The chain to get the UniversalAddress for + * @param token The address to get the UniversalAddress for + * @returns The UniversalAddress of the token + */ + async getTokenUniversalAddress( + chain: C, + token: NativeAddress, + ): Promise { + const ctx = this.getChain(chain); + const tb = await ctx.getTokenBridge(); + return await tb.getTokenUniversalAddress(token); + } + + /** + * Returns the native address of the token. This may require fetching on-chain data. + * @param chain The chain to get the native address for + * @param originChain The chain the token is from / native to + * @param token The address to get the native address for + * @returns The native address of the token + */ + async getTokenNativeAddress( + chain: C, + originChain: Chain, + token: UniversalAddress, + ): Promise> { + const ctx = this.getChain(chain); + const tb = await ctx.getTokenBridge(); + return await tb.getTokenNativeAddress(originChain, token); + } + /** * Gets the number of decimals for a token on a given chain * diff --git a/core/definitions/src/protocols/tokenBridge/tokenBridge.ts b/core/definitions/src/protocols/tokenBridge/tokenBridge.ts index 3982b0730..56b263936 100644 --- a/core/definitions/src/protocols/tokenBridge/tokenBridge.ts +++ b/core/definitions/src/protocols/tokenBridge/tokenBridge.ts @@ -1,7 +1,7 @@ import type { Chain, Network } from "@wormhole-foundation/sdk-base"; import { lazyInstantiate } from "@wormhole-foundation/sdk-base"; -import type { AccountAddress, ChainAddress } from "../../address.js"; -import { UniversalAddress } from "../../universalAddress.js"; +import type { AccountAddress, ChainAddress, NativeAddress } from "../../address.js"; +import type { UniversalAddress } from "../../universalAddress.js"; import type { TokenAddress, TokenId } from "../../types.js"; import type { UnsignedTransaction } from "../../unsignedTransaction.js"; import type { ProtocolPayload, ProtocolVAA } from "./../../vaa/index.js"; @@ -135,19 +135,26 @@ export interface TokenBridge): Promise>; /** - * Returns the UniversalAddress of the token. This may require retrieving data on-chain. + * Returns the UniversalAddress of the token. This may require fetching on-chain data. * - * @param nativeAddress The address to get the UniversalAddress for + * @param token The address to get the UniversalAddress for * @returns The UniversalAddress of the token */ - getTokenUniversalAddress(nativeAddress: TokenAddress): Promise; + getTokenUniversalAddress(token: NativeAddress): Promise; + /** + * Returns the native address of the token. This may require fetching on-chain data. + * @param originChain The chain the token is from / native to + * @param token The address to get the native address for + * @returns The native address of the token + */ + getTokenNativeAddress(originChain: Chain, token: UniversalAddress): Promise>; /** * returns the wrapped version of the native asset * * @returns The address of the native gas token that has been wrapped * for use where the gas token is not possible to use (e.g. bridging) */ - getWrappedNative(): Promise>; + getWrappedNative(): Promise>; /** * Check to see if a foreign token has a wrapped version * diff --git a/core/definitions/src/testing/mocks/tokenBridge.ts b/core/definitions/src/testing/mocks/tokenBridge.ts index f56c00004..283a97345 100644 --- a/core/definitions/src/testing/mocks/tokenBridge.ts +++ b/core/definitions/src/testing/mocks/tokenBridge.ts @@ -1,4 +1,4 @@ -import type { Network, Platform, PlatformToChains } from "@wormhole-foundation/sdk-base"; +import type { Chain, Network, Platform, PlatformToChains } from "@wormhole-foundation/sdk-base"; import type { ChainAddress, NativeAddress, @@ -20,7 +20,10 @@ export class MockTokenBridge): Promise { throw new Error("Method not implemented."); } - getTokenUniversalAddress(nativeAddress: TokenAddress): Promise { + getTokenUniversalAddress(token: NativeAddress): Promise { + throw new Error("Method not implemented."); + } + getTokenNativeAddress(originChain: Chain, token: UniversalAddress): Promise> { throw new Error("Method not implemented."); } hasWrappedAsset(original: ChainAddress): Promise { diff --git a/platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts b/platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts index 189006fb7..538ff546f 100644 --- a/platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts +++ b/platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts @@ -146,12 +146,20 @@ export class AlgorandTokenBridge return { chain, address }; } - async getTokenUniversalAddress(token: TokenAddress): Promise { + async getTokenUniversalAddress(token: NativeAddress): Promise { return new AlgorandAddress(token).toUniversalAddress(); } + async getTokenNativeAddress( + originChain: Chain, + token: UniversalAddress, + ): Promise> { + return new AlgorandAddress(token).toNative() as NativeAddress; + } + // Returns the address of the native version of this asset async getWrappedAsset(token: TokenId): Promise> { + if (isNative(token.address)) throw new Error("native asset cannot be a wrapped asset"); const storageAccount = StorageLogicSig.forWrappedAsset(this.tokenBridgeAppId, token); const data = await StorageLogicSig.decodeLocalState( this.connection, diff --git a/platforms/algorand/src/address.ts b/platforms/algorand/src/address.ts index 472d6aecf..7c7a75162 100644 --- a/platforms/algorand/src/address.ts +++ b/platforms/algorand/src/address.ts @@ -8,7 +8,7 @@ import { _platform, safeBigIntToNumber } from "./types.js"; export const AlgorandZeroAddress = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ"; // Note: for ASA/App IDs we encode them as 8 bytes at the start of -// the 32 byte adddress bytes. +// the 32 byte address bytes. export class AlgorandAddress implements Address { static readonly byteSize = 32; diff --git a/platforms/aptos/protocols/tokenBridge/src/tokenBridge.ts b/platforms/aptos/protocols/tokenBridge/src/tokenBridge.ts index 4c1acdc36..3f503dce1 100644 --- a/platforms/aptos/protocols/tokenBridge/src/tokenBridge.ts +++ b/platforms/aptos/protocols/tokenBridge/src/tokenBridge.ts @@ -1,8 +1,10 @@ import type { + Chain, ChainAddress, ChainId, ChainsConfig, Contracts, + NativeAddress, Network, TokenBridge, TokenId, @@ -98,10 +100,23 @@ export class AptosTokenBridge return { chain, address }; } - async getTokenUniversalAddress(token: AnyAptosAddress): Promise { + async getTokenUniversalAddress(token: NativeAddress): Promise { return new UniversalAddress(encoding.hex.encode(sha3_256(token.toString()), true)); } + async getTokenNativeAddress( + originChain: Chain, + token: UniversalAddress, + ): Promise> { + const assetType = + originChain === this.chain + ? await this.getTypeFromExternalAddress(token.toString()) + : await this.getAssetFullyQualifiedType({ chain: originChain, address: token }); + + if (!assetType) throw new Error("Invalid asset address."); + return new AptosAddress(assetType) as NativeAddress; + } + async hasWrappedAsset(token: TokenId): Promise { try { await this.getWrappedAsset(token); @@ -111,6 +126,7 @@ export class AptosTokenBridge } async getWrappedAsset(token: TokenId) { + if (isNative(token.address)) throw new Error("native asset cannot be a wrapped asset"); const assetFullyQualifiedType = await this.getAssetFullyQualifiedType(token); if (!assetFullyQualifiedType) throw new Error("Invalid asset address."); diff --git a/platforms/cosmwasm/protocols/tokenBridge/src/tokenBridge.ts b/platforms/cosmwasm/protocols/tokenBridge/src/tokenBridge.ts index e0b05bda6..06b4db4ee 100644 --- a/platforms/cosmwasm/protocols/tokenBridge/src/tokenBridge.ts +++ b/platforms/cosmwasm/protocols/tokenBridge/src/tokenBridge.ts @@ -1,5 +1,6 @@ import type { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; import type { + Chain, ChainAddress, ChainsConfig, Contracts, @@ -125,10 +126,17 @@ export class CosmwasmTokenBridge }; } - async getTokenUniversalAddress(token: AnyCosmwasmAddress): Promise { + async getTokenUniversalAddress(token: NativeAddress): Promise { return new CosmwasmAddress(token).toUniversalAddress(); } + async getTokenNativeAddress( + originChain: Chain, + token: UniversalAddress, + ): Promise> { + return new CosmwasmAddress(token).toNative() as NativeAddress; + } + async isTransferCompleted(vaa: TokenBridge.TransferVAA): Promise { const data = encoding.b64.encode(serialize(vaa)); const result = await this.rpc.queryContractSmart(this.tokenBridge, { diff --git a/platforms/cosmwasm/src/gateway.ts b/platforms/cosmwasm/src/gateway.ts index c74e593eb..c37591b36 100644 --- a/platforms/cosmwasm/src/gateway.ts +++ b/platforms/cosmwasm/src/gateway.ts @@ -12,6 +12,7 @@ import { CosmwasmChain } from "./chain.js"; import { IBC_TRANSFER_PORT } from "./constants.js"; import { CosmwasmPlatform } from "./platform.js"; import type { CosmwasmChains } from "./types.js"; +import { isNative } from "@wormhole-foundation/sdk-connect"; export class Gateway extends CosmwasmChain { static chain: "Wormchain" = "Wormchain"; @@ -25,6 +26,7 @@ export class Gateway extends CosmwasmChain { // Get the wrapped version of an asset created on wormchain async getWrappedAsset(token: TokenId): Promise { + if (isNative(token.address)) throw new Error("native asset cannot be a wrapped asset"); const tb = await this.getTokenBridge(); const wrappedAsset = new CosmwasmAddress(await tb.getWrappedAsset(token)); diff --git a/platforms/evm/protocols/tokenBridge/src/tokenBridge.ts b/platforms/evm/protocols/tokenBridge/src/tokenBridge.ts index 84fc413b5..9ed731131 100644 --- a/platforms/evm/protocols/tokenBridge/src/tokenBridge.ts +++ b/platforms/evm/protocols/tokenBridge/src/tokenBridge.ts @@ -106,11 +106,18 @@ export class EvmTokenBridge } async getTokenUniversalAddress( - token: TokenAddress, + token: NativeAddress, ): Promise { return new EvmAddress(token).toUniversalAddress(); } + async getTokenNativeAddress( + originChain: Chain, + token: UniversalAddress, + ): Promise> { + return new EvmAddress(token).toNative() as NativeAddress; + } + async hasWrappedAsset(token: TokenId): Promise { try { await this.getWrappedAsset(token); diff --git a/platforms/solana/protocols/tokenBridge/src/tokenBridge.ts b/platforms/solana/protocols/tokenBridge/src/tokenBridge.ts index f01c3e909..a2f284ab8 100644 --- a/platforms/solana/protocols/tokenBridge/src/tokenBridge.ts +++ b/platforms/solana/protocols/tokenBridge/src/tokenBridge.ts @@ -1,8 +1,10 @@ import type { + Chain, ChainAddress, ChainId, ChainsConfig, Contracts, + NativeAddress, Network, Platform, TokenBridge, @@ -157,7 +159,7 @@ export class SolanaTokenBridge return { chain: toChain(meta.chain as ChainId), - address: new UniversalAddress(meta.tokenAddress), + address: new UniversalAddress(new Uint8Array(meta.tokenAddress)), }; } catch (_) { throw ErrNotWrapped(token.toString()); @@ -165,11 +167,18 @@ export class SolanaTokenBridge } async getTokenUniversalAddress( - token: AnySolanaAddress, + token: NativeAddress, ): Promise { return new SolanaAddress(token).toUniversalAddress(); } + async getTokenNativeAddress( + originChain: Chain, + token: UniversalAddress, + ): Promise> { + return new SolanaAddress(token).toNative() as NativeAddress; + } + async hasWrappedAsset(token: TokenId): Promise { try { await this.getWrappedAsset(token); diff --git a/platforms/sui/protocols/tokenBridge/src/tokenBridge.ts b/platforms/sui/protocols/tokenBridge/src/tokenBridge.ts index 929b0b48e..14dff4dd2 100644 --- a/platforms/sui/protocols/tokenBridge/src/tokenBridge.ts +++ b/platforms/sui/protocols/tokenBridge/src/tokenBridge.ts @@ -29,7 +29,8 @@ import { toNative, } from "@wormhole-foundation/sdk-connect"; -import type { SuiAddress, SuiBuildOutput, SuiChains } from "@wormhole-foundation/sdk-sui"; +import type { SuiBuildOutput, SuiChains } from "@wormhole-foundation/sdk-sui"; +import { SuiAddress } from "@wormhole-foundation/sdk-sui"; import { SuiPlatform, SuiUnsignedTransaction, @@ -148,7 +149,7 @@ export class SuiTokenBridge implements T throw ErrNotWrapped(coinType); } - async getTokenUniversalAddress(token: TokenAddress): Promise { + async getTokenUniversalAddress(token: NativeAddress): Promise { let coinType = (token as SuiAddress).getCoinType(); if (!isValidSuiType(coinType)) throw new Error(`Invalid Sui type: ${coinType}`); @@ -190,11 +191,20 @@ export class SuiTokenBridge implements T } throw new Error(`Token of type ${coinType} is not a native asset`); + } - //// TODO: implement - //return new UniversalAddress( - // Buffer.from("9258181f5ceac8dbffb7030890243caed69a9599d2886d957a9cb7656af3bdb3", "hex"), - //); + async getTokenNativeAddress( + originChain: Chain, + token: UniversalAddress, + ): Promise> { + const address = await getTokenCoinType( + this.provider, + this.tokenBridgeObjectId, + token.toUint8Array(), + toChainId(originChain), + ); + if (!address) throw new Error(`Token ${token.toString()} not found in token registry`); + return new SuiAddress(address) as NativeAddress; } async hasWrappedAsset(token: TokenId): Promise {