Skip to content
This repository was archived by the owner on Dec 17, 2025. It is now read-only.

Commit d76496a

Browse files
authored
Merge pull request #21 from morpho-labs/test/bucket-bit-manipulation
test: formal verification of bit manipulation in bucket computation
2 parents d9a5003 + 20f447e commit d76496a

4 files changed

Lines changed: 83 additions & 28 deletions

File tree

.github/workflows/ci-foundry.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,9 @@ jobs:
3737

3838
- name: Run tests
3939
run: forge test
40+
41+
- name: Install symtest
42+
run: python3 -m pip install --upgrade symtest
43+
44+
- name: Run symtest
45+
run: symtest --function testProve --loop 4

src/LogarithmicBuckets.sol

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,17 @@ library LogarithmicBuckets {
3939

4040
if (value == 0) {
4141
if (_newValue == 0) revert ZeroValue();
42-
_insert(_buckets, _id, _computeBucket(_newValue), _head);
42+
_insert(_buckets, _id, computeBucket(_newValue), _head);
4343
return;
4444
}
4545

46-
uint256 currentBucket = _computeBucket(value);
46+
uint256 currentBucket = computeBucket(value);
4747
if (_newValue == 0) {
4848
_remove(_buckets, _id, currentBucket);
4949
return;
5050
}
5151

52-
uint256 newBucket = _computeBucket(_newValue);
52+
uint256 newBucket = computeBucket(_newValue);
5353
if (newBucket != currentBucket) {
5454
_remove(_buckets, _id, currentBucket);
5555
_insert(_buckets, _id, newBucket, _head);
@@ -71,7 +71,7 @@ library LogarithmicBuckets {
7171
view
7272
returns (BucketDLL.List storage)
7373
{
74-
uint256 bucket = _computeBucket(_value);
74+
uint256 bucket = computeBucket(_value);
7575
return _buckets.lists[bucket];
7676
}
7777

@@ -82,13 +82,13 @@ library LogarithmicBuckets {
8282
function getMatch(BucketList storage _buckets, uint256 _value) internal view returns (address) {
8383
uint256 bucketsMask = _buckets.bucketsMask;
8484
if (bucketsMask == 0) return address(0);
85-
uint256 lowerMask = _setLowerBits(_value);
85+
uint256 lowerMask = setLowerBits(_value);
8686

87-
uint256 next = _nextBucket(lowerMask, bucketsMask);
87+
uint256 next = nextBucket(lowerMask, bucketsMask);
8888

8989
if (next != 0) return _buckets.lists[next].getHead();
9090

91-
uint256 prev = _prevBucket(lowerMask, bucketsMask);
91+
uint256 prev = prevBucket(lowerMask, bucketsMask);
9292

9393
return _buckets.lists[prev].getHead();
9494
}
@@ -126,14 +126,14 @@ library LogarithmicBuckets {
126126
}
127127

128128
/// @notice Returns the bucket in which the given value would fall.
129-
function _computeBucket(uint256 _value) private pure returns (uint256) {
130-
uint256 lowerMask = _setLowerBits(_value);
129+
function computeBucket(uint256 _value) internal pure returns (uint256) {
130+
uint256 lowerMask = setLowerBits(_value);
131131
return lowerMask ^ (lowerMask >> 1);
132132
}
133133

134134
/// @notice Sets all the bits lower than (or equal to) the highest bit in the input.
135135
/// @dev This is the same as rounding the input the nearest upper value of the form `2 ** n - 1`.
136-
function _setLowerBits(uint256 x) private pure returns (uint256 y) {
136+
function setLowerBits(uint256 x) internal pure returns (uint256 y) {
137137
assembly {
138138
x := or(x, shr(1, x))
139139
x := or(x, shr(2, x))
@@ -148,8 +148,8 @@ library LogarithmicBuckets {
148148

149149
/// @notice Returns the following bucket which contains greater values.
150150
/// @dev The bucket returned is the lowest that is in `bucketsMask` and not in `lowerMask`.
151-
function _nextBucket(uint256 lowerMask, uint256 bucketsMask)
152-
private
151+
function nextBucket(uint256 lowerMask, uint256 bucketsMask)
152+
internal
153153
pure
154154
returns (uint256 bucket)
155155
{
@@ -161,8 +161,8 @@ library LogarithmicBuckets {
161161

162162
/// @notice Returns the preceding bucket which contains smaller values.
163163
/// @dev The bucket returned is the highest that is in both `bucketsMask` and `lowerMask`.
164-
function _prevBucket(uint256 lowerMask, uint256 bucketsMask) private pure returns (uint256) {
165-
uint256 lowerBucketsMask = _setLowerBits(lowerMask & bucketsMask);
164+
function prevBucket(uint256 lowerMask, uint256 bucketsMask) internal pure returns (uint256) {
165+
uint256 lowerBucketsMask = setLowerBits(lowerMask & bucketsMask);
166166
return lowerBucketsMask ^ (lowerBucketsMask >> 1);
167167
}
168168
}

test/TestLogarithmicBuckets.t.sol

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,54 @@ contract TestLogarithmicBuckets is LogarithmicBucketsMock, Test {
115115
assertEq(bucketList.getMatch(16), accounts[0], "head equal");
116116
assertEq(bucketList.getMatch(32), accounts[0], "head above");
117117
}
118+
119+
function isPowerOfTwo(uint256 x) public pure returns (bool) {
120+
unchecked {
121+
return x != 0 && (x & (x - 1)) == 0;
122+
}
123+
}
124+
125+
function testProveComputeBucket(uint256 _value) public {
126+
uint256 bucket = LogarithmicBuckets.computeBucket(_value);
127+
unchecked {
128+
// cross-check that bucket == 2^{floor(log_2 value)}, or 0 if value == 0
129+
assertTrue(bucket == 0 || isPowerOfTwo(bucket));
130+
assertTrue(bucket <= _value);
131+
assertTrue(_value <= 2 * bucket - 1); // abusing overflow when bucket == 2**255
132+
}
133+
}
134+
135+
function testProveNextBucket(uint256 _value) public {
136+
uint256 curr = LogarithmicBuckets.computeBucket(_value);
137+
uint256 next = nextBucket(_value);
138+
uint256 bucketsMask = bucketList.bucketsMask;
139+
// check that `next` is a strictly higer non-empty bucket, or zero
140+
assertTrue(next == 0 || isPowerOfTwo(next));
141+
assertTrue(next == 0 || next > curr);
142+
assertTrue(next == 0 || bucketsMask & next != 0);
143+
unchecked {
144+
// check that `next` is the lowest one among such higher non-empty buckets, if exist
145+
// note: this also checks that all the higher buckets are empty when `next` == 0
146+
for (uint256 i = curr << 1; i != next; i <<= 1) {
147+
assertTrue(bucketsMask & i == 0);
148+
}
149+
}
150+
}
151+
152+
function testProvePrevBucket(uint256 _value) public {
153+
uint256 curr = LogarithmicBuckets.computeBucket(_value);
154+
uint256 prev = prevBucket(_value);
155+
uint256 bucketsMask = bucketList.bucketsMask;
156+
// check that `prev` is a non-empty bucket that is lower than or equal to `curr`; or zero
157+
assertTrue(prev == 0 || isPowerOfTwo(prev));
158+
assertTrue(prev <= curr);
159+
assertTrue(prev == 0 || bucketsMask & prev != 0);
160+
unchecked {
161+
// check that `prev` is the highest one among such lower non-empty buckets, if exist
162+
// note: this also checks that all the lower buckets are empty when `prev` == 0
163+
for (uint256 i = curr; i > prev; i >>= 1) {
164+
assertTrue(bucketsMask & i == 0);
165+
}
166+
}
167+
}
118168
}

test/mocks/LogarithmicBucketsMock.sol

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,8 @@ contract LogarithmicBucketsMock {
2121
return bucketList.getValueOf(_id);
2222
}
2323

24-
function _setLowerBits(uint256 x) private pure returns (uint256 y) {
25-
assembly {
26-
x := or(x, shr(1, x))
27-
x := or(x, shr(2, x))
28-
x := or(x, shr(4, x))
29-
x := or(x, shr(8, x))
30-
x := or(x, shr(16, x))
31-
x := or(x, shr(32, x))
32-
x := or(x, shr(64, x))
33-
y := or(x, shr(128, x))
34-
}
35-
}
36-
3724
function maxBucket() public view returns (uint256) {
38-
uint256 lowerMask = _setLowerBits(bucketList.bucketsMask);
25+
uint256 lowerMask = LogarithmicBuckets.setLowerBits(bucketList.bucketsMask);
3926
return lowerMask ^ (lowerMask >> 1);
4027
}
4128

@@ -60,4 +47,16 @@ contract LogarithmicBucketsMock {
6047
}
6148
return true;
6249
}
50+
51+
function nextBucket(uint256 _value) internal view returns (uint256) {
52+
uint256 bucketsMask = bucketList.bucketsMask;
53+
uint256 lowerMask = LogarithmicBuckets.setLowerBits(_value);
54+
return LogarithmicBuckets.nextBucket(lowerMask, bucketsMask);
55+
}
56+
57+
function prevBucket(uint256 _value) internal view returns (uint256) {
58+
uint256 bucketsMask = bucketList.bucketsMask;
59+
uint256 lowerMask = LogarithmicBuckets.setLowerBits(_value);
60+
return LogarithmicBuckets.prevBucket(lowerMask, bucketsMask);
61+
}
6362
}

0 commit comments

Comments
 (0)