Skip to content
Merged
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
9 changes: 9 additions & 0 deletions contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ evm_version = "paris" # Autonity doesn't support PUSH0 (Shanghai)
optimizer = true
optimizer_runs = 10000

gas_reports = ["KeyRAAccessControl"]

[fuzz]
runs = 1000

[invariant]
runs = 256
depth = 64

[rpc_endpoints]
local = "http://127.0.0.1:8545"

Expand Down
124 changes: 124 additions & 0 deletions contracts/test/AccessList.invariant.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";
import {KeyRAAccessControl} from "../src/AccessList.sol";

/// @notice Handler contract that drives invariant testing by calling
/// admin and access management functions with bounded random inputs.
/// Catches only expected errors — unexpected reverts propagate as failures.
contract KeyRAAccessControlHandler is Test {
KeyRAAccessControl public acl;

// Track successful operations for observability
uint256 public adminsAdded;
uint256 public adminsRemoved;
uint256 public accessGranted;
uint256 public accessRevoked;

constructor(KeyRAAccessControl _acl) {
acl = _acl;
}

function addAdmin(uint256 seed) external {
address account = _boundAddr(seed);
try acl.addAdmin(account) {
adminsAdded++;
} catch (bytes memory reason) {
_expectKnownError(reason);
}
}

function removeAdmin(uint256 seed) external {
address account = _boundAddr(seed);
try acl.removeAdmin(account) {
adminsRemoved++;
} catch (bytes memory reason) {
_expectKnownError(reason);
}
}

function grantAccess(uint256 seed) external {
address account = _boundAddr(seed);
try acl.grantAccess(account) {
accessGranted++;
} catch (bytes memory reason) {
_expectKnownError(reason);
}
}

function revokeAccess(uint256 seed) external {
address account = _boundAddr(seed);
try acl.revokeAccess(account) {
accessRevoked++;
} catch (bytes memory reason) {
_expectKnownError(reason);
}
}

function _boundAddr(uint256 seed) internal pure returns (address) {
return address(uint160(bound(seed, 1, type(uint160).max)));
}

/// @dev Only allow known/expected revert reasons. Unknown reverts fail the test.
function _expectKnownError(bytes memory reason) internal pure {
bytes4 selector;
if (reason.length >= 4) {
assembly {
selector := mload(add(reason, 32))
}
}

if (
selector == KeyRAAccessControl.AlreadyAdmin.selector
|| selector == KeyRAAccessControl.NotAnAdmin.selector
|| selector == KeyRAAccessControl.CannotRemoveLastAdmin.selector
|| selector == KeyRAAccessControl.AlreadyHasAccess.selector
|| selector == KeyRAAccessControl.NoAccess.selector
|| selector == KeyRAAccessControl.ZeroAddress.selector
) {
return; // expected — ignore
}

// Unexpected revert — propagate original revert data as test failure
assembly {
revert(add(reason, 32), mload(reason))
}
}
}

/// @notice Invariant tests for KeyRAAccessControl.
/// Verifies properties that must hold regardless of operation sequence.
contract KeyRAAccessControlInvariantTest is Test {
KeyRAAccessControl public acl;
KeyRAAccessControlHandler public handler;

function setUp() public {
acl = new KeyRAAccessControl(address(this));
handler = new KeyRAAccessControlHandler(acl);

// Handler acts as admin so its calls go through onlyAdmin
acl.addAdmin(address(handler));

targetContract(address(handler));
}

/// @notice Admin count must never reach zero — the contract guard prevents it.
function invariant_adminCountNeverZero() public view {
assertGt(acl.adminCount(), 0);
}

/// @notice Operations are actually being exercised — not all silently reverting.
/// Uses a call counter to skip the check on the initial setUp() invocation.
uint256 private _invariantCalls;

function invariant_operationsExercised() public {
_invariantCalls++;
if (_invariantCalls <= 1) return; // skip initial setUp() check

uint256 total =
handler.adminsAdded() + handler.adminsRemoved() + handler.accessGranted()
+ handler.accessRevoked();
assertGt(total, 0, "No operations succeeded - handler may be misconfigured");
}
}
216 changes: 216 additions & 0 deletions contracts/test/AccessList.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,220 @@ contract KeyRAAccessControlTest is Test {
vm.expectRevert(KeyRAAccessControl.ZeroAddress.selector);
acl.grantAccess(address(0));
}

// Self-referential operation tests

function test_removeAdmin_selfRemoval() public {
acl.addAdmin(user1);
// admin removes themselves — should succeed since adminCount > 1
acl.removeAdmin(admin);
assertFalse(acl.isAdmin(admin));
assertEq(acl.adminCount(), 1);
}

function test_grantAccess_toAdmin() public {
// admin and access are orthogonal — admin can grant access to themselves
acl.grantAccess(admin);
assertTrue(acl.hasAccess(admin));
assertTrue(acl.isAdmin(admin));
}

function test_revokeAccess_fromAdmin() public {
acl.grantAccess(admin);
acl.revokeAccess(admin);
assertFalse(acl.hasAccess(admin));
assertTrue(acl.isAdmin(admin)); // admin role unaffected
}

// State transition cycle tests

function test_grantAccess_afterRevoke_succeeds() public {
acl.grantAccess(user1);
acl.revokeAccess(user1);
assertFalse(acl.hasAccess(user1));

// Re-grant should succeed
acl.grantAccess(user1);
assertTrue(acl.hasAccess(user1));
}

