Skip to content

Commit 0acead9

Browse files
committed
feat: add requireAligned() and requireMinStep() convenience guards
Closes #93. Adds BelowMinStep error for detecting floor-to-zero on small nonzero values, and two guard functions that let consumers revert with standardized library errors instead of custom ones.
1 parent 9413c86 commit 0acead9

File tree

2 files changed

+135
-6
lines changed

2 files changed

+135
-6
lines changed

src/UintQuantizationLib.sol

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ error NotAligned(uint256 value, uint256 stepSize);
4444
/// @notice Thrown by `create` when the (discardedBitWidth, encodedBitWidth) pair is invalid.
4545
error BadConfig(uint256 discardedBitWidth, uint256 encodedBitWidth);
4646

47+
/// @notice Thrown by `requireMinStep` when a non-zero value is smaller than the step size.
48+
error BelowMinStep(uint256 value, uint256 stepSize);
49+
4750
/// @notice Thrown by `ceil` when rounding up would overflow uint256.
4851
error CeilOverflow(uint256 value);
4952

@@ -195,6 +198,21 @@ library UintQuantizationLib {
195198
return encoded < (uint256(1) << encodedBitWidth(q));
196199
}
197200

201+
/// @notice Reverts with `NotAligned` if `value` is not a multiple of the step size.
202+
function requireAligned(Quant q, uint256 value) internal pure {
203+
uint256 step = stepSize(q);
204+
if (value & (step - 1) != 0) revert NotAligned(value, step);
205+
}
206+
207+
/// @notice Reverts with `BelowMinStep` if `value` is non-zero and smaller than the step size.
208+
/// Zero is allowed (represents empty/uninitialized state).
209+
function requireMinStep(Quant q, uint256 value) internal pure {
210+
if (value != 0) {
211+
uint256 step = stepSize(q);
212+
if (value < step) revert BelowMinStep(value, step);
213+
}
214+
}
215+
198216
/// @notice Rounds `value` down to the nearest step boundary (clears low `discardedBitWidth` bits).
199217
function floor(Quant q, uint256 value) internal pure returns (uint256) {
200218
return value & ~((uint256(1) << discardedBitWidth(q)) - 1);

test/UintQuantizationLib.t.sol

Lines changed: 117 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22
pragma solidity ^0.8.25;
33

44
import {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

Comments
 (0)