Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/7702 sentinellist #5

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
branch = v1.5.2
[submodule "lib/EIP7702Storage"]
path = lib/EIP7702Storage
url = https://github.com/rhinestonewtf/EIP7702Storage
1 change: 1 addition & 0 deletions lib/EIP7702Storage
Submodule EIP7702Storage added at a8fd53
112 changes: 112 additions & 0 deletions src/SentinelList7702.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

address constant SENTINEL = address(0x1);
address constant ZERO_ADDRESS = address(0x0);

import "EIP7702Storage/RDataLib.sol";

library SentinelListLib {
using RData for RData.Address;

struct SentinelList {
mapping(address => RData.Address) entries;
}

error LinkedList_AlreadyInitialized();
error LinkedList_InvalidPage();
error LinkedList_InvalidEntry(address entry);
error LinkedList_EntryAlreadyInList(address entry);

function init(SentinelList storage self) internal {
if (alreadyInitialized(self)) revert LinkedList_AlreadyInitialized();
self.entries[SENTINEL].store(SENTINEL);
}

function alreadyInitialized(SentinelList storage self) internal view returns (bool) {
return self.entries[SENTINEL].load() != ZERO_ADDRESS;
}

function getNext(SentinelList storage self, address entry) internal view returns (address) {
if (entry == ZERO_ADDRESS) {
revert LinkedList_InvalidEntry(entry);
}
return self.entries[entry].load();
}

function push(SentinelList storage self, address newEntry) internal {
if (newEntry == ZERO_ADDRESS || newEntry == SENTINEL) {
revert LinkedList_InvalidEntry(newEntry);
}
if (self.entries[newEntry].load() != ZERO_ADDRESS) {
revert LinkedList_EntryAlreadyInList(newEntry);
}
self.entries[newEntry].store(self.entries[SENTINEL].load());
self.entries[SENTINEL].store(newEntry);
}

function pop(SentinelList storage self, address prevEntry, address popEntry) internal {
if (popEntry == ZERO_ADDRESS || popEntry == SENTINEL) {
revert LinkedList_InvalidEntry(prevEntry);
}
if (self.entries[prevEntry].load() != popEntry) revert LinkedList_InvalidEntry(popEntry);
self.entries[prevEntry].store(self.entries[popEntry].load());
self.entries[popEntry].store(ZERO_ADDRESS);
}

function popAll(SentinelList storage self) internal {
address next = self.entries[SENTINEL].load();
while (next != ZERO_ADDRESS) {
address current = next;
next = self.entries[next].load();
self.entries[current].store(ZERO_ADDRESS);
}
self.entries[SENTINEL].store(ZERO_ADDRESS);
}

function contains(SentinelList storage self, address entry) internal view returns (bool) {
return SENTINEL != entry && self.entries[entry].load() != ZERO_ADDRESS;
}

function getEntriesPaginated(
SentinelList storage self,
address start,
uint256 pageSize
)
internal
view
returns (address[] memory array, address next)
{
if (start != SENTINEL && !contains(self, 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].load();
while (next != ZERO_ADDRESS && next != SENTINEL && entryCount < pageSize) {
array[entryCount] = next;
next = self.entries[next].load();
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 && entryCount > 0) {
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)
}
}
}
235 changes: 235 additions & 0 deletions test/concrete/SentinelList7702Test.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import "forge-std/Test.sol";
import { SentinelListLib, SENTINEL, ZERO_ADDRESS } from "src/SentinelList7702.sol";
import { SentinelListHelper } from "src/SentinelListHelper.sol";
import "EIP7702Storage/RDataStorage.sol";
import "EIP7702Storage/RDataLib.sol";