function test_addAdmin_afterRemoval_succeeds() public {
acl.addAdmin(user1);
acl.removeAdmin(user1);
assertFalse(acl.isAdmin(user1));

// Re-add should succeed
acl.addAdmin(user1);
assertTrue(acl.isAdmin(user1));
assertEq(acl.adminCount(), 2);
}

function test_removeAdmin_doesNotRevokeAccess() public {
acl.addAdmin(user1);
acl.grantAccess(user1);

acl.removeAdmin(user1);

assertFalse(acl.isAdmin(user1));
assertTrue(acl.hasAccess(user1)); // access and admin are independent
}

// Removed admin cannot act tests

function test_removedAdmin_cannotGrantAccess() public {
acl.addAdmin(user1);
acl.removeAdmin(user1);

vm.prank(user1);
vm.expectRevert(KeyRAAccessControl.NotAdmin.selector);
acl.grantAccess(user2);
}

function test_removedAdmin_cannotAddAdmin() public {
acl.addAdmin(user1);
acl.removeAdmin(user1);

vm.prank(user1);
vm.expectRevert(KeyRAAccessControl.NotAdmin.selector);
acl.addAdmin(user2);
}

// Multi-admin complex scenario tests

function test_removeAdmin_threeAdmins_canRemoveTwo() public {
address user3 = makeAddr("user3");
acl.addAdmin(user1);
acl.addAdmin(user3);
assertEq(acl.adminCount(), 3);

acl.removeAdmin(user1);
acl.removeAdmin(user3);
assertEq(acl.adminCount(), 1);

// Last admin cannot be removed
vm.expectRevert(KeyRAAccessControl.CannotRemoveLastAdmin.selector);
acl.removeAdmin(admin);
}

function test_newAdmin_removesOriginalAdmin() public {
acl.addAdmin(user1);

// user1 removes the original admin who added them
vm.prank(user1);
acl.removeAdmin(admin);

assertFalse(acl.isAdmin(admin));
assertEq(acl.adminCount(), 1);

// user1 is now the sole admin and cannot remove themselves
vm.prank(user1);
vm.expectRevert(KeyRAAccessControl.CannotRemoveLastAdmin.selector);
acl.removeAdmin(user1);
}

function test_adminA_grants_adminB_revokes_sameUser() public {
acl.addAdmin(user1);

// admin grants access to user2
acl.grantAccess(user2);

// user1 (different admin) revokes user2's access
vm.prank(user1);
acl.revokeAccess(user2);

assertFalse(acl.hasAccess(user2));
}

// View function dedicated tests

function test_adminCount_afterMultipleAddRemove() public {
assertEq(acl.adminCount(), 1);
acl.addAdmin(user1);
assertEq(acl.adminCount(), 2);
acl.addAdmin(user2);
assertEq(acl.adminCount(), 3);
acl.removeAdmin(user1);
assertEq(acl.adminCount(), 2);
acl.removeAdmin(user2);
assertEq(acl.adminCount(), 1);
}

// Full lifecycle integration test

function test_fullLifecycle() public {
// 1. Initial admin grants access to user1
acl.grantAccess(user1);
assertTrue(acl.hasAccess(user1));

// 2. Initial admin adds user1 as admin
acl.addAdmin(user1);

// 3. user1 (now admin) grants access to user2
vm.prank(user1);
acl.grantAccess(user2);
assertTrue(acl.hasAccess(user2));

// 4. user1 revokes user2's access
vm.prank(user1);
acl.revokeAccess(user2);
assertFalse(acl.hasAccess(user2));

// 5. Original admin removes user1 as admin
acl.removeAdmin(user1);

// 6. user1 still has access but cannot admin
assertTrue(acl.hasAccess(user1));
vm.prank(user1);
vm.expectRevert(KeyRAAccessControl.NotAdmin.selector);
acl.grantAccess(user2);
}

// Fuzz tests — access control enforcement

function testFuzz_grantAccess_revertsIfNotAdmin(address caller) public {
vm.assume(caller != admin);
vm.assume(caller != address(0));
vm.prank(caller);
vm.expectRevert(KeyRAAccessControl.NotAdmin.selector);
acl.grantAccess(user1);
}

function testFuzz_addAdmin_revertsIfNotAdmin(address caller) public {
vm.assume(caller != admin);
vm.assume(caller != address(0));
vm.prank(caller);
vm.expectRevert(KeyRAAccessControl.NotAdmin.selector);
acl.addAdmin(user1);
}

function testFuzz_removeAdmin_revertsIfNotAdmin(address caller) public {
vm.assume(caller != admin);
vm.assume(caller != address(0));
vm.prank(caller);
vm.expectRevert(KeyRAAccessControl.NotAdmin.selector);
acl.removeAdmin(admin);
}

function testFuzz_revokeAccess_revertsIfNotAdmin(address caller) public {
acl.grantAccess(user1);
vm.assume(caller != admin);
vm.assume(caller != address(0));
vm.prank(caller);
vm.expectRevert(KeyRAAccessControl.NotAdmin.selector);
acl.revokeAccess(user1);
}

function testFuzz_grantAccess_thenHasAccess(address account) public {
vm.assume(account != address(0));
vm.assume(!acl.hasAccess(account));
acl.grantAccess(account);
assertTrue(acl.hasAccess(account));
}

function testFuzz_addAdmin_thenIsAdmin(address account) public {
vm.assume(account != admin);
vm.assume(account != address(0));
acl.addAdmin(account);
assertTrue(acl.isAdmin(account));
}
}
Loading