Skip to content

Commit 3c5b47e

Browse files
authored
sdk: evm - support gas token transfers (wormhole-foundation#601)
* sdk: evm - support gas token transfers The NTT manager doesn't support gas token transfers directly, so we have to wrap it first. * remove unused imports
1 parent 5f2882e commit 3c5b47e

File tree

5 files changed

+109
-45
lines changed

5 files changed

+109
-45
lines changed

evm/ts/src/ntt.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,19 @@ export class EvmNtt<N extends Network, C extends EvmChains>
472472
options
473473
);
474474

475+
if (options.wrapNative) {
476+
// TODO: the contract should handle this for us
477+
const wrappedNative = new Contract(this.tokenAddress, [
478+
"function deposit() public payable",
479+
]);
480+
481+
const txReq = await wrappedNative
482+
.getFunction("deposit")
483+
.populateTransaction({ value: amount });
484+
485+
yield this.createUnsignedTx(addFrom(txReq, senderAddress), "Ntt.Deposit");
486+
}
487+
475488
//TODO check for ERC-2612 (permit) support on token?
476489
const tokenContract = EvmPlatform.getTokenImplementation(
477490
this.provider,
@@ -487,6 +500,7 @@ export class EvmNtt<N extends Network, C extends EvmChains>
487500
this.managerAddress,
488501
amount
489502
);
503+
490504
yield this.createUnsignedTx(addFrom(txReq, senderAddress), "Ntt.Approve");
491505
}
492506

sdk/definitions/src/ntt.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export namespace Ntt {
5858
automatic?: boolean;
5959
/** How much native gas on the destination to send along with the transfer */
6060
gasDropoff?: bigint;
61+
/** Whether or not the token needs to be wrapped, only relevant for gas token transfers */
62+
wrapNative?: boolean;
6163
};
6264

6365
// TODO: what are the set of attestation types for Ntt?
@@ -154,7 +156,7 @@ export namespace Ntt {
154156
* Ntt is the interface for the Ntt
155157
*
156158
* The Ntt is responsible for managing the coordination between the token contract and
157-
* the transceiver(s). It is also responsible for managing the capacity of inbound or outbount transfers.
159+
* the transceiver(s). It is also responsible for managing the capacity of inbound or outbound transfers.
158160
*
159161
* @typeparam N the network
160162
* @typeparam C the chain

sdk/route/src/automatic.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
finality,
2020
isAttested,
2121
isDestinationQueued,
22+
isNative,
2223
isRedeemed,
2324
isSourceFinalized,
2425
isSourceInitiated,
@@ -98,13 +99,14 @@ export class NttAutomaticRoute<N extends Network>
9899
}
99100

100101
async isAvailable(request: routes.RouteTransferRequest<N>): Promise<boolean> {
101-
const nttContracts = NttRoute.resolveNttContracts(
102+
const { srcContracts } = NttRoute.resolveNttContracts(
102103
this.staticConfig,
103-
request.source.id
104+
request.source.id,
105+
request.destination.id
104106
);
105107

106108
const ntt = await request.fromChain.getProtocol("Ntt", {
107-
ntt: nttContracts,
109+
ntt: srcContracts,
108110
});
109111

110112
return ntt.isRelayingAvailable(request.toChain.chain);
@@ -121,29 +123,32 @@ export class NttAutomaticRoute<N extends Network>
121123
request.toChain.config.nativeTokenDecimals
122124
);
123125

126+
const wrapNative = isNative(request.source.id.address);
127+
124128
const parsedAmount = amount.parse(params.amount, request.source.decimals);
125129
// The trimmedAmount may differ from the parsedAmount if the parsedAmount includes dust
126130
const trimmedAmount = NttRoute.trimAmount(
127131
parsedAmount,
128132
request.destination.decimals
129133
);
130134

135+
const { srcContracts, dstContracts } = NttRoute.resolveNttContracts(
136+
this.staticConfig,
137+
request.source.id,
138+
request.destination.id
139+
);
140+
131141
const validatedParams: Vp = {
132142
amount: params.amount,
133143
normalizedParams: {
134144
amount: trimmedAmount,
135-
sourceContracts: NttRoute.resolveNttContracts(
136-
this.staticConfig,
137-
request.source.id
138-
),
139-
destinationContracts: NttRoute.resolveNttContracts(
140-
this.staticConfig,
141-
request.destination.id
142-
),
145+
sourceContracts: srcContracts,
146+
destinationContracts: dstContracts,
143147
options: {
144148
queue: false,
145149
automatic: true,
146150
gasDropoff: amount.units(gasDropoff),
151+
wrapNative,
147152
},
148153
},
149154
options,

sdk/route/src/manual.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
routes,
2424
signSendWait,
2525
finality,
26+
isNative,
2627
} from "@wormhole-foundation/sdk-connect";
2728
import "@wormhole-foundation/sdk-definitions-ntt";
2829
import { NttRoute } from "./types.js";
@@ -116,22 +117,25 @@ export class NttManualRoute<N extends Network>
116117
)
117118
);
118119

