Skip to content

Commit

Permalink
feat: adding 4337 flavor of linked list
Browse files Browse the repository at this point in the history
  • Loading branch information
zeroknots committed Feb 21, 2024
1 parent 1f9ec02 commit 2783f46
Show file tree
Hide file tree
Showing 2 changed files with 245 additions and 0 deletions.
138 changes: 138 additions & 0 deletions src/SentinelList4337.sol
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)
}
}
}
107 changes: 107 additions & 0 deletions test/SentinelListTest4337.t.sol
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]);
}
}
}

0 comments on commit 2783f46

Please sign in to comment.