Skip to content

Commit

Permalink
tokenBridge protocol: add token address conversion methods
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
kev1n-peters committed Jul 31, 2024
1 parent 1b8c5ba commit b89b671
Show file tree
Hide file tree
Showing 12 changed files with 151 additions and 46 deletions.
52 changes: 27 additions & 25 deletions connect/src/protocols/tokenBridge/tokenTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
} from "@wormhole-foundation/sdk-definitions";
import {
TokenBridge,
UniversalAddress,
deserialize,
isNative,
isTokenId,
Expand Down Expand Up @@ -501,6 +502,9 @@ export namespace TokenTransfer {
dstChain: ChainContext<N, DC>,
token: TokenId<SC>,
): Promise<TokenId<DC>> {
if (UniversalAddress.instanceof(token.address)) {
throw new Error("Cannot look up the destination token for a UniversalAddress");
}
// that will be minted when the transfer is redeemed
let lookup: TokenId;
const tb = await srcChain.getTokenBridge();
Expand All @@ -522,12 +526,16 @@ export namespace TokenTransfer {
}

// 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<DC>;
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 };
}
Expand Down Expand Up @@ -627,13 +635,19 @@ export namespace TokenTransfer {
dstChain: ChainContext<N, Chain>,
transfer: Omit<TokenTransferDetails, "from" | "to">,
): Promise<TransferQuote> {
if (UniversalAddress.instanceof(transfer.token.address)) {
throw new Error("Universal addresses are not supported for token transfers");
}
const srcDecimals = await srcChain.getDecimals(transfer.token.address);
const srcAmount = amount.fromBaseUnits(transfer.amount, srcDecimals);
const srcAmountTruncated = amount.truncate(srcAmount, TokenTransfer.MAX_DECIMALS);

const srcTb = await srcChain.getTokenBridge();
const srcToken = isNative(transfer.token.address)
? await srcChain.getNativeWrappedTokenId()
: transfer.token;
? await srcTb.getWrappedNative()
: transfer.token.address;
// @ts-ignore: TS2339
const srcTokenId = Wormhole.tokenId(srcChain.chain, srcToken.toString());

// Ensure the transfer would not violate governor transfer limits
const [tokens, limits] = await Promise.all([
Expand All @@ -643,13 +657,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 {
Expand All @@ -658,7 +670,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),
};
}
}
Expand All @@ -685,26 +697,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,
Expand All @@ -716,7 +718,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,
Expand Down Expand Up @@ -791,11 +793,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,
Expand Down
33 changes: 33 additions & 0 deletions connect/src/wormhole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
getVaaBytesWithRetry,
getVaaWithRetry,
} from "./whscan-api.js";
import { UniversalAddress } from "@wormhole-foundation/sdk-definitions";