contract SentinelListTest is Test {
using SentinelListLib for SentinelListLib.SentinelList;

/*//////////////////////////////////////////////////////////////////////////
VARIABLES
//////////////////////////////////////////////////////////////////////////*/

SentinelListLib.SentinelList list;
SentinelListLib.SentinelList newList;

/*//////////////////////////////////////////////////////////////////////////
SETUP
//////////////////////////////////////////////////////////////////////////*/

function setUp() public {
RDataStorage storageContract = new RDataStorage();
vm.etch(address(RData.storageContract), address(storageContract).code);
// only the first list is initialized
list.init();
}

/*//////////////////////////////////////////////////////////////////////////
UTILS
//////////////////////////////////////////////////////////////////////////*/

function addMany(uint256 amount) public {
for (uint256 i = 1; i <= amount; i++) {
address addr = makeAddr(vm.toString(i));
list.push(addr);
}

for (uint256 i = 1; i <= amount; i++) {
address addr = makeAddr(vm.toString(i));
assertTrue(list.contains(addr));
}
}

/*//////////////////////////////////////////////////////////////////////////
TESTS
//////////////////////////////////////////////////////////////////////////*/

function test_InitRevertWhen_AlreadyInitialized() external {
// it should revert
list.init();
}

function test_InitWhenNotInitialized() external {
// it should set sentinel to sentinel
address next = list.getNext(SENTINEL);
assertEq(next, SENTINEL);
}

function test_AlreadyInitializedWhenSentinelDoesNotPointTo0() external {
// it should return true
bool isInitialized = list.alreadyInitialized();
assertTrue(isInitialized);
}

function test_AlreadyInitializedWhenSentinelPointsTo0() external {
// it should return false
bool isInitialized = newList.alreadyInitialized();
assertFalse(isInitialized);
}

function test_GetNextRevertWhen_EntryIsZero() external {
// it should revert
vm.expectRevert();
list.getNext(ZERO_ADDRESS);
}

function test_GetNextWhenEntryIsNotZero() external {
// it should return the next entry
address newEntry = makeAddr("newEntry");
list.push(address(newEntry));

address next = list.getNext(SENTINEL);
assertEq(next, newEntry);
}

function test_PushRevertWhen_EntryIsZero() external {
// it should revert
vm.expectRevert();
list.push(ZERO_ADDRESS);
}

function test_PushRevertWhen_EntryIsSentinel() external {
// it should revert
vm.expectRevert();
list.push(SENTINEL);
}

function test_PushRevertWhen_EntryIsAlreadyAdded() external whenEntryIsNotSentinel {
// it should revert
address newEntry = makeAddr("newEntry");
list.push(address(newEntry));

address next = list.getNext(SENTINEL);
assertEq(next, newEntry);

vm.expectRevert();
list.push(address(newEntry));
}

function test_PushWhenEntryIsNotAdded() external whenEntryIsNotSentinel {
// it should add the entry to the list
address newEntry = makeAddr("newEntry");
list.push(address(newEntry));
}

function test_PopRevertWhen_EntryIsZero() external {
// it should revert
vm.expectRevert();
list.pop(SENTINEL, ZERO_ADDRESS);
}

function test_PopRevertWhen_EntryIsSentinel() external {
// it should revert
vm.expectRevert();
list.pop(SENTINEL, SENTINEL);
}

function test_PopRevertWhen_PrevEntryDoesNotPointToEntry() external whenEntryIsNotSentinel {
// it should revert
address newEntry = makeAddr("newEntry");

vm.expectRevert();
list.pop(SENTINEL, address(newEntry));
}

function test_PopWhenPrevEntryPointsToEntry() external whenEntryIsNotSentinel {
// it should remove the entry from the list
address newEntry = makeAddr("newEntry");
list.push(address(newEntry));

address next = list.getNext(SENTINEL);
assertEq(next, newEntry);

list.pop(SENTINEL, address(newEntry));

next = list.getNext(SENTINEL);
assertEq(next, SENTINEL);
}

function test_PopAllShouldRemoveAllEntries() external {
// it should remove all entries
uint256 amount = 8;
addMany(amount);
list.popAll();

for (uint256 i = 1; i <= amount; i++) {
assertFalse(list.contains(makeAddr(vm.toString(i))));
}
}

function test_PopAllShouldSetSentinelToZero() external {
// it should set sentinel to zero
uint256 amount = 8;
addMany(amount);
list.popAll();

address next = list.getNext(SENTINEL);
assertEq(next, ZERO_ADDRESS);
}

function test_ContainsWhenEntryIsSentinel() external {
// it should return false
bool contains = list.contains(SENTINEL);
assertFalse(contains);
}

function test_ContainsWhenEntryIsZero() external whenEntryIsNotSentinel {
// it should return false
bool contains = list.contains(ZERO_ADDRESS);
assertFalse(contains);
}

function test_ContainsWhenEntryIsNotZero() external whenEntryIsNotSentinel {
// it should return true
address newEntry = makeAddr("newEntry");
list.push(address(newEntry));

bool contains = list.contains(address(newEntry));
assertTrue(contains);
}

function test_GetEntriesPaginatedRevertWhen_EntryIsNotContainedOrSentinel() external {
// it should revert
vm.expectRevert();
list.getEntriesPaginated(ZERO_ADDRESS, 1);
}

function test_GetEntriesPaginatedRevertWhen_PageSizeIsZero()
external
whenEntryIsContainedOrSentinel
{
// it should revert
vm.expectRevert();
list.getEntriesPaginated(SENTINEL, 0);
}

function test_GetEntriesPaginatedWhenPageSizeIsNotZero()
external
whenEntryIsContainedOrSentinel
{
// it should return all entries until page size
// it should return the next entry
uint256 amount = 8;
addMany(amount);

(address[] memory array, address next) = list.getEntriesPaginated(SENTINEL, 4);
assertEq(array.length, 4);
assertEq(next, makeAddr("5"));
}

/*//////////////////////////////////////////////////////////////////////////
MODIFIERS
//////////////////////////////////////////////////////////////////////////*/

modifier whenEntryIsNotSentinel() {
_;
}

modifier whenEntryIsContainedOrSentinel() {
_;
}
}