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

Commit d9a5003

Browse files
authored
Merge pull request #2 from morpho-labs/feat/log-buckets
Logarithmic buckets
2 parents 0a78bb4 + 83db25a commit d9a5003

19 files changed

Lines changed: 825 additions & 10 deletions

.gitmodules

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
33
url = https://github.com/dapphub/ds-test
44
[submodule "lib/forge-std"]
55
path = lib/forge-std
6-
url = https://github.com/foundry-rs/forge-std
6+
url = https://github.com/MathisGD/forge-std
7+
branch = feat/real-gas-consumption
78
[submodule "lib/openzeppelin-contracts"]
89
path = lib/openzeppelin-contracts
910
url = https://github.com/OpenZeppelin/openzeppelin-contracts
1011
branch = v4.8.0
12+
[submodule "lib/morpho-utils"]
13+
path = lib/morpho-utils
14+
url = https://github.com/morpho-dao/morpho-utils
15+

certora/applyHarnessFIFO.patch

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ diff -ruN MockDLL.sol MockDLL.sol
3737
--- MockDLL.sol 1970-01-01 01:00:00.000000000 +0100
3838
+++ MockDLL.sol 2022-12-16 20:35:27.432652652 +0100
3939
@@ -0,0 +1,111 @@
40-
+// SPDX-License-Identifier: GNU AGPLv3
40+
+// SPDX-License-Identifier: AGPL-3.0-only
4141
+pragma solidity ^0.8.0;
4242
+
4343
+import "./DoubleLinkedList.sol";

certora/applyHarnessSimple.patch

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ diff -ruN MockDLL.sol MockDLL.sol
5656
--- MockDLL.sol 1970-01-01 01:00:00.000000000 +0100
5757
+++ MockDLL.sol 2022-11-11 11:27:17.368989154 +0100
5858
@@ -0,0 +1,91 @@
59-
+// SPDX-License-Identifier: GNU AGPLv3
59+
+// SPDX-License-Identifier: AGPL-3.0-only
6060
+pragma solidity ^0.8.0;
6161
+
6262
+import "./DoubleLinkedList.sol";

foundry.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
[profile.default]
22
solc_version = "0.8.17"
3+
no_match_test = "testGasUsage"
4+
5+
[profile.gas]
6+
no_match_test = "None"
7+
match_test = "testGasUsage"
8+
verbosity = 2
39

410
[fuzz]
511
runs = 1024

lib/ds-test

lib/morpho-utils

Submodule morpho-utils added at fcf73bd

src/BucketDLL.sol

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
pragma solidity ^0.8.0;
3+
4+
library BucketDLL {
5+
/// STRUCTS ///
6+
7+
struct Account {
8+
address prev;
9+
address next;
10+
}
11+
12+
struct List {
13+
mapping(address => Account) accounts;
14+
}
15+
16+
/// INTERNAL ///
17+
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.
35+
/// @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;
46+
}
47+
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.
52+
/// @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];
55+
address prev = account.prev;
56+
address next = account.next;
57+
58+
_list.accounts[prev].next = next;
59+
_list.accounts[next].prev = prev;
60+
61+
delete _list.accounts[_id];
62+
63+
return (prev == address(0) && next == address(0));
64+
}
65+
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.
71+
/// @return Whether the bucket was empty before insertion.
72+
function insert(
73+
List storage _list,
74+
address _id,
75+
bool _head
76+
) 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;
82+
return head == address(0);
83+
} 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;
88+
return tail == address(0);
89+
}
90+
}
91+
}

src/LogarithmicBuckets.sol

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
pragma solidity ^0.8.0;
3+
4+
import "./BucketDLL.sol";
5+
6+
library LogarithmicBuckets {
7+
using BucketDLL for BucketDLL.List;
8+
9+
struct BucketList {
10+
mapping(uint256 => BucketDLL.List) lists;
11+
mapping(address => uint256) valueOf;
12+
uint256 bucketsMask;
13+
}
14+
15+
/// ERRORS ///
16+
17+
/// @notice Thrown when the address is zero at insertion.
18+
error ZeroAddress();
19+
20+
/// @notice Thrown when 0 value is inserted.
21+
error ZeroValue();
22+
23+
/// INTERNAL ///
24+
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.
30+
function update(
31+
BucketList storage _buckets,
32+
address _id,
33+
uint256 _newValue,
34+
bool _head
35+
) internal {
36+
if (_id == address(0)) revert ZeroAddress();
37+
uint256 value = _buckets.valueOf[_id];
38+
_buckets.valueOf[_id] = _newValue;
39+
40+
if (value == 0) {
41+
if (_newValue == 0) revert ZeroValue();
42+
_insert(_buckets, _id, _computeBucket(_newValue), _head);
43+
return;
44+
}
45+
46+
uint256 currentBucket = _computeBucket(value);
47+
if (_newValue == 0) {
48+
_remove(_buckets, _id, currentBucket);
49+
return;
50+
}
51+
52+
uint256 newBucket = _computeBucket(_newValue);
53+
if (newBucket != currentBucket) {
54+
_remove(_buckets, _id, currentBucket);
55+
_insert(_buckets, _id, newBucket, _head);
56+
}
57+
}
58+
59+
/// @notice Returns the value of `_id`.
60+
/// @param _buckets The buckets to search in.
61+
/// @param _id The address of the account.
62+
function getValueOf(BucketList storage _buckets, address _id) internal view returns (uint256) {
63+
return _buckets.valueOf[_id];
64+
}
65+
66+
/// @notice Returns the bucket in which to look for `_value`.
67+
/// @param _buckets The buckets to search in.
68+
/// @param _value The value to look for.
69+
function getBucketOf(BucketList storage _buckets, uint256 _value)
70+
internal
71+
view
72+
returns (BucketDLL.List storage)
73+
{
74+
uint256 bucket = _computeBucket(_value);
75+
return _buckets.lists[bucket];
76+
}
77+
78+
/// @notice Returns the address in `_buckets` that is a candidate for matching the value `_value`.
79+
/// @param _buckets The buckets to get the head.
80+
/// @param _value The value to match.
81+
/// @return The address of the head.
82+
function getMatch(BucketList storage _buckets, uint256 _value) internal view returns (address) {
83+
uint256 bucketsMask = _buckets.bucketsMask;
84+
if (bucketsMask == 0) return address(0);
85+
uint256 lowerMask = _setLowerBits(_value);
86+
87+
uint256 next = _nextBucket(lowerMask, bucketsMask);
88+
89+
if (next != 0) return _buckets.lists[next].getHead();
90+
91+
uint256 prev = _prevBucket(lowerMask, bucketsMask);
92+
93+
return _buckets.lists[prev].getHead();
94+
}
95+
96+
/// PRIVATE ///
97+
98+
/// @notice Removes an account in the `_buckets`.
99+
/// @dev Does not update the value.
100+
/// @param _buckets The buckets to modify.
101+
/// @param _id The address of the account to remove.
102+
/// @param _bucket The mask of the bucket where to remove.
103+
function _remove(
104+
BucketList storage _buckets,
105+
address _id,
106+
uint256 _bucket
107+
) private {
108+
if (_buckets.lists[_bucket].remove(_id))
109+
_buckets.bucketsMask &= _bucket ^ type(uint256).max;
110+
}
111+
112+
/// @notice Inserts an account in the `_buckets`.
113+
/// @dev Expects that `_id` != 0.
114+
/// @dev Does not update the value.
115+
/// @param _buckets The buckets to modify.
116+
/// @param _id The address of the account to update.
117+
/// @param _bucket The mask of the bucket where to insert.
118+
/// @param _head Whether to insert at the head or at the tail of the list.
119+
function _insert(
120+
BucketList storage _buckets,
121+
address _id,
122+
uint256 _bucket,
123+
bool _head
124+
) private {
125+
if (_buckets.lists[_bucket].insert(_id, _head)) _buckets.bucketsMask |= _bucket;
126+
}
127+
128+
/// @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);
131+
return lowerMask ^ (lowerMask >> 1);
132+
}
133+
134+
/// @notice Sets all the bits lower than (or equal to) the highest bit in the input.
135+
/// @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) {
137+
assembly {
138+
x := or(x, shr(1, x))
139+
x := or(x, shr(2, x))
140+
x := or(x, shr(4, x))
141+
x := or(x, shr(8, x))
142+
x := or(x, shr(16, x))
143+
x := or(x, shr(32, x))
144+
x := or(x, shr(64, x))
145+
y := or(x, shr(128, x))
146+
}
147+
}
148+
149+
/// @notice Returns the following bucket which contains greater values.
150+
/// @dev The bucket returned is the lowest that is in `bucketsMask` and not in `lowerMask`.
151+
function _nextBucket(uint256 lowerMask, uint256 bucketsMask)
152+
private
153+
pure
154+
returns (uint256 bucket)
155+
{
156+
assembly {
157+
let higherBucketsMask := and(not(lowerMask), bucketsMask)
158+
bucket := and(higherBucketsMask, add(not(higherBucketsMask), 1))
159+
}
160+
}
161+
162+
/// @notice Returns the preceding bucket which contains smaller values.
163+
/// @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);
166+
return lowerBucketsMask ^ (lowerBucketsMask >> 1);
167+
}
168+
}

0 commit comments

Comments
 (0)