type PlatformMap<N extends Network, P extends Platform = Platform> = Map<P, PlatformContext<N, P>>;
type ChainMap<N extends Network, C extends Chain = Chain> = Map<C, ChainContext<N, C>>;
Expand Down Expand Up @@ -222,6 +223,38 @@ export class Wormhole<N extends Network> {
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<C extends Chain>(
chain: C,
token: NativeAddress<C>,
): Promise<UniversalAddress> {
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<C extends Chain>(
chain: C,
originChain: Chain,
token: UniversalAddress,
): Promise<NativeAddress<C>> {
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
*
Expand Down
19 changes: 13 additions & 6 deletions core/definitions/src/protocols/tokenBridge/tokenBridge.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -135,19 +135,26 @@ export interface TokenBridge<N extends Network = Network, C extends Chain = Chai
*/
getOriginalAsset(nativeAddress: TokenAddress<C>): Promise<TokenId<Chain>>;
/**
* 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<C>): Promise<UniversalAddress>;
getTokenUniversalAddress(token: NativeAddress<C>): Promise<UniversalAddress>;
/**
* 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<NativeAddress<C>>;
/**
* 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<TokenAddress<C>>;
getWrappedNative(): Promise<NativeAddress<C>>;
/**
* Check to see if a foreign token has a wrapped version
*
Expand Down
7 changes: 5 additions & 2 deletions core/definitions/src/testing/mocks/tokenBridge.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -20,7 +20,10 @@ export class MockTokenBridge<N extends Network, P extends Platform, C extends Pl
getOriginalAsset(token: TokenAddress<C>): Promise<ChainAddress> {
throw new Error("Method not implemented.");
}
getTokenUniversalAddress(nativeAddress: TokenAddress<C>): Promise<UniversalAddress> {
getTokenUniversalAddress(token: NativeAddress<C>): Promise<UniversalAddress> {
throw new Error("Method not implemented.");
}
getTokenNativeAddress(originChain: Chain, token: UniversalAddress): Promise<NativeAddress<C>> {
throw new Error("Method not implemented.");
}
hasWrappedAsset(original: ChainAddress): Promise<boolean> {
Expand Down
10 changes: 9 additions & 1 deletion platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,20 @@ export class AlgorandTokenBridge<N extends Network, C extends AlgorandChains>
return { chain, address };
}

async getTokenUniversalAddress(token: TokenAddress<C>): Promise<UniversalAddress> {
async getTokenUniversalAddress(token: NativeAddress<C>): Promise<UniversalAddress> {
return new AlgorandAddress(token).toUniversalAddress();
}

async getTokenNativeAddress(
originChain: Chain,
token: UniversalAddress,
): Promise<NativeAddress<C>> {
return new AlgorandAddress(token).toNative() as NativeAddress<C>;
}

// Returns the address of the native version of this asset
async getWrappedAsset(token: TokenId<Chain>): Promise<NativeAddress<C>> {
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,
Expand Down
2 changes: 1 addition & 1 deletion platforms/algorand/src/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 17 additions & 1 deletion platforms/aptos/protocols/tokenBridge/src/tokenBridge.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type {
Chain,
ChainAddress,
ChainId,
ChainsConfig,
Contracts,
NativeAddress,
Network,
TokenBridge,
TokenId,
Expand Down Expand Up @@ -98,10 +100,23 @@ export class AptosTokenBridge<N extends Network, C extends AptosChains>
return { chain, address };
}

async getTokenUniversalAddress(token: AnyAptosAddress): Promise<UniversalAddress> {
async getTokenUniversalAddress(token: NativeAddress<C>): Promise<UniversalAddress> {
return new UniversalAddress(encoding.hex.encode(sha3_256(token.toString()), true));
}

async getTokenNativeAddress(
originChain: Chain,
token: UniversalAddress,
): Promise<NativeAddress<C>> {
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<C>;
}

async hasWrappedAsset(token: TokenId): Promise<boolean> {
try {
await this.getWrappedAsset(token);
Expand All @@ -111,6 +126,7 @@ export class AptosTokenBridge<N extends Network, C extends AptosChains>
}

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.");

Expand Down
10 changes: 9 additions & 1 deletion platforms/cosmwasm/protocols/tokenBridge/src/tokenBridge.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import type {
Chain,
ChainAddress,
ChainsConfig,
Contracts,
Expand Down Expand Up @@ -125,10 +126,17 @@ export class CosmwasmTokenBridge<N extends Network, C extends CosmwasmChains>
};
}

async getTokenUniversalAddress(token: AnyCosmwasmAddress): Promise<UniversalAddress> {
async getTokenUniversalAddress(token: NativeAddress<C>): Promise<UniversalAddress> {
return new CosmwasmAddress(token).toUniversalAddress();
}

async getTokenNativeAddress(
originChain: Chain,
token: UniversalAddress,
): Promise<NativeAddress<C>> {
return new CosmwasmAddress(token).toNative() as NativeAddress<C>;
}

async isTransferCompleted(vaa: TokenBridge.TransferVAA): Promise<boolean> {
const data = encoding.b64.encode(serialize(vaa));
const result = await this.rpc.queryContractSmart(this.tokenBridge, {
Expand Down
2 changes: 2 additions & 0 deletions platforms/cosmwasm/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<N extends Network> extends CosmwasmChain<N, "Wormchain"> {
static chain: "Wormchain" = "Wormchain";
Expand All @@ -25,6 +26,7 @@ export class Gateway<N extends Network> extends CosmwasmChain<N, "Wormchain"> {

// Get the wrapped version of an asset created on wormchain
async getWrappedAsset(token: TokenId): Promise<CosmwasmAddress> {
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));

Expand Down
9 changes: 8 additions & 1 deletion platforms/evm/protocols/tokenBridge/src/tokenBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,18 @@ export class EvmTokenBridge<N extends Network, C extends EvmChains>
}

async getTokenUniversalAddress(
token: TokenAddress<C>,
token: NativeAddress<C>,
): Promise<UniversalAddress> {
return new EvmAddress(token).toUniversalAddress();
}

async getTokenNativeAddress(
originChain: Chain,
token: UniversalAddress,
): Promise<NativeAddress<C>> {
return new EvmAddress(token).toNative() as NativeAddress<C>;
}

async hasWrappedAsset(token: TokenId): Promise<boolean> {
try {
await this.getWrappedAsset(token);
Expand Down
Loading

0 comments on commit b89b671

Please sign in to comment.