generated from zeroknots/femplate
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adding 4337 flavor of linked list
- Loading branch information
Showing
2 changed files
with
245 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
|
||
address constant SENTINEL = address(0x1); | ||
address constant ZERO_ADDRESS = address(0x0); | ||
|
||
/** | ||
* Implements a linked list, but adheres to ERC-4337 storage restrictions. | ||
* Intended use: validator modules for modular ERC-4337 smart accounts | ||
* @author kopy-kat | rhinestone.wtf | ||
*/ | ||
library SentinelList4337Lib { | ||
struct SentinelList { | ||
mapping(address key => mapping(address account => address entry)) entries; | ||
} | ||
|
||
error LinkedList_AlreadyInitialized(); | ||
error LinkedList_InvalidPage(); | ||
error LinkedList_InvalidEntry(address entry); | ||
error LinkedList_EntryAlreadyInList(address entry); | ||
|
||
function init(SentinelList storage self, address account) internal { | ||
if (alreadyInitialized(self, account)) revert LinkedList_AlreadyInitialized(); | ||
self.entries[SENTINEL][account] = SENTINEL; | ||
} | ||
|
||
function alreadyInitialized( | ||
SentinelList storage self, | ||
address account | ||
) | ||
internal | ||
view | ||
returns (bool) | ||
{ | ||
return self.entries[SENTINEL][account] != ZERO_ADDRESS; | ||
} | ||
|
||
function getNext( | ||
SentinelList storage self, | ||
address account, | ||
address entry | ||
) | ||
internal | ||
view | ||
returns (address) | ||
{ | ||
if (entry == ZERO_ADDRESS) { | ||
revert LinkedList_InvalidEntry(entry); | ||
} | ||
return self.entries[entry][account]; | ||
} | ||
|
||
function push(SentinelList storage self, address account, address newEntry) internal { | ||
if (newEntry == ZERO_ADDRESS || newEntry == SENTINEL) { | ||
revert LinkedList_InvalidEntry(newEntry); | ||
} | ||
if (self.entries[newEntry][account] != ZERO_ADDRESS) { | ||
revert LinkedList_EntryAlreadyInList(newEntry); | ||
} | ||
self.entries[newEntry][account] = self.entries[SENTINEL][account]; | ||
self.entries[SENTINEL][account] = newEntry; | ||
} | ||
|
||
function pop( | ||
SentinelList storage self, | ||
address account, | ||
address prevEntry, | ||
address popEntry | ||
) | ||
internal | ||
{ | ||
if (popEntry == ZERO_ADDRESS || popEntry == SENTINEL) { | ||
revert LinkedList_InvalidEntry(prevEntry); | ||
} | ||
if (self.entries[prevEntry][account] != popEntry) { | ||
revert LinkedList_InvalidEntry(popEntry); | ||
} | ||
self.entries[prevEntry][account] = self.entries[popEntry][account]; | ||
self.entries[popEntry][account] = ZERO_ADDRESS; | ||
} | ||
|
||
function contains( | ||
SentinelList storage self, | ||
address account, | ||
address entry | ||
) | ||
internal | ||
view | ||
returns (bool) | ||
{ | ||
return SENTINEL != entry && self.entries[entry][account] != ZERO_ADDRESS; | ||
} | ||
|
||
function getEntriesPaginated( | ||
SentinelList storage self, | ||
address account, | ||
address start, | ||
uint256 pageSize | ||
) | ||
internal | ||
view | ||
returns (address[] memory array, address next) | ||
{ | ||
if (start != SENTINEL && contains(self, account, start)) { | ||
revert LinkedList_InvalidEntry(start); | ||
} | ||
if (pageSize == 0) revert LinkedList_InvalidPage(); | ||
// Init array with max page size | ||
array = new address[](pageSize); | ||
|
||
// Populate return array | ||
uint256 entryCount = 0; | ||
next = self.entries[start][account]; | ||
while (next != ZERO_ADDRESS && next != SENTINEL && entryCount < pageSize) { | ||
array[entryCount] = next; | ||
next = self.entries[next][account]; | ||
entryCount++; | ||
} | ||
|
||
/** | ||
* Because of the argument validation, we can assume that the loop will always iterate over the valid entry list values | ||
* and the `next` variable will either be an enabled entry or a sentinel address (signalling the end). | ||
* | ||
* If we haven't reached the end inside the loop, we need to set the next pointer to the last element of the entry array | ||
* because the `next` variable (which is a entry by itself) acting as a pointer to the start of the next page is neither | ||
* incSENTINELrent page, nor will it be included in the next one if you pass it as a start. | ||
*/ | ||
if (next != SENTINEL) { | ||
next = array[entryCount - 1]; | ||
} | ||
// Set correct size of returned array | ||
// solhint-disable-next-line no-inline-assembly | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
mstore(array, entryCount) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.19; | ||
|
||
import "forge-std/Test.sol"; | ||
|
||
import { SentinelList4337Lib } from "../src/SentinelList4337.sol"; | ||
import "../src/SentinelListHelper.sol"; | ||
|
||
/// @author kopy-kat | ||
contract SentinelList4337Test is Test { | ||
using SentinelList4337Lib for SentinelList4337Lib.SentinelList; | ||
|
||
SentinelList4337Lib.SentinelList list; | ||
|
||
address account; | ||
|
||
function setUp() public { | ||
account = makeAddr("account"); | ||
list.init({ account: account }); | ||
} | ||
|
||
function testAddMany() public { | ||
address addr1 = makeAddr("1"); | ||
address addr2 = makeAddr("2"); | ||
address addr3 = makeAddr("3"); | ||
address addr4 = makeAddr("4"); | ||
address addr5 = makeAddr("5"); | ||
address addr6 = makeAddr("6"); | ||
address addr7 = makeAddr("7"); | ||
address addr8 = makeAddr("8"); | ||
|
||
list.push({ account: account, newEntry: addr2 }); | ||
list.push({ account: account, newEntry: addr4 }); | ||
list.push({ account: account, newEntry: addr7 }); | ||
list.push({ account: account, newEntry: addr5 }); | ||
list.push({ account: account, newEntry: addr1 }); | ||
list.push({ account: account, newEntry: addr6 }); | ||
list.push({ account: account, newEntry: addr3 }); | ||
list.push({ account: account, newEntry: addr8 }); | ||
|
||
assertTrue(list.contains({ account: account, entry: addr1 })); | ||
assertTrue(list.contains({ account: account, entry: addr2 })); | ||
assertTrue(list.contains({ account: account, entry: addr3 })); | ||
assertTrue(list.contains({ account: account, entry: addr4 })); | ||
assertTrue(list.contains({ account: account, entry: addr5 })); | ||
assertTrue(list.contains({ account: account, entry: addr6 })); | ||
assertTrue(list.contains({ account: account, entry: addr7 })); | ||
assertTrue(list.contains({ account: account, entry: addr8 })); | ||
|
||
assertFalse(list.contains({ account: account, entry: makeAddr("9") })); | ||
} | ||
|
||
function testAdd() public { | ||
address addr1 = makeAddr("1"); | ||
address addr2 = makeAddr("2"); | ||
|
||
list.push({ account: account, newEntry: addr2 }); | ||
assertFalse(list.contains({ account: account, entry: addr1 })); | ||
assertTrue(list.contains({ account: account, entry: addr2 })); | ||
} | ||
|
||
function testRemove() public { | ||
address addr1 = makeAddr("1"); | ||
address addr2 = makeAddr("2"); | ||
address addr3 = makeAddr("3"); | ||
address addr4 = makeAddr("4"); | ||
|
||
list.push({ account: account, newEntry: addr1 }); | ||
list.push({ account: account, newEntry: addr2 }); | ||
list.push({ account: account, newEntry: addr3 }); | ||
list.push({ account: account, newEntry: addr4 }); | ||
|
||
list.pop({ account: account, prevEntry: addr3, popEntry: addr2 }); | ||
|
||
assertTrue(list.contains({ account: account, entry: addr1 })); | ||
assertFalse(list.contains({ account: account, entry: addr2 })); | ||
assertTrue(list.contains({ account: account, entry: addr3 })); | ||
assertTrue(list.contains({ account: account, entry: addr4 })); | ||
|
||
list.push({ account: account, newEntry: addr2 }); | ||
|
||
assertTrue(list.contains({ account: account, entry: addr1 })); | ||
assertTrue(list.contains({ account: account, entry: addr2 })); | ||
assertTrue(list.contains({ account: account, entry: addr3 })); | ||
assertTrue(list.contains({ account: account, entry: addr4 })); | ||
|
||
(address[] memory array, address next) = | ||
list.getEntriesPaginated({ account: account, start: address(0x1), pageSize: 100 }); | ||
|
||
address remove = addr4; | ||
address prev = SentinelListHelper.findPrevious(array, remove); | ||
console2.log("prev", prev); | ||
console2.log("should be", addr4); | ||
|
||
list.pop({ account: account, prevEntry: prev, popEntry: remove }); | ||
assertFalse(list.contains({ account: account, entry: remove })); | ||
|
||
_log(array, next); | ||
} | ||
|
||
function _log(address[] memory array, address next) internal { | ||
console2.log("next", next); | ||
for (uint256 i = 0; i < array.length; i++) { | ||
console2.log("array[%s]: %s", i, array[i]); | ||
} | ||
} | ||
} |