120+
const wrapNative = isNative(request.source.id.address);
121+
122+
const { srcContracts, dstContracts } = NttRoute.resolveNttContracts(
123+
this.staticConfig,
124+
request.source.id,
125+
request.destination.id
126+
);
127+
119128
const validatedParams: Vp = {
120129
amount: params.amount,
121130
normalizedParams: {
122131
amount: trimmedAmount,
123-
sourceContracts: NttRoute.resolveNttContracts(
124-
this.staticConfig,
125-
request.source.id
126-
),
127-
destinationContracts: NttRoute.resolveNttContracts(
128-
this.staticConfig,
129-
request.destination.id
130-
),
132+
sourceContracts: srcContracts,
133+
destinationContracts: dstContracts,
131134
options: {
132135
queue: false,
133136
automatic: false,
134137
gasDropoff,
138+
wrapNative,
135139
},
136140
},
137141
options,

sdk/route/src/types.ts

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
TransferReceipt as _TransferReceipt,
1111
amount,
1212
canonicalAddress,
13+
isNative,
14+
nativeTokenId,
1315
routes,
1416
} from "@wormhole-foundation/sdk-connect";
1517
import { Ntt } from "@wormhole-foundation/sdk-definitions-ntt";
@@ -31,6 +33,7 @@ export namespace NttRoute {
3133
manager: string;
3234
transceiver: TransceiverConfig[];
3335
quoter?: string;
36+
isWrappedGasToken?: boolean;
3437
};
3538

3639
export type Config = {
@@ -107,15 +110,22 @@ export namespace NttRoute {
107110
config: Config,
108111
fromChain: ChainContext<Network>
109112
): TokenId[] {
110-
const srcTokens = Object.entries(config.tokens)
111-
.map(([, configs]) => {
113+
const srcTokens = Object.entries(config.tokens).reduce<TokenId[]>(
114+
(acc, [, configs]) => {
112115
const tokenConf = configs.find(
113116
(config) => config.chain === fromChain.chain
114117
);
115-
if (!tokenConf) return null;
116-
return Wormhole.tokenId(fromChain.chain, tokenConf!.token);
117-
})
118-
.filter((x) => !!x) as TokenId[];
118+
if (tokenConf) {
119+
acc.push(Wormhole.tokenId(fromChain.chain, tokenConf.token));
120+
121+
if (tokenConf.isWrappedGasToken) {
122+
acc.push(nativeTokenId(fromChain.chain));
123+
}
124+
}
125+
return acc;
126+
},
127+
[]
128+
);
119129

120130
// TODO: dedupe? //return routes.uniqueTokens(srcTokens);
121131
return srcTokens;
@@ -132,8 +142,9 @@ export namespace NttRoute {
132142
const match = configs.find(
133143
(config) =>
134144
config.chain === fromChain.chain &&
135-
config.token.toLowerCase() ===
136-
canonicalAddress(sourceToken).toLowerCase()
145+
(config.token.toLowerCase() ===
146+
canonicalAddress(sourceToken).toLowerCase() ||
147+
(isNative(sourceToken.address) && config.isWrappedGasToken))
137148
);
138149
if (!match) return;
139150

@@ -147,28 +158,56 @@ export namespace NttRoute {
147158

148159
export function resolveNttContracts(
149160
config: Config,
150-
token: TokenId
151-
): Ntt.Contracts {
161+
srcToken: TokenId,
162+
dstToken: TokenId
163+
): { srcContracts: Ntt.Contracts; dstContracts: Ntt.Contracts } {
152164
const cfg = Object.values(config.tokens);
153-
const address = canonicalAddress(token);
165+
const srcAddress = canonicalAddress(srcToken);
166+
const dstAddress = canonicalAddress(dstToken);
167+
154168
for (const tokens of cfg) {
155-
const found = tokens.find(
169+
const srcFound = tokens.find(
156170
(tc) =>
157-
tc.token.toLowerCase() === address.toLowerCase() &&
158-
tc.chain === token.chain
171+
tc.chain === srcToken.chain &&
172+
(tc.token.toLowerCase() === srcAddress.toLowerCase() ||
173+
(isNative(srcToken.address) && tc.isWrappedGasToken))
159174
);
160-
if (found)
161-
return {
162-
token: found.token,
163-
manager: found.manager,
164-
transceiver: {
165-
wormhole: found.transceiver.find((v) => v.type === "wormhole")!
166-
.address,
167-
},
168-
quoter: found.quoter,
169-
};
175+
176+
if (srcFound) {
177+
const dstFound = tokens.find(
178+
(tc) =>
179+
tc.chain === dstToken.chain &&
180+
(tc.token.toLowerCase() === dstAddress.toLowerCase() ||
181+
(isNative(dstToken.address) && tc.isWrappedGasToken))
182+
);
183+
184+
if (dstFound) {
185+
return {
186+
srcContracts: {
187+
token: srcFound.token,
188+
manager: srcFound.manager,
189+
transceiver: {
190+
wormhole: srcFound.transceiver.find(
191+
(v) => v.type === "wormhole"
192+
)!.address,
193+
},
194+
quoter: srcFound.quoter,
195+
},
196+
dstContracts: {
197+
token: dstFound.token,
198+
manager: dstFound.manager,
199+
transceiver: {
200+
wormhole: dstFound.transceiver.find(
201+
(v) => v.type === "wormhole"
202+
)!.address,
203+
},
204+
},
205+
};
206+
}
207+
}
170208
}
171-
throw new Error("Cannot find Ntt contracts in config for: " + address);
209+
210+
throw new Error("Cannot find Ntt contracts in config for: " + srcAddress);
172211
}
173212

174213
export function resolveDestinationNttContracts<C extends Chain>(

0 commit comments

Comments
 (0)