Skip to content

Commit e757df7

Browse files
authored
add nttManagerWithExecutorWithToken capability (#889)
* add nttManagerWithExecutorWithToken capability * fix case where ntt token and fee token are the same, leading to two token approvals for one token, overriding the first one * handle nonwrap vs wrap token approval cases * in the case where no feeToken is selected, default to native or first allowed token * add nttManagerWithExecutorWithToken mainnet address for tempo
1 parent 4a15527 commit e757df7

5 files changed

Lines changed: 281 additions & 61 deletions

File tree

evm/ts/src/executorGasLimits.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { Network } from "@wormhole-foundation/sdk-base";
2+
import type { EvmChains } from "@wormhole-foundation/sdk-evm";
3+
4+
// Must be high enough to cover the destination chain's worst case (otherwise
5+
// the executor's relay simulation reverts), but not so high that it inflates
6+
// the `estimatedCost` returned by /v0/quote.
7+
export const executorGasLimitOverrides: Partial<
8+
Record<Network, Partial<Record<EvmChains, bigint>>>
9+
> = {
10+
Mainnet: {
11+
Arbitrum: 800_000n,
12+
CreditCoin: 1_500_000n,
13+
Monad: 1_000_000n,
14+
MegaETH: 1_000_000n,
15+
Seievm: 1_000_000n,
16+
},
17+
Testnet: {
18+
ArbitrumSepolia: 800_000n,
19+
Seievm: 1_000_000n,
20+
Tempo: 1_500_000n,
21+
},
22+
};
23+
24+
export const DEFAULT_EXECUTOR_GAS_LIMIT = 500_000n;

evm/ts/src/nttWithExecutor.ts

Lines changed: 114 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ import {
1919
} from "@wormhole-foundation/sdk-evm";
2020
import { Provider, Interface } from "ethers";
2121
import { EvmNtt } from "./ntt.js";
22+
import {
23+
DEFAULT_EXECUTOR_GAS_LIMIT,
24+
executorGasLimitOverrides,
25+
} from "./executorGasLimits.js";
2226

2327
const nttManagerWithExecutorAddresses: Partial<
2428
Record<Network, Partial<Record<EvmChains, string>>>
@@ -69,22 +73,14 @@ const nttManagerWithExecutorAddresses: Partial<
6973
},
7074
};
7175

72-
// Gas limits must be high enough to cover the worst-case scenario for each chain
73-
// to avoid relay failures. However, they should not be too high to reduce the
74-
// `estimatedCost` returned by the quote endpoint.
75-
const gasLimitOverrides: Partial<
76-
Record<Network, Partial<Record<EvmChains, bigint>>>
76+
const nttManagerWithExecutorWithTokenAddresses: Partial<
77+
Record<Network, Partial<Record<EvmChains, string>>>
7778
> = {
7879
Mainnet: {
79-
Arbitrum: 800_000n,
80-
CreditCoin: 1_500_000n,
81-
Monad: 1_000_000n,
82-
MegaETH: 1_000_000n,
83-
Seievm: 1_000_000n,
80+
Tempo: "0x46d59af07A35751Deb45EC778150C7f0dFbb3d3a",
8481
},
8582
Testnet: {
86-
ArbitrumSepolia: 800_000n,
87-
Seievm: 1_000_000n,
83+
Tempo: "0x3A91179E506A15ff91467e42f5B4bD4239c6eC68",
8884
},
8985
};
9086

@@ -93,11 +89,17 @@ export const nttWithExecutorAbi = [
9389
"function transferETH(address nttManager, uint256 amount, uint16 recipientChain, bytes32 recipientAddress, bytes32 refundAddress, bytes encodedInstructions, (uint256 value, address refundAddress, bytes signedQuote, bytes instructions) executorArgs, (uint256 transferTokenFee, uint256 nativeTokenFee, address payee) feeArgs) external payable returns (uint64 msgId)",
9490
];
9591

92+
export const nttWithExecutorWithTokenAbi = [
93+
"function transfer(address nttManager, uint256 amount, uint16 recipientChain, bytes32 recipientAddress, bytes32 refundAddress, bytes encodedInstructions, (uint256 value, uint256 amount, address srcToken, address refundAddress, bytes signedQuote, bytes instructions) executorArgs, (uint256 transferTokenFee, uint256 nativeTokenFee, address payee) feeArgs) external payable returns (uint64 msgId)",
94+
"function transferETH(address nttManager, uint256 amount, uint16 recipientChain, bytes32 recipientAddress, bytes32 refundAddress, bytes encodedInstructions, (uint256 value, uint256 amount, address srcToken, address refundAddress, bytes signedQuote, bytes instructions) executorArgs, (uint256 transferTokenFee, uint256 nativeTokenFee, address payee) feeArgs) external payable returns (uint64 msgId)",
95+
];
96+
9697
export class EvmNttWithExecutor<N extends Network, C extends EvmChains>
9798
implements NttWithExecutor<N, C>
9899
{
99100
readonly chainId: bigint;
100-
readonly executorAddress: string;
101+
readonly executorAddress: string | undefined;
102+
readonly executorWithTokenAddress: string | undefined;
101103

102104
constructor(
103105
readonly network: N,
@@ -110,11 +112,10 @@ export class EvmNttWithExecutor<N extends Network, C extends EvmChains>
110112
chain
111113
) as bigint;
112114

113-
const executorAddress =
115+
this.executorAddress =
114116
nttManagerWithExecutorAddresses[this.network]?.[this.chain];
115-
if (!executorAddress)
116-
throw new Error(`Executor address not found for chain ${this.chain}`);
117-
this.executorAddress = executorAddress;
117+
this.executorWithTokenAddress =
118+
nttManagerWithExecutorWithTokenAddresses[this.network]?.[this.chain];
118119
}
119120

120121
static async fromRpc<N extends Network>(
@@ -134,6 +135,21 @@ export class EvmNttWithExecutor<N extends Network, C extends EvmChains>
134135
);
135136
}
136137

138+
private requireShimAddress(quote: NttWithExecutor.Quote): string {
139+
if (quote.feeToken) {
140+
if (!this.executorWithTokenAddress) {
141+
throw new Error(
142+
`NttManagerWithExecutorWithToken address not found for chain ${this.chain}`
143+
);
144+
}
145+
return this.executorWithTokenAddress;
146+
}
147+
if (!this.executorAddress) {
148+
throw new Error(`Executor address not found for chain ${this.chain}`);
149+
}
150+
return this.executorAddress;
151+
}
152+
137153
async *transfer(
138154
sender: AccountAddress<C>,
139155
destination: ChainAddress,
@@ -142,17 +158,35 @@ export class EvmNttWithExecutor<N extends Network, C extends EvmChains>
142158
ntt: EvmNtt<N, C>,
143159
wrapNative: boolean = false
144160
): AsyncGenerator<UnsignedTransaction<N, C>> {
161+
const shimAddress = this.requireShimAddress(quote);
145162
const senderAddress = new EvmAddress(sender).toString();
146163
const deliveryPrice = await ntt.quoteDeliveryPrice(destination.chain, {
147164
queue: false,
148165
automatic: false,
149166
});
150167

151-
// Fee is deducted from the transfer amount.
152-
// Approval covers remainingAmount + transferTokenFee = the full user amount.
153-
const totalAmount = quote.remainingAmount + quote.transferTokenFee;
168+
const isTokenFee = quote.feeToken !== undefined;
169+
const iface = new Interface(
170+
isTokenFee ? nttWithExecutorWithTokenAbi : nttWithExecutorAbi
171+
);
154172

155-
const iface = new Interface(nttWithExecutorAbi);
173+
// executorArgs.value = native forwarded to the Executor: estimatedCost on
174+
// the native-fee path, 0 on the token-fee path (the fee is pulled as ERC20).
175+
const executorArgs = isTokenFee
176+
? {
177+
value: 0n,
178+
amount: quote.estimatedCost,
179+
srcToken: quote.feeToken!,
180+
refundAddress: senderAddress,
181+
signedQuote: quote.signedQuote,
182+
instructions: quote.relayInstructions,
183+
}
184+
: {
185+
value: quote.estimatedCost,
186+
refundAddress: senderAddress,
187+
signedQuote: quote.signedQuote,
188+
instructions: quote.relayInstructions,
189+
};
156190

157191
const commonArgs = [
158192
ntt.managerAddress,
@@ -163,22 +197,32 @@ export class EvmNttWithExecutor<N extends Network, C extends EvmChains>
163197
Ntt.encodeTransceiverInstructions(
164198
ntt.encodeOptions({ queue: false, automatic: false })
165199
),
166-
{
167-
value: quote.estimatedCost,
168-
refundAddress: senderAddress,
169-
signedQuote: quote.signedQuote,
170-
instructions: quote.relayInstructions,
171-
},
200+
executorArgs,
172201
] as const;
173202

203+
// Merge into one approval when feeToken == bridged token (non-wrap only).
204+
const sameTokenAsFee =
205+
isTokenFee &&
206+
quote.feeToken!.toLowerCase() === ntt.tokenAddress.toLowerCase();
207+
const mergeFeeIntoBridgedApproval = sameTokenAsFee && !wrapNative;
208+
209+
if (isTokenFee && !mergeFeeIntoBridgedApproval) {
210+
yield* this.approveIfNeeded(
211+
senderAddress,
212+
shimAddress,
213+
quote.feeToken!,
214+
quote.estimatedCost,
215+
ntt
216+
);
217+
}
218+
174219
let data: string;
175220
let msgValue: bigint;
176221

177222
if (wrapNative) {
178-
// The source token is native gas, so the contract can't pull
179-
// transferTokenFee as ERC20 from msg.sender (no WETH approval exists).
180-
// Fold it into nativeTokenFee — denominated in the same units — so the
181-
// referrer is paid out of msg.value directly.
223+
// The source token is native gas, deposited into WETH by the shim, so
224+
// transferTokenFee can't be pulled as ERC20. Fold it into nativeTokenFee
225+
// (same units) — the referrer is paid out of msg.value directly.
182226
const combinedNativeFee = quote.nativeTokenFee + quote.transferTokenFee;
183227
const feeArgs = {
184228
transferTokenFee: 0n,
@@ -187,8 +231,8 @@ export class EvmNttWithExecutor<N extends Network, C extends EvmChains>
187231
};
188232
data = iface.encodeFunctionData("transferETH", [...commonArgs, feeArgs]);
189233
msgValue =
190-
quote.estimatedCost +
191234
deliveryPrice +
235+
executorArgs.value +
192236
combinedNativeFee +
193237
quote.remainingAmount;
194238
} else {
@@ -197,41 +241,55 @@ export class EvmNttWithExecutor<N extends Network, C extends EvmChains>
197241
nativeTokenFee: quote.nativeTokenFee,
198242
payee: quote.referrer.address.toString(),
199243
};
244+
const bridgedApprovalAmount =
245+
quote.remainingAmount +
246+
quote.transferTokenFee +
247+
(mergeFeeIntoBridgedApproval ? quote.estimatedCost : 0n);
200248
yield* this.approveIfNeeded(
201249
senderAddress,
202-
this.executorAddress,
203-
totalAmount,
250+
shimAddress,
251+
ntt.tokenAddress,
252+
bridgedApprovalAmount,
204253
ntt
205254
);
206255
data = iface.encodeFunctionData("transfer", [...commonArgs, feeArgs]);
207-
msgValue = quote.estimatedCost + deliveryPrice + quote.nativeTokenFee;
256+
msgValue = deliveryPrice + executorArgs.value + quote.nativeTokenFee;
208257
}
209258

210259
yield ntt.createUnsignedTx(
211-
{ to: this.executorAddress, data, value: msgValue },
212-
wrapNative ? "NttWithExecutor.transferETH" : "NttWithExecutor.transfer"
260+
{ to: shimAddress, data, value: msgValue },
261+
this.txDescription(isTokenFee, wrapNative)
213262
);
214263
}
215264

265+
private txDescription(isTokenFee: boolean, wrapNative: boolean): string {
266+
if (isTokenFee) {
267+
return wrapNative
268+
? "NttWithExecutorWithToken.transferETH"
269+
: "NttWithExecutorWithToken.transfer";
270+
}
271+
return wrapNative
272+
? "NttWithExecutor.transferETH"
273+
: "NttWithExecutor.transfer";
274+
}
275+
216276
private async *approveIfNeeded(
217277
senderAddress: string,
218-
contractAddress: string,
278+
spender: string,
279+
tokenAddress: string,
219280
requiredAmount: bigint,
220281
ntt: EvmNtt<N, C>
221282
): AsyncGenerator<UnsignedTransaction<N, C>> {
222283
const tokenContract = EvmPlatform.getTokenImplementation(
223284
this.provider,
224-
ntt.tokenAddress
285+
tokenAddress
225286
);
226287

227-
const allowance = await tokenContract.allowance(
228-
senderAddress,
229-
contractAddress
230-
);
288+
const allowance = await tokenContract.allowance(senderAddress, spender);
231289

232290
if (allowance < requiredAmount) {
233291
const txReq = await tokenContract.approve.populateTransaction(
234-
contractAddress,
292+
spender,
235293
requiredAmount
236294
);
237295
yield ntt.createUnsignedTx(txReq, "Ntt.Approve");
@@ -241,7 +299,9 @@ export class EvmNttWithExecutor<N extends Network, C extends EvmChains>
241299
async estimateMsgValueAndGasLimit(
242300
recipient: ChainAddress | undefined
243301
): Promise<{ msgValue: bigint; gasLimit: bigint }> {
244-
const gasLimit = gasLimitOverrides[this.network]?.[this.chain] ?? 500_000n;
302+
const gasLimit =
303+
executorGasLimitOverrides[this.network]?.[this.chain] ??
304+
DEFAULT_EXECUTOR_GAS_LIMIT;
245305
return { msgValue: 0n, gasLimit };
246306
}
247307
}
@@ -255,3 +315,12 @@ export function hasExecutorDeployed(
255315
): boolean {
256316
return nttManagerWithExecutorAddresses[network]?.[chain] !== undefined;
257317
}
318+
319+
export function hasExecutorWithTokenDeployed(
320+
network: Network,
321+
chain: EvmChains
322+
): boolean {
323+
return (
324+
nttManagerWithExecutorWithTokenAddresses[network]?.[chain] !== undefined
325+
);
326+
}

sdk/definitions/src/nttWithExecutor.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ export namespace NttWithExecutor {
1111
export type Quote = {
1212
signedQuote: Uint8Array; // The signed quote from the /v0/quote endpoint
1313
relayInstructions: Uint8Array; // The relay instructions for the transfer
14-
estimatedCost: bigint; // The estimated cost of the transfer in native token base units
14+
estimatedCost: bigint; // Estimated cost of the transfer. Denominated in source-chain native by default (EQ01), or in `feeToken` base units when `feeToken` is set (EQ03).
1515
payeeAddress: Uint8Array; // The wallet address on the source chain, designated by the Quoter, to receive funds when requesting an execution
1616
referrer: ChainAddress; // The referrer address (to whom the fees should be paid)
1717
transferTokenFee: bigint; // Fee in transfer token base units, deducted from the transfer amount
1818
nativeTokenFee: bigint; // Fee in native token base units
1919
remainingAmount: bigint; // The amount after fee deduction (what gets bridged)
2020
expires: Date; // The expiry time of the quote
2121
gasDropOff: bigint; // The gas drop-off amount in native token base units
22+
feeToken?: string; // When set, `estimatedCost` is in this ERC20's base units (EQ03)
23+
feeTokenDecimals?: number; // Decimals for `feeToken`
2224
};
2325
}
2426

0 commit comments

Comments
 (0)