@@ -96,4 +96,342 @@ contract ExecutorQuoterTest is Test {
9696 RelayInstructions.encodeGas (250000 , 0 )
9797 );
9898 }
99+
100+ // Error path tests
101+
102+ function test_chainInfoUpdate_invalidUpdater () public {
103+ address notUpdater = address (0xdead );
104+ vm.prank (notUpdater);
105+ vm.expectRevert (abi.encodeWithSelector (ExecutorQuoter.InvalidUpdater.selector , notUpdater, UPDATER));
106+ executorQuoter.chainInfoUpdate (chainInfoUpdates);
107+ }
108+
109+ function test_quoteUpdate_invalidUpdater () public {
110+ address notUpdater = address (0xdead );
111+ vm.prank (notUpdater);
112+ vm.expectRevert (abi.encodeWithSelector (ExecutorQuoter.InvalidUpdater.selector , notUpdater, UPDATER));
113+ executorQuoter.quoteUpdate (updates);
114+ }
115+
116+ function test_requestQuote_chainDisabled () public {
117+ uint16 disabledChain = 65000 ;
118+ vm.expectRevert (abi.encodeWithSelector (ExecutorQuoter.ChainDisabled.selector , disabledChain));
119+ executorQuoter.requestQuote (
120+ disabledChain,
121+ DST_ADDR,
122+ UPDATER,
123+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
124+ RelayInstructions.encodeGas (250000 , 0 )
125+ );
126+ }
127+
128+ function test_requestQuote_unsupportedInstruction () public {
129+ // Type 0xFF is not a valid instruction type
130+ bytes memory badInstruction = hex "ff " ;
131+ vm.expectRevert (abi.encodeWithSelector (ExecutorQuoter.UnsupportedInstruction.selector , 0xff ));
132+ executorQuoter.requestQuote (
133+ DST_CHAIN,
134+ DST_ADDR,
135+ UPDATER,
136+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
137+ badInstruction
138+ );
139+ }
140+
141+ function test_requestQuote_moreThanOneDropOff () public {
142+ // Two Type 2 (drop-off) instructions
143+ bytes memory twoDropOffs = abi.encodePacked (
144+ RelayInstructions.encodeGasDropOffInstructions (1000 , bytes32 (uint256 (uint160 (address (this ))))),
145+ RelayInstructions.encodeGasDropOffInstructions (2000 , bytes32 (uint256 (uint160 (address (this )))))
146+ );
147+ vm.expectRevert (abi.encodeWithSelector (ExecutorQuoter.MoreThanOneDropOff.selector ));
148+ executorQuoter.requestQuote (
149+ DST_CHAIN,
150+ DST_ADDR,
151+ UPDATER,
152+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
153+ twoDropOffs
154+ );
155+ }
156+
157+ // Edge case tests for large values
158+ //
159+ // Note: The quote calculation uses uint128 * uint64 which fits in uint256 (2^192 < 2^256),
160+ // so overflow is not possible with the current type constraints. These tests verify
161+ // the contract handles extreme values correctly without reverting.
162+ //
163+ // Run with `forge test -vv` to see the actual quote values logged.
164+
165+ /// @notice Test that max uint128 gas limit is handled without overflow.
166+ /// uint128 * uint64 = 2^192 max, which fits in uint256.
167+ function test_requestQuote_maxGasLimit () public {
168+ uint128 maxGas = type (uint128 ).max;
169+ bytes memory relayInstructions = RelayInstructions.encodeGas (maxGas, 0 );
170+
171+ uint256 quote = executorQuoter.requestQuote (
172+ DST_CHAIN,
173+ DST_ADDR,
174+ UPDATER,
175+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
176+ relayInstructions
177+ );
178+
179+ // Log the actual values for inspection
180+ emit log_named_uint ("maxGasLimit input " , maxGas);
181+ emit log_named_uint ("quote result " , quote);
182+ emit log_named_uint ("quote in ETH (approx) " , quote / 1e18 );
183+
184+ assertGt (quote, maxGas, "Quote should be non-zero " );
185+ }
186+
187+ /// @notice Test that max uint128 msgValue is handled without overflow.
188+ function test_requestQuote_maxMsgValue () public {
189+ uint128 maxMsgValue = type (uint128 ).max;
190+ bytes memory relayInstructions = RelayInstructions.encodeGas (250000 , maxMsgValue);
191+
192+ uint256 quote = executorQuoter.requestQuote (
193+ DST_CHAIN,
194+ DST_ADDR,
195+ UPDATER,
196+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
197+ relayInstructions
198+ );
199+
200+ assertGt (quote, maxMsgValue, "Quote should be greater than u128.max " );
201+ }
202+
203+ /// @notice Test with extreme price values - all max uint64.
204+ /// When srcPrice == dstPrice, conversion ratio is ~1, so no overflow.
205+ function test_requestQuote_extremePrices () public {
206+ ExecutorQuoter.Update[] memory extremeUpdates = new ExecutorQuoter.Update [](1 );
207+ extremeUpdates[0 ].chainId = DST_CHAIN;
208+ extremeUpdates[0 ].update = packUint64 (
209+ type (uint64 ).max, // baseFee
210+ type (uint64 ).max, // dstGasPrice
211+ type (uint64 ).max, // srcPrice
212+ type (uint64 ).max // dstPrice
213+ );
214+ executorQuoter.quoteUpdate (extremeUpdates);
215+
216+ bytes memory relayInstructions = RelayInstructions.encodeGas (250000 , 0 );
217+
218+ uint256 quote = executorQuoter.requestQuote (
219+ DST_CHAIN,
220+ DST_ADDR,
221+ UPDATER,
222+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
223+ relayInstructions
224+ );
225+
226+ emit log_named_uint ("baseFee " , type (uint64 ).max);
227+ emit log_named_uint ("dstGasPrice " , type (uint64 ).max);
228+ emit log_named_uint ("srcPrice " , type (uint64 ).max);
229+ emit log_named_uint ("dstPrice " , type (uint64 ).max);
230+ emit log_named_uint ("gasLimit " , 250000 );
231+ emit log_named_uint ("quote result " , quote);
232+ emit log_named_uint ("quote in ETH (approx) " , quote / 1e18 );
233+
234+ assertGt (quote, type (uint64 ).max, "Quote should be greater than a max price " );
235+ }
236+
237+ /// @notice Test quote with max msgValue AND max dropoff to verify they sum correctly.
238+ function test_requestQuote_maxMsgValueAndDropoff () public {
239+ uint128 maxGas = type (uint128 ).max;
240+ uint128 maxMsgValue = type (uint128 ).max;
241+ uint128 maxDropoff = type (uint128 ).max;
242+
243+ // Combine gas instruction (with max gas and max msgValue) + dropoff instruction
244+ bytes memory relayInstructions = abi.encodePacked (
245+ RelayInstructions.encodeGas (maxGas, maxMsgValue),
246+ RelayInstructions.encodeGasDropOffInstructions (maxDropoff, bytes32 (uint256 (uint160 (address (this )))))
247+ );
248+
249+ uint256 quote = executorQuoter.requestQuote (
250+ DST_CHAIN,
251+ DST_ADDR,
252+ UPDATER,
253+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
254+ relayInstructions
255+ );
256+
257+ emit log_named_uint ("maxGas input " , maxGas);
258+ emit log_named_uint ("maxMsgValue input " , maxMsgValue);
259+ emit log_named_uint ("maxDropoff input " , maxDropoff);
260+ emit log_named_uint ("quote result " , quote);
261+ emit log_named_uint ("quote in ETH (approx) " , quote / 1e18 );
262+ emit log_named_uint ("type(uint256).max " , type (uint256 ).max);
263+ emit log_named_uint ("type(uint128).max * 3 " , uint256 (type (uint128 ).max) * 3 );
264+
265+ // Verify quote is not capped at uint256 max (i.e., it's a real sum)
266+ assertGt (quote, uint256 (type (uint128 ).max), "Quote should be greater than a single max uint128 " );
267+ }
268+
269+ /// @notice Compare individual quotes vs combined to verify addition.
270+ function test_requestQuote_verifyAddition () public {
271+ uint128 gasLimit = 250000 ;
272+ uint128 msgValue = 1e18 ; // 1 ETH worth
273+ uint128 dropoff = 2e18 ; // 2 ETH worth
274+
275+ // Get quote with just gas
276+ bytes memory gasOnly = RelayInstructions.encodeGas (gasLimit, 0 );
277+ uint256 quoteGasOnly = executorQuoter.requestQuote (
278+ DST_CHAIN,
279+ DST_ADDR,
280+ UPDATER,
281+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
282+ gasOnly
283+ );
284+
285+ // Get quote with gas + msgValue
286+ bytes memory gasAndMsg = RelayInstructions.encodeGas (gasLimit, msgValue);
287+ uint256 quoteGasAndMsg = executorQuoter.requestQuote (
288+ DST_CHAIN,
289+ DST_ADDR,
290+ UPDATER,
291+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
292+ gasAndMsg
293+ );
294+
295+ // Get quote with gas + msgValue + dropoff
296+ bytes memory all = abi.encodePacked (
297+ RelayInstructions.encodeGas (gasLimit, msgValue),
298+ RelayInstructions.encodeGasDropOffInstructions (dropoff, bytes32 (uint256 (uint160 (address (this )))))
299+ );
300+ uint256 quoteAll = executorQuoter.requestQuote (
301+ DST_CHAIN,
302+ DST_ADDR,
303+ UPDATER,
304+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
305+ all
306+ );
307+
308+ // Verify monotonic increase
309+ assertGt (quoteGasAndMsg, quoteGasOnly, "Adding msgValue should increase quote " );
310+ assertGt (quoteAll, quoteGasAndMsg, "Adding dropoff should increase quote " );
311+ }
312+
313+ /// @notice Test quote calculation with zero prices (division by zero protection).
314+ function test_requestQuote_zeroPrices () public {
315+ // Set up a quote with zero srcPrice (would cause division by zero)
316+ ExecutorQuoter.Update[] memory zeroUpdates = new ExecutorQuoter.Update [](1 );
317+ zeroUpdates[0 ].chainId = DST_CHAIN;
318+ zeroUpdates[0 ].update = packUint64 (
319+ 27971 , // baseFee
320+ 100000000 , // dstGasPrice
321+ 0 , // srcPrice = 0 (division by zero)
322+ 35751300000000 // dstPrice
323+ );
324+ executorQuoter.quoteUpdate (zeroUpdates);
325+
326+ bytes memory relayInstructions = RelayInstructions.encodeGas (250000 , 0 );
327+
328+ // Should revert on division by zero
329+ vm.expectRevert ();
330+ executorQuoter.requestQuote (
331+ DST_CHAIN,
332+ DST_ADDR,
333+ UPDATER,
334+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
335+ relayInstructions
336+ );
337+ }
338+
339+ /// @notice Test with zero gas limit - should return just base fee.
340+ function test_requestQuote_zeroGasLimit () public view {
341+ bytes memory relayInstructions = RelayInstructions.encodeGas (0 , 0 );
342+
343+ uint256 quote = executorQuoter.requestQuote (
344+ DST_CHAIN,
345+ DST_ADDR,
346+ UPDATER,
347+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
348+ relayInstructions
349+ );
350+
351+ // With zero gas, quote should be just the normalized base fee
352+ // baseFee = 27971 (in 10^10 decimals), normalized to 18 decimals
353+ // 27971 * 10^(18-10) = 27971 * 10^8 = 2797100000000
354+ require (quote >= 2797100000000 , "Quote should include base fee " );
355+ }
356+
357+ /// @notice Test normalize function with from > to (division path).
358+ /// This exercises the branch at line 107 where decimals are reduced.
359+ function test_requestQuote_normalizeFromGreaterThanTo () public {
360+ // Create a new quoter with SRC_TOKEN_DECIMALS = 8 (less than DECIMAL_RESOLUTION = 18)
361+ // This will hit the from > to branch in normalize() at lines 183 and 187
362+ ExecutorQuoter lowDecimalQuoter = new ExecutorQuoter (UPDATER, UPDATER, 8 , bytes32 (uint256 (uint160 (UPDATER))));
363+
364+ // Set up chain info
365+ ExecutorQuoter.Update[] memory chainInfo = new ExecutorQuoter.Update [](1 );
366+ chainInfo[0 ].chainId = DST_CHAIN;
367+ chainInfo[0 ].update = CHAIN_INFO_UPDATE_PACKED;
368+ lowDecimalQuoter.chainInfoUpdate (chainInfo);
369+
370+ // Set up quote with non-zero values
371+ ExecutorQuoter.Update[] memory quoteUpdates = new ExecutorQuoter.Update [](1 );
372+ quoteUpdates[0 ].chainId = DST_CHAIN;
373+ quoteUpdates[0 ].update = packUint64 (27971 , 100000000 , 35751300000000 , 35751300000000 );
374+ lowDecimalQuoter.quoteUpdate (quoteUpdates);
375+
376+ bytes memory relayInstructions = RelayInstructions.encodeGas (250000 , 1e18 );
377+
378+ uint256 quote = lowDecimalQuoter.requestQuote (
379+ DST_CHAIN,
380+ DST_ADDR,
381+ UPDATER,
382+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
383+ relayInstructions
384+ );
385+
386+ // Quote should be non-zero and scaled down due to 8 decimals
387+ assertGt (quote, 0 , "Quote should be non-zero " );
388+ }
389+
390+ /// @notice Test normalize function with from == to (identity path).
391+ /// This exercises the branch at line 111 where no scaling is needed.
392+ function test_requestQuote_normalizeFromEqualsTo () public {
393+ // Create a quoter with SRC_TOKEN_DECIMALS = 10 (equals QUOTE_DECIMALS)
394+ // This will hit the from == to branch at line 174: normalize(baseFee, 10, 10)
395+ ExecutorQuoter equalDecimalQuoter = new ExecutorQuoter (UPDATER, UPDATER, 10 , bytes32 (uint256 (uint160 (UPDATER))));
396+
397+ // Set up chain info with gasPriceDecimals = 18, nativeDecimals = 18
398+ ExecutorQuoter.Update[] memory chainInfo = new ExecutorQuoter.Update [](1 );
399+ chainInfo[0 ].chainId = DST_CHAIN;
400+ chainInfo[0 ].update = CHAIN_INFO_UPDATE_PACKED;
401+ equalDecimalQuoter.chainInfoUpdate (chainInfo);
402+
403+ // Set up quote
404+ ExecutorQuoter.Update[] memory quoteUpdates = new ExecutorQuoter.Update [](1 );
405+ quoteUpdates[0 ].chainId = DST_CHAIN;
406+ quoteUpdates[0 ].update = packUint64 (27971 , 100000000 , 35751300000000 , 35751300000000 );
407+ equalDecimalQuoter.quoteUpdate (quoteUpdates);
408+
409+ // Use zero gas and zero msgValue to isolate the baseFee normalization
410+ bytes memory relayInstructions = RelayInstructions.encodeGas (0 , 0 );
411+
412+ uint256 quote = equalDecimalQuoter.requestQuote (
413+ DST_CHAIN,
414+ DST_ADDR,
415+ UPDATER,
416+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
417+ relayInstructions
418+ );
419+
420+ // With SRC_TOKEN_DECIMALS = 10 = QUOTE_DECIMALS, baseFee should pass through unchanged
421+ // baseFee = 27971
422+ assertEq (quote, 27971 , "Quote should equal baseFee when decimals match " );
423+ }
424+
425+ /// @notice Test requestExecutionQuote reverts when chain is disabled.
426+ function test_requestExecutionQuote_chainDisabled () public {
427+ uint16 disabledChain = 65000 ;
428+ vm.expectRevert (abi.encodeWithSelector (ExecutorQuoter.ChainDisabled.selector , disabledChain));
429+ executorQuoter.requestExecutionQuote (
430+ disabledChain,
431+ DST_ADDR,
432+ UPDATER,
433+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
434+ RelayInstructions.encodeGas (250000 , 0 )
435+ );
436+ }
99437}
0 commit comments