Skip to content

Commit e056a12

Browse files
authored
evm: on-chain quote test coverage
* feat: testing coverage added * new tests for more coverage * tests assert equality, not greater than * forge fmt fixes * revert executor quoter router changes
1 parent e775abc commit e056a12

File tree

2 files changed

+648
-1
lines changed

2 files changed

+648
-1
lines changed

evm/test/ExecutorQuoter.t.sol

Lines changed: 295 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)