@@ -46,7 +46,7 @@ contract ExecutorQuoterTest is Test {
4646 update2.update = packUint64 (quote2.baseFee, quote2.dstGasPrice, quote2.srcPrice, quote2.dstPrice);
4747 ExecutorQuoter.OnChainQuoteBody memory quote3;
4848 quote3.baseFee = 27971 ;
49- quote3.dstGasPrice = 1000078 ;
49+ quote3.dstGasPrice = 100000000 ;
5050 quote3.srcPrice = 35751300000000 ;
5151 quote3.dstPrice = 35751300000000 ;
5252 ExecutorQuoter.Update memory update3;
@@ -96,4 +96,298 @@ 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+ assertEq (quote, 34028236692093846346337460743176823942600000000 , "Quote should match expected value " );
185+ }
186+
187+ /// @notice Test that max uint128 msgValue is handled without overflow.
188+ function test_requestQuote_maxMsgValue () public view {
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+ assertEq (quote, 340282366920938463463374635228868211455 , "Quote should match expected value " );
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+ assertEq (quote, 1849286093389382549403750000 , "Quote should match expected value for extreme prices " );
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+ assertEq (
266+ quote, 34028237372658580188214387669926038806136422910 , "Quote should match expected value for max values "
267+ );
268+ }
269+
270+ /// @notice Test quote calculation with zero prices (division by zero protection).
271+ function test_requestQuote_zeroPrices () public {
272+ // Set up a quote with zero srcPrice (would cause division by zero)
273+ ExecutorQuoter.Update[] memory zeroUpdates = new ExecutorQuoter.Update [](1 );
274+ zeroUpdates[0 ].chainId = DST_CHAIN;
275+ zeroUpdates[0 ].update = packUint64 (
276+ 27971 , // baseFee
277+ 100000000 , // dstGasPrice
278+ 0 , // srcPrice = 0 (division by zero)
279+ 35751300000000 // dstPrice
280+ );
281+ executorQuoter.quoteUpdate (zeroUpdates);
282+
283+ bytes memory relayInstructions = RelayInstructions.encodeGas (250000 , 0 );
284+
285+ // Should revert on division by zero
286+ vm.expectRevert ();
287+ executorQuoter.requestQuote (
288+ DST_CHAIN,
289+ DST_ADDR,
290+ UPDATER,
291+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
292+ relayInstructions
293+ );
294+ }
295+
296+ /// @notice Test with zero gas limit - should return just base fee.
297+ function test_requestQuote_zeroGasLimit () public view {
298+ bytes memory relayInstructions = RelayInstructions.encodeGas (0 , 0 );
299+
300+ uint256 quote = executorQuoter.requestQuote (
301+ DST_CHAIN,
302+ DST_ADDR,
303+ UPDATER,
304+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
305+ relayInstructions
306+ );
307+
308+ // With zero gas, quote should be just the normalized base fee
309+ // baseFee = 27971 (in 10^10 decimals), normalized to 18 decimals
310+ // 27971 * 10^(18-10) = 27971 * 10^8 = 2797100000000
311+ assertEq (quote, 2797100000000 , "Quote should equal normalized base fee " );
312+ }
313+
314+ /// @notice Test normalize function with from > to (division path).
315+ /// This exercises the branch at line 107 where decimals are reduced.
316+ function test_requestQuote_normalizeFromGreaterThanTo () public {
317+ // Create a new quoter with SRC_TOKEN_DECIMALS = 8 (less than DECIMAL_RESOLUTION = 18)
318+ // This will hit the from > to branch in normalize() at lines 183 and 187
319+ ExecutorQuoter lowDecimalQuoter = new ExecutorQuoter (UPDATER, UPDATER, 8 , bytes32 (uint256 (uint160 (UPDATER))));
320+
321+ // Set up chain info
322+ ExecutorQuoter.Update[] memory chainInfo = new ExecutorQuoter.Update [](1 );
323+ chainInfo[0 ].chainId = DST_CHAIN;
324+ chainInfo[0 ].update = CHAIN_INFO_UPDATE_PACKED;
325+ lowDecimalQuoter.chainInfoUpdate (chainInfo);
326+
327+ // Set up quote with non-zero values
328+ ExecutorQuoter.Update[] memory quoteUpdates = new ExecutorQuoter.Update [](1 );
329+ quoteUpdates[0 ].chainId = DST_CHAIN;
330+ quoteUpdates[0 ].update = packUint64 (27971 , 100000000 , 35751300000000 , 35751300000000 );
331+ lowDecimalQuoter.quoteUpdate (quoteUpdates);
332+
333+ bytes memory relayInstructions = RelayInstructions.encodeGas (250000 , 1e18 );
334+
335+ uint256 quote = lowDecimalQuoter.requestQuote (
336+ DST_CHAIN,
337+ DST_ADDR,
338+ UPDATER,
339+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
340+ relayInstructions
341+ );
342+
343+ assertEq (quote, 100002779 , "Quote should match expected value for 8 decimal token " );
344+ }
345+
346+ /// @notice Test normalize function with from == to (identity path).
347+ /// This exercises the branch at line 111 where no scaling is needed.
348+ function test_requestQuote_normalizeFromEqualsTo () public {
349+ // Create a quoter with SRC_TOKEN_DECIMALS = 10 (equals QUOTE_DECIMALS)
350+ // This will hit the from == to branch at line 174: normalize(baseFee, 10, 10)
351+ ExecutorQuoter equalDecimalQuoter = new ExecutorQuoter (UPDATER, UPDATER, 10 , bytes32 (uint256 (uint160 (UPDATER))));
352+
353+ // Set up chain info with gasPriceDecimals = 18, nativeDecimals = 18
354+ ExecutorQuoter.Update[] memory chainInfo = new ExecutorQuoter.Update [](1 );
355+ chainInfo[0 ].chainId = DST_CHAIN;
356+ chainInfo[0 ].update = CHAIN_INFO_UPDATE_PACKED;
357+ equalDecimalQuoter.chainInfoUpdate (chainInfo);
358+
359+ // Set up quote
360+ ExecutorQuoter.Update[] memory quoteUpdates = new ExecutorQuoter.Update [](1 );
361+ quoteUpdates[0 ].chainId = DST_CHAIN;
362+ quoteUpdates[0 ].update = packUint64 (27971 , 100000000 , 35751300000000 , 35751300000000 );
363+ equalDecimalQuoter.quoteUpdate (quoteUpdates);
364+
365+ // Use zero gas and zero msgValue to isolate the baseFee normalization
366+ bytes memory relayInstructions = RelayInstructions.encodeGas (0 , 0 );
367+
368+ uint256 quote = equalDecimalQuoter.requestQuote (
369+ DST_CHAIN,
370+ DST_ADDR,
371+ UPDATER,
372+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
373+ relayInstructions
374+ );
375+
376+ // With SRC_TOKEN_DECIMALS = 10 = QUOTE_DECIMALS, baseFee should pass through unchanged
377+ // baseFee = 27971
378+ assertEq (quote, 27971 , "Quote should equal baseFee when decimals match " );
379+ }
380+
381+ /// @notice Test requestExecutionQuote reverts when chain is disabled.
382+ function test_requestExecutionQuote_chainDisabled () public {
383+ uint16 disabledChain = 65000 ;
384+ vm.expectRevert (abi.encodeWithSelector (ExecutorQuoter.ChainDisabled.selector , disabledChain));
385+ executorQuoter.requestExecutionQuote (
386+ disabledChain,
387+ DST_ADDR,
388+ UPDATER,
389+ ExecutorMessages.makeVAAv1Request (10002 , bytes32 (uint256 (uint160 (address (this )))), 1 ),
390+ RelayInstructions.encodeGas (250000 , 0 )
391+ );
392+ }
99393}
0 commit comments