@@ -19,6 +19,10 @@ import {
1919} from "@wormhole-foundation/sdk-evm" ;
2020import { Provider , Interface } from "ethers" ;
2121import { EvmNtt } from "./ntt.js" ;
22+ import {
23+ DEFAULT_EXECUTOR_GAS_LIMIT ,
24+ executorGasLimitOverrides ,
25+ } from "./executorGasLimits.js" ;
2226
2327const 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+
9697export 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+ }
0 commit comments