22pragma solidity ^ 0.8.25 ;
33
44import {Test} from "forge-std/Test.sol " ;
5- import {Quant, UintQuantizationLib, Overflow, NotAligned, BadConfig, CeilOverflow} from "src/UintQuantizationLib.sol " ;
5+ import {
6+ Quant,
7+ UintQuantizationLib,
8+ Overflow,
9+ NotAligned,
10+ BadConfig,
11+ CeilOverflow,
12+ BelowMinStep
13+ } from "src/UintQuantizationLib.sol " ;
614
715/// @notice Thin harness that exposes library functions via `using-for` so tests call them on
816/// `Quant` values rather than through the library name directly.
@@ -78,6 +86,14 @@ contract QuantHarness {
7886 function ceil (Quant q , uint256 value ) external pure returns (uint256 ) {
7987 return q.ceil (value);
8088 }
89+
90+ function requireAligned (Quant q , uint256 value ) external pure {
91+ q.requireAligned (value);
92+ }
93+
94+ function requireMinStep (Quant q , uint256 value ) external pure {
95+ q.requireMinStep (value);
96+ }
8197}
8298
8399/// @notice Fast concrete regression checks. Mathematical completeness is covered by fuzz tests.
@@ -291,6 +307,54 @@ contract UintQuantizationLibSmokeTest is Test {
291307 harness.ceil (q, type (uint256 ).max);
292308 }
293309
310+ // -------------------------------------------------------------------------
311+ // requireAligned: revert on non-aligned, pass on aligned
312+ // -------------------------------------------------------------------------
313+
314+ function test_requireAligned_notAligned_reverts () public {
315+ Quant q = harness.create (DISCARDED_8, ENCODED_8);
316+ uint256 step = harness.stepSize (q); // 256
317+ vm.expectRevert (abi.encodeWithSelector (NotAligned.selector , step + 1 , step));
318+ harness.requireAligned (q, step + 1 );
319+ }
320+
321+ function test_requireAligned_aligned_succeeds () public view {
322+ Quant q = harness.create (DISCARDED_8, ENCODED_8);
323+ harness.requireAligned (q, 512 ); // 2 * stepSize, aligned
324+ }
325+
326+ function test_requireAligned_zero_succeeds () public view {
327+ Quant q = harness.create (DISCARDED_8, ENCODED_8);
328+ harness.requireAligned (q, 0 );
329+ }
330+
331+ // -------------------------------------------------------------------------
332+ // requireMinStep: revert below step, pass on zero and >= step
333+ // -------------------------------------------------------------------------
334+
335+ function test_requireMinStep_belowStep_reverts () public {
336+ Quant q = harness.create (DISCARDED_8, ENCODED_8);
337+ uint256 step = harness.stepSize (q); // 256
338+ vm.expectRevert (abi.encodeWithSelector (BelowMinStep.selector , uint256 (1 ), step));
339+ harness.requireMinStep (q, 1 );
340+ }
341+
342+ function test_requireMinStep_zero_succeeds () public view {
343+ Quant q = harness.create (DISCARDED_8, ENCODED_8);
344+ harness.requireMinStep (q, 0 );
345+ }
346+
347+ function test_requireMinStep_exactStep_succeeds () public view {
348+ Quant q = harness.create (DISCARDED_8, ENCODED_8);
349+ harness.requireMinStep (q, 256 ); // exactly stepSize
350+ }
351+
352+ function test_requireMinStep_noShift_always_succeeds () public view {
353+ // discardedBitWidth=0: stepSize=1, so every non-zero value >= 1 passes
354+ Quant q = harness.create (0 , 8 );
355+ harness.requireMinStep (q, 1 );
356+ }
357+
294358 // -------------------------------------------------------------------------
295359 // Fuzz tests
296360 // -------------------------------------------------------------------------
@@ -302,7 +366,10 @@ contract UintQuantizationLibSmokeTest is Test {
302366 assertTrue (harness.isAligned (q, floored));
303367 }
304368
305- function testFuzz_lower_bound_round_trip (uint8 discardedBitWidth_ , uint8 encodedBitWidth_ , uint256 value ) public view {
369+ function testFuzz_lower_bound_round_trip (uint8 discardedBitWidth_ , uint8 encodedBitWidth_ , uint256 value )
370+ public
371+ view
372+ {
306373 vm.assume (encodedBitWidth_ > 0 && uint256 (discardedBitWidth_) + uint256 (encodedBitWidth_) <= 256 );
307374 Quant q = UintQuantizationLib.create (uint256 (discardedBitWidth_), uint256 (encodedBitWidth_));
308375 // Use bound instead of assume: schemes with small max reject most random uint256 values.
@@ -311,21 +378,30 @@ contract UintQuantizationLibSmokeTest is Test {
311378 assertLe (decoded, value);
312379 }
313380
314- function testFuzz_decodeMax_ge_decode (uint8 discardedBitWidth_ , uint8 encodedBitWidth_ , uint256 encoded ) public view {
381+ function testFuzz_decodeMax_ge_decode (uint8 discardedBitWidth_ , uint8 encodedBitWidth_ , uint256 encoded )
382+ public
383+ view
384+ {
315385 vm.assume (encodedBitWidth_ > 0 && uint256 (discardedBitWidth_) + uint256 (encodedBitWidth_) <= 256 );
316386 Quant q = UintQuantizationLib.create (uint256 (discardedBitWidth_), uint256 (encodedBitWidth_));
317387 // Bound to valid encoded range so the test exercises the documented domain.
318388 encoded = bound (encoded, 0 , (uint256 (1 ) << harness.encodedBitWidth (q)) - 1 );
319389 assertGe (harness.decodeMax (q, encoded), harness.decode (q, encoded));
320390 }
321391
322- function testFuzz_remainder_lt_stepSize (uint8 discardedBitWidth_ , uint8 encodedBitWidth_ , uint256 value ) public view {
392+ function testFuzz_remainder_lt_stepSize (uint8 discardedBitWidth_ , uint8 encodedBitWidth_ , uint256 value )
393+ public
394+ view
395+ {
323396 vm.assume (encodedBitWidth_ > 0 && uint256 (discardedBitWidth_) + uint256 (encodedBitWidth_) <= 256 );
324397 Quant q = UintQuantizationLib.create (uint256 (discardedBitWidth_), uint256 (encodedBitWidth_));
325398 assertLt (harness.remainder (q, value), harness.stepSize (q));
326399 }
327400
328- function testFuzz_isAligned_equivalence (uint8 discardedBitWidth_ , uint8 encodedBitWidth_ , uint256 value ) public view {
401+ function testFuzz_isAligned_equivalence (uint8 discardedBitWidth_ , uint8 encodedBitWidth_ , uint256 value )
402+ public
403+ view
404+ {
329405 vm.assume (encodedBitWidth_ > 0 && uint256 (discardedBitWidth_) + uint256 (encodedBitWidth_) <= 256 );
330406 Quant q = UintQuantizationLib.create (uint256 (discardedBitWidth_), uint256 (encodedBitWidth_));
331407 assertEq (harness.isAligned (q, value), harness.remainder (q, value) == 0 );
@@ -353,7 +429,10 @@ contract UintQuantizationLibSmokeTest is Test {
353429 assertGe (harness.ceil (q, value), value);
354430 }
355431
356- function testFuzz_encode_monotonicity (uint8 discardedBitWidth_ , uint8 encodedBitWidth_ , uint256 v1 , uint256 v2 ) public view {
432+ function testFuzz_encode_monotonicity (uint8 discardedBitWidth_ , uint8 encodedBitWidth_ , uint256 v1 , uint256 v2 )
433+ public
434+ view
435+ {
357436 vm.assume (encodedBitWidth_ > 0 && uint256 (discardedBitWidth_) + uint256 (encodedBitWidth_) <= 256 );
358437 Quant q = UintQuantizationLib.create (uint256 (discardedBitWidth_), uint256 (encodedBitWidth_));
359438 uint256 m = harness.max (q);
@@ -363,4 +442,36 @@ contract UintQuantizationLibSmokeTest is Test {
363442 if (v1 > v2) (v1, v2) = (v2, v1);
364443 assertLe (harness.encode (q, v1), harness.encode (q, v2));
365444 }
445+
446+ function testFuzz_requireAligned_consistent_with_isAligned (
447+ uint8 discardedBitWidth_ ,
448+ uint8 encodedBitWidth_ ,
449+ uint256 value
450+ ) public {
451+ vm.assume (encodedBitWidth_ > 0 && uint256 (discardedBitWidth_) + uint256 (encodedBitWidth_) <= 256 );
452+ Quant q = UintQuantizationLib.create (uint256 (discardedBitWidth_), uint256 (encodedBitWidth_));
453+ if (harness.isAligned (q, value)) {
454+ harness.requireAligned (q, value); // must not revert
455+ } else {
456+ vm.expectRevert (abi.encodeWithSelector (NotAligned.selector , value, harness.stepSize (q)));
457+ harness.requireAligned (q, value);
458+ }
459+ }
460+
461+ function testFuzz_requireMinStep_zero_always_passes (uint8 discardedBitWidth_ , uint8 encodedBitWidth_ ) public view {
462+ vm.assume (encodedBitWidth_ > 0 && uint256 (discardedBitWidth_) + uint256 (encodedBitWidth_) <= 256 );
463+ Quant q = UintQuantizationLib.create (uint256 (discardedBitWidth_), uint256 (encodedBitWidth_));
464+ harness.requireMinStep (q, 0 ); // must not revert
465+ }
466+
467+ function testFuzz_requireMinStep_ge_step_passes (uint8 discardedBitWidth_ , uint8 encodedBitWidth_ , uint256 value )
468+ public
469+ view
470+ {
471+ vm.assume (encodedBitWidth_ > 0 && uint256 (discardedBitWidth_) + uint256 (encodedBitWidth_) <= 256 );
472+ Quant q = UintQuantizationLib.create (uint256 (discardedBitWidth_), uint256 (encodedBitWidth_));
473+ uint256 step = harness.stepSize (q);
474+ value = bound (value, step, type (uint256 ).max);
475+ harness.requireMinStep (q, value); // must not revert
476+ }
366477}
0 commit comments