Skip to content

Commit 4d9d9a2

Browse files
authored
Merge pull request #39 from morpho-labs/staging-spearbit
2 parents 06a9fb3 + 8f605e7 commit 4d9d9a2

7 files changed

+177
-149
lines changed

src/BucketDLL.sol

Lines changed: 40 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
// SPDX-License-Identifier: AGPL-3.0-only
22
pragma solidity ^0.8.0;
33

4+
/// @title BucketDLL
5+
/// @author Morpho Labs
6+
/// @custom:contact security@morpho.xyz
7+
/// @notice The doubly linked list used in logarithmic buckets.
48
library BucketDLL {
5-
/// STRUCTS ///
9+
/* STRUCTS */
610

711
struct Account {
812
address prev;
@@ -13,78 +17,59 @@ library BucketDLL {
1317
mapping(address => Account) accounts;
1418
}
1519

16-
/// INTERNAL ///
20+
/* INTERNAL */
1721

18-
/// @notice Returns the address at the head of the `_list`.
19-
/// @param _list The list from which to get the head.
20-
/// @return The address of the head.
21-
function getHead(List storage _list) internal view returns (address) {
22-
return _list.accounts[address(0)].next;
23-
}
24-
25-
/// @notice Returns the address at the tail of the `_list`.
26-
/// @param _list The list from which to get the tail.
27-
/// @return The address of the tail.
28-
function getTail(List storage _list) internal view returns (address) {
29-
return _list.accounts[address(0)].prev;
30-
}
31-
32-
/// @notice Returns the next id address from the current `_id`.
33-
/// @param _list The list to search in.
34-
/// @param _id The address of the current account.
22+
/// @notice Returns the next id address from the current `id`.
23+
/// @dev Pass the address 0 to get the head of the list.
24+
/// @param list The list to search in.
25+
/// @param id The address of the current account.
3526
/// @return The address of the next account.
36-
function getNext(List storage _list, address _id) internal view returns (address) {
37-
return _list.accounts[_id].next;
38-
}
39-
40-
/// @notice Returns the previous id address from the current `_id`.
41-
/// @param _list The list to search in.
42-
/// @param _id The address of the current account.
43-
/// @return The address of the previous account.
44-
function getPrev(List storage _list, address _id) internal view returns (address) {
45-
return _list.accounts[_id].prev;
27+
function getNext(List storage list, address id) internal view returns (address) {
28+
return list.accounts[id].next;
4629
}
4730

48-
/// @notice Removes an account of the `_list`.
49-
/// @dev This function should not be called with `_id` equal to address 0.
50-
/// @param _list The list to search in.
51-
/// @param _id The address of the account.
31+
/// @notice Removes an account of the `list`.
32+
/// @dev This function should not be called with `id` equal to address 0.
33+
/// @dev This function should not be called with an `_id` that is not in the list.
34+
/// @param list The list to search in.
35+
/// @param id The address of the account.
5236
/// @return Whether the bucket is empty after removal.
53-
function remove(List storage _list, address _id) internal returns (bool) {
54-
Account memory account = _list.accounts[_id];
37+
function remove(List storage list, address id) internal returns (bool) {
38+
Account memory account = list.accounts[id];
5539
address prev = account.prev;
5640
address next = account.next;
5741

58-
_list.accounts[prev].next = next;
59-
_list.accounts[next].prev = prev;
42+
list.accounts[prev].next = next;
43+
list.accounts[next].prev = prev;
6044

61-
delete _list.accounts[_id];
45+
delete list.accounts[id];
6246

6347
return (prev == address(0) && next == address(0));
6448
}
6549

66-
/// @notice Inserts an account in the `_list`.
67-
/// @dev This function should not be called with `_id` equal to address 0.
68-
/// @param _list The list to search in.
69-
/// @param _id The address of the account.
70-
/// @param _head Tells whether to insert at the head or at the tail of the list.
50+
/// @notice Inserts an account in the `list`.
51+
/// @dev This function should not be called with `id` equal to address 0.
52+
/// @dev This function should not be called with an `id` that is already in the list.
53+
/// @param list The list to search in.
54+
/// @param id The address of the account.
55+
/// @param atHead Tells whether to insert at the head or at the tail of the list.
7156
/// @return Whether the bucket was empty before insertion.
7257
function insert(
73-
List storage _list,
74-
address _id,
75-
bool _head
58+
List storage list,
59+
address id,
60+
bool atHead
7661
) internal returns (bool) {
77-
if (_head) {
78-
address head = _list.accounts[address(0)].next;
79-
_list.accounts[address(0)].next = _id;
80-
_list.accounts[head].prev = _id;
81-
_list.accounts[_id].next = head;
62+
if (atHead) {
63+
address head = list.accounts[address(0)].next;
64+
list.accounts[address(0)].next = id;
65+
list.accounts[head].prev = id;
66+
list.accounts[id].next = head;
8267
return head == address(0);
8368
} else {
84-
address tail = _list.accounts[address(0)].prev;
85-
_list.accounts[address(0)].prev = _id;
86-
_list.accounts[tail].next = _id;
87-
_list.accounts[_id].prev = tail;
69+
address tail = list.accounts[address(0)].prev;
70+
list.accounts[address(0)].prev = id;
71+
list.accounts[tail].next = id;
72+
list.accounts[id].prev = tail;
8873
return tail == address(0);
8974
}
9075
}

src/LogarithmicBuckets.sol

Lines changed: 69 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ pragma solidity ^0.8.0;
33

44
import "./BucketDLL.sol";
55

6+
/// @title LogarithmicBuckets
7+
/// @author Morpho Labs
8+
/// @custom:contact security@morpho.xyz
9+
/// @notice The logarithmic buckets data-structure.
610
library LogarithmicBuckets {
711
using BucketDLL for BucketDLL.List;
812

@@ -12,104 +16,108 @@ library LogarithmicBuckets {
1216
uint256 bucketsMask;
1317
}
1418

15-
/// ERRORS ///
19+
/* ERRORS */
1620

1721
/// @notice Thrown when the address is zero at insertion.
1822
error ZeroAddress();
1923

2024
/// @notice Thrown when 0 value is inserted.
2125
error ZeroValue();
2226

23-
/// INTERNAL ///
27+
/* INTERNAL */
2428

25-
/// @notice Updates an account in the `_buckets`.
26-
/// @param _buckets The buckets to update.
27-
/// @param _id The address of the account.
28-
/// @param _newValue The new value of the account.
29-
/// @param _head Indicates whether to insert the new values at the head or at the tail of the buckets list.
29+
/// @notice Updates an account in the `buckets`.
30+
/// @param buckets The buckets to update.
31+
/// @param id The address of the account.
32+
/// @param newValue The new value of the account.
33+
/// @param head Indicates whether to insert the new values at the head or at the tail of the buckets list.
3034
function update(
31-
Buckets storage _buckets,
32-
address _id,
33-
uint256 _newValue,
34-
bool _head
35+
Buckets storage buckets,
36+
address id,
37+
uint256 newValue,
38+
bool head
3539
) internal {
36-
if (_id == address(0)) revert ZeroAddress();
37-
uint256 value = _buckets.valueOf[_id];
38-
_buckets.valueOf[_id] = _newValue;
40+
if (id == address(0)) revert ZeroAddress();
41+
uint256 value = buckets.valueOf[id];
42+
buckets.valueOf[id] = newValue;
3943

4044
if (value == 0) {
41-
if (_newValue == 0) revert ZeroValue();
42-
_insert(_buckets, _id, computeBucket(_newValue), _head);
45+
if (newValue == 0) revert ZeroValue();
46+
// `highestSetBit` is used to compute the bucket associated with `newValue`.
47+
_insert(buckets, id, highestSetBit(newValue), head);
4348
return;
4449
}
4550

46-
uint256 currentBucket = computeBucket(value);
47-
if (_newValue == 0) {
48-
_remove(_buckets, _id, currentBucket);
51+
// `highestSetBit` is used to compute the bucket associated with `value`.
52+
uint256 currentBucket = highestSetBit(value);
53+
if (newValue == 0) {
54+
_remove(buckets, id, currentBucket);
4955
return;
5056
}
5157

52-
uint256 newBucket = computeBucket(_newValue);
58+
// `highestSetBit` is used to compute the bucket associated with `newValue`.
59+
uint256 newBucket = highestSetBit(newValue);
5360
if (newBucket != currentBucket) {
54-
_remove(_buckets, _id, currentBucket);
55-
_insert(_buckets, _id, newBucket, _head);
61+
_remove(buckets, id, currentBucket);
62+
_insert(buckets, id, newBucket, head);
5663
}
5764
}
5865

59-
/// @notice Returns the address in `_buckets` that is a candidate for matching the value `_value`.
60-
/// @param _buckets The buckets to get the head.
61-
/// @param _value The value to match.
66+
/// @notice Returns the address in `buckets` that is a candidate for matching the value `value`.
67+
/// @param buckets The buckets to get the head.
68+
/// @param value The value to match.
6269
/// @return The address of the head.
63-
function getMatch(Buckets storage _buckets, uint256 _value) internal view returns (address) {
64-
uint256 bucketsMask = _buckets.bucketsMask;
70+
function getMatch(Buckets storage buckets, uint256 value) internal view returns (address) {
71+
uint256 bucketsMask = buckets.bucketsMask;
6572
if (bucketsMask == 0) return address(0);
66-
uint256 lowerMask = setLowerBits(_value);
6773

68-
uint256 next = nextBucket(lowerMask, bucketsMask);
74+
uint256 next = nextBucket(value, bucketsMask);
75+
if (next != 0) return buckets.buckets[next].getNext(address(0));
6976

70-
if (next != 0) return _buckets.buckets[next].getHead();
71-
72-
uint256 prev = prevBucket(lowerMask, bucketsMask);
73-
74-
return _buckets.buckets[prev].getHead();
77+
// `highestSetBit` is used to compute the highest non-empty bucket.
78+
// Knowing that `next` == 0, it is also the highest previous non-empty bucket.
79+
uint256 prev = highestSetBit(bucketsMask);
80+
return buckets.buckets[prev].getNext(address(0));
7581
}
7682

77-
/// PRIVATE ///
83+
/* PRIVATE */
7884

79-
/// @notice Removes an account in the `_buckets`.
85+
/// @notice Removes an account in the `buckets`.
8086
/// @dev Does not update the value.
81-
/// @param _buckets The buckets to modify.
82-
/// @param _id The address of the account to remove.
83-
/// @param _bucket The mask of the bucket where to remove.
87+
/// @param buckets The buckets to modify.
88+
/// @param id The address of the account to remove.
89+
/// @param bucket The mask of the bucket where to remove.
8490
function _remove(
85-
Buckets storage _buckets,
86-
address _id,
87-
uint256 _bucket
91+
Buckets storage buckets,
92+
address id,
93+
uint256 bucket
8894
) private {
89-
if (_buckets.buckets[_bucket].remove(_id)) _buckets.bucketsMask &= ~_bucket;
95+
if (buckets.buckets[bucket].remove(id)) buckets.bucketsMask &= ~bucket;
9096
}
9197

92-
/// @notice Inserts an account in the `_buckets`.
93-
/// @dev Expects that `_id` != 0.
98+
/// @notice Inserts an account in the `buckets`.
99+
/// @dev Expects that `id` != 0.
94100
/// @dev Does not update the value.
95-
/// @param _buckets The buckets to modify.
96-
/// @param _id The address of the account to update.
97-
/// @param _bucket The mask of the bucket where to insert.
98-
/// @param _head Whether to insert at the head or at the tail of the list.
101+
/// @param buckets The buckets to modify.
102+
/// @param id The address of the account to update.
103+
/// @param bucket The mask of the bucket where to insert.
104+
/// @param head Whether to insert at the head or at the tail of the list.
99105
function _insert(
100-
Buckets storage _buckets,
101-
address _id,
102-
uint256 _bucket,
103-
bool _head
106+
Buckets storage buckets,
107+
address id,
108+
uint256 bucket,
109+
bool head
104110
) private {
105-
if (_buckets.buckets[_bucket].insert(_id, _head)) _buckets.bucketsMask |= _bucket;
111+
if (buckets.buckets[bucket].insert(id, head)) buckets.bucketsMask |= bucket;
106112
}
107113

108-
/// PURE HELPERS ///
114+
/* PURE HELPERS */
109115

110-
/// @notice Returns the bucket in which the given value would fall.
111-
function computeBucket(uint256 _value) internal pure returns (uint256) {
112-
uint256 lowerMask = setLowerBits(_value);
116+
/// @notice Returns the highest set bit.
117+
/// @dev Used to compute the bucket associated to a given `value`.
118+
/// @dev Used to compute the highest non empty bucket given the `bucketsMask`.
119+
function highestSetBit(uint256 value) internal pure returns (uint256) {
120+
uint256 lowerMask = setLowerBits(value);
113121
return lowerMask ^ (lowerMask >> 1);
114122
}
115123

@@ -128,23 +136,13 @@ library LogarithmicBuckets {
128136
}
129137
}
130138

131-
/// @notice Returns the following bucket which contains greater values.
139+
/// @notice Returns the lowest non-empty bucket containing larger values.
132140
/// @dev The bucket returned is the lowest that is in `bucketsMask` and not in `lowerMask`.
133-
function nextBucket(uint256 lowerMask, uint256 bucketsMask)
134-
internal
135-
pure
136-
returns (uint256 bucket)
137-
{
141+
function nextBucket(uint256 value, uint256 bucketsMask) internal pure returns (uint256 bucket) {
142+
uint256 lowerMask = setLowerBits(value);
138143
assembly {
139144
let higherBucketsMask := and(not(lowerMask), bucketsMask)
140145
bucket := and(higherBucketsMask, add(not(higherBucketsMask), 1))
141146
}
142147
}
143-
144-
/// @notice Returns the preceding bucket which contains smaller values.
145-
/// @dev The bucket returned is the highest that is in both `bucketsMask` and `lowerMask`.
146-
function prevBucket(uint256 lowerMask, uint256 bucketsMask) internal pure returns (uint256) {
147-
uint256 lowerBucketsMask = setLowerBits(lowerMask & bucketsMask);
148-
return lowerBucketsMask ^ (lowerBucketsMask >> 1);
149-
}
150148
}

test/TestBucketDLL.t.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
pragma solidity ^0.8.0;
33

44
import "forge-std/Test.sol";
5-
import "src/BucketDLL.sol";
5+
import "./mocks/BucketDLLMock.sol";
66

77
contract TestBucketDLL is Test {
8-
using BucketDLL for BucketDLL.List;
8+
using BucketDLLMock for BucketDLL.List;
99

1010
uint256 internal numberOfAccounts = 50;
1111
address[] public accounts;

0 commit comments

Comments
 (0)