diff --git a/test/access/Owner/LibOwner.t.sol b/test/access/Owner/LibOwner.t.sol new file mode 100644 index 00000000..1416928d --- /dev/null +++ b/test/access/Owner/LibOwner.t.sol @@ -0,0 +1,321 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test, console2} from "forge-std/Test.sol"; +import {LibOwner} from "../../../src/access/Owner/LibOwner.sol"; +import {LibOwnerHarness} from "./harnesses/LibOwnerHarness.sol"; + +contract LibOwnerTest is Test { + LibOwnerHarness public harness; + + address INITIAL_OWNER = makeAddr("owner"); + address NEW_OWNER = makeAddr("newOwner"); + address ALICE = makeAddr("alice"); + address BOB = makeAddr("bob"); + address ZERO_ADDRESS = address(0); + + // Events + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + function setUp() public { + harness = new LibOwnerHarness(); + harness.initialize(INITIAL_OWNER); + } + + // ============================================ + // Storage Tests + // ============================================ + + function test_GetStorage_ReturnsCorrectOwner() public view { + assertEq(harness.owner(), INITIAL_OWNER); + assertEq(harness.getStorageOwner(), INITIAL_OWNER); + } + + function test_StorageSlot_UsesCorrectPosition() public { + bytes32 expectedSlot = keccak256("compose.owner"); + + // Change owner + vm.prank(INITIAL_OWNER); + harness.transferOwnership(NEW_OWNER); + + // Read directly from storage + bytes32 storedValue = vm.load(address(harness), expectedSlot); + address storedOwner = address(uint160(uint256(storedValue))); + + assertEq(storedOwner, NEW_OWNER); + assertEq(harness.owner(), NEW_OWNER); + } + + // ============================================ + // Owner Getter Tests + // ============================================ + + function test_Owner_ReturnsCurrentOwner() public { + assertEq(harness.owner(), INITIAL_OWNER); + + vm.prank(INITIAL_OWNER); + harness.transferOwnership(NEW_OWNER); + assertEq(harness.owner(), NEW_OWNER); + } + + function test_Owner_ReturnsZeroAfterRenounce() public { + vm.prank(INITIAL_OWNER); + harness.transferOwnership(ZERO_ADDRESS); + assertEq(harness.owner(), ZERO_ADDRESS); + } + + // ============================================ + // Transfer Ownership Tests + // ============================================ + + function test_TransferOwnership_UpdatesOwner() public { + vm.prank(INITIAL_OWNER); + harness.transferOwnership(NEW_OWNER); + assertEq(harness.owner(), NEW_OWNER); + } + + function test_TransferOwnership_EmitsOwnershipTransferredEvent() public { + vm.expectEmit(true, true, false, true); + emit OwnershipTransferred(INITIAL_OWNER, NEW_OWNER); + + vm.prank(INITIAL_OWNER); + harness.transferOwnership(NEW_OWNER); + } + + function test_TransferOwnership_AllowsTransferToZeroAddress() public { + vm.prank(INITIAL_OWNER); + harness.transferOwnership(ZERO_ADDRESS); + assertEq(harness.owner(), ZERO_ADDRESS); + } + + function test_TransferOwnership_AllowsTransferToSelf() public { + vm.prank(INITIAL_OWNER); + harness.transferOwnership(INITIAL_OWNER); + assertEq(harness.owner(), INITIAL_OWNER); + } + + // TODO: When LibOwner is fixed to make renouncement irreversible: + // 1. Rename this test to: test_RevertWhen_TransferOwnership_FromRenouncedOwner + // 2. Change logic to: + // vm.expectRevert(LibOwner.OwnerAlreadyRenounced.selector); + // harness.transferOwnership(NEW_OWNER); + // 3. Remove the assertions for successful transfer + function test_TransferOwnership_AfterRenounce_AllowsNewOwner() public { + // Force renounce + harness.forceRenounce(); + assertEq(harness.owner(), ZERO_ADDRESS); + + // CURRENT BEHAVIOR (BUG): Library allows transferOwnership after renouncement + // EXPECTED BEHAVIOR: Should revert with OwnerAlreadyRenounced error + harness.transferOwnership(NEW_OWNER); + assertEq(harness.owner(), NEW_OWNER); + } + + // ============================================ + // Sequential Transfer Tests + // ============================================ + + function test_MultipleTransfers() public { + // First transfer + vm.prank(INITIAL_OWNER); + harness.transferOwnership(ALICE); + assertEq(harness.owner(), ALICE); + + // Second transfer + vm.prank(ALICE); + harness.transferOwnership(BOB); + assertEq(harness.owner(), BOB); + + // Third transfer + vm.prank(BOB); + harness.transferOwnership(NEW_OWNER); + assertEq(harness.owner(), NEW_OWNER); + } + + // ============================================ + // Event Tests + // ============================================ + + function test_Events_CorrectPreviousOwner() public { + vm.expectEmit(true, true, false, true); + emit OwnershipTransferred(INITIAL_OWNER, ALICE); + + vm.prank(INITIAL_OWNER); + harness.transferOwnership(ALICE); + + vm.expectEmit(true, true, false, true); + emit OwnershipTransferred(ALICE, BOB); + + vm.prank(ALICE); + harness.transferOwnership(BOB); + } + + function test_Events_RenounceEmitsZeroAddress() public { + vm.expectEmit(true, true, false, true); + emit OwnershipTransferred(INITIAL_OWNER, ZERO_ADDRESS); + + vm.prank(INITIAL_OWNER); + harness.transferOwnership(ZERO_ADDRESS); + } + + // ============================================ + // Edge Cases + // ============================================ + + // TODO: When LibOwner is fixed to make renouncement irreversible: + // 1. Rename this test to: test_RenounceOwnership_PermanentlyDisablesTransfers + // 2. Change logic to: + // vm.expectRevert(LibOwner.OwnerAlreadyRenounced.selector); + // harness.transferOwnership(ALICE); + // 3. Remove the assertion for successful transfer + function test_RenounceOwnership_AllowsRecovery() public { + // Renounce ownership + vm.prank(INITIAL_OWNER); + harness.transferOwnership(ZERO_ADDRESS); + assertEq(harness.owner(), ZERO_ADDRESS); + + // CURRENT BEHAVIOR (BUG): Library allows recovery after renouncement + // EXPECTED BEHAVIOR: Should revert with OwnerAlreadyRenounced error + harness.transferOwnership(ALICE); + assertEq(harness.owner(), ALICE); + } + + function test_LibraryDoesNotCheckMsgSender() public { + // The library doesn't check msg.sender - that's the facet's responsibility + // This test verifies the library works regardless of caller + // (In production, the facet should check permissions before calling the library) + + vm.prank(ALICE); // Not the owner + harness.transferOwnership(BOB); + assertEq(harness.owner(), BOB); + + // This shows the library itself doesn't enforce access control + // Access control should be implemented in the facet that uses the library + } + + // ============================================ + // Fuzz Tests + // ============================================ + + function test_Fuzz_TransferOwnership(address newOwner) public { + vm.prank(INITIAL_OWNER); + harness.transferOwnership(newOwner); + assertEq(harness.owner(), newOwner); + } + + function test_Fuzz_MultipleTransfers(address owner1, address owner2, address owner3) public { + vm.assume(owner1 != address(0)); + vm.assume(owner2 != address(0)); + + vm.prank(INITIAL_OWNER); + harness.transferOwnership(owner1); + assertEq(harness.owner(), owner1); + + vm.prank(owner1); + harness.transferOwnership(owner2); + assertEq(harness.owner(), owner2); + + vm.prank(owner2); + harness.transferOwnership(owner3); + assertEq(harness.owner(), owner3); + } + + // TODO: When LibOwner is fixed to make renouncement irreversible: + // 1. Rename this test to: test_Fuzz_RevertWhen_RenouncedOwnerTransfers + // 2. Change logic to: + // vm.expectRevert(LibOwner.OwnerAlreadyRenounced.selector); + // harness.transferOwnership(target); + // 3. Remove the assertion for successful transfer + function test_Fuzz_TransferAfterRenounce_AllowsRecovery(address target) public { + vm.assume(target != address(0)); + + // Renounce + vm.prank(INITIAL_OWNER); + harness.transferOwnership(ZERO_ADDRESS); + assertEq(harness.owner(), ZERO_ADDRESS); + + // CURRENT BEHAVIOR (BUG): Library allows recovery - can transfer to new owner + // EXPECTED BEHAVIOR: Should revert with OwnerAlreadyRenounced error + harness.transferOwnership(target); + assertEq(harness.owner(), target); + } + + // ============================================ + // Renounce Ownership Tests (New Function) + // ============================================ + + function test_RenounceOwnership_SetsOwnerToZero() public { + // Use the new renounceOwnership function + harness.renounceOwnership(); + assertEq(harness.owner(), ZERO_ADDRESS); + } + + function test_RenounceOwnership_EmitsCorrectEvent() public { + vm.expectEmit(true, true, false, true); + emit OwnershipTransferred(INITIAL_OWNER, ZERO_ADDRESS); + + harness.renounceOwnership(); + } + + // ============================================ + // Require Owner Tests (New Function) + // ============================================ + + function test_RequireOwner_PassesForOwner() public { + // Should not revert when called by owner + vm.prank(INITIAL_OWNER); + harness.requireOwner(); + } + + function test_RevertWhen_RequireOwner_CalledByNonOwner() public { + vm.expectRevert(LibOwner.OwnerUnauthorizedAccount.selector); + vm.prank(ALICE); + harness.requireOwner(); + } + + function test_Fuzz_RequireOwner(address caller) public { + if (caller == INITIAL_OWNER) { + // Should not revert for owner + vm.prank(caller); + harness.requireOwner(); + } else { + // Should revert for non-owner + vm.expectRevert(LibOwner.OwnerUnauthorizedAccount.selector); + vm.prank(caller); + harness.requireOwner(); + } + } + + // ============================================ + // Gas Tests + // ============================================ + + function test_Gas_Owner() public view { + uint256 gasBefore = gasleft(); + harness.owner(); + uint256 gasUsed = gasBefore - gasleft(); + + console2.log("Gas used for LibOwner.owner():", gasUsed); + assertTrue(gasUsed < 10000, "Owner getter uses too much gas"); + } + + function test_Gas_TransferOwnership() public { + uint256 gasBefore = gasleft(); + vm.prank(INITIAL_OWNER); + harness.transferOwnership(NEW_OWNER); + uint256 gasUsed = gasBefore - gasleft(); + + console2.log("Gas used for LibOwner.transferOwnership():", gasUsed); + assertTrue(gasUsed < 50000, "Transfer ownership uses too much gas"); + } + + function test_Gas_TransferOwnership_Renounce() public { + uint256 gasBefore = gasleft(); + vm.prank(INITIAL_OWNER); + harness.transferOwnership(ZERO_ADDRESS); + uint256 gasUsed = gasBefore - gasleft(); + + console2.log("Gas used for LibOwner renounce:", gasUsed); + assertTrue(gasUsed < 50000, "Renounce uses too much gas"); + } +} diff --git a/test/access/Owner/OwnerFacet.t.sol b/test/access/Owner/OwnerFacet.t.sol new file mode 100644 index 00000000..43169555 --- /dev/null +++ b/test/access/Owner/OwnerFacet.t.sol @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test, console2} from "forge-std/Test.sol"; +import {OwnerFacet} from "../../../src/access/Owner/OwnerFacet.sol"; +import {OwnerFacetHarness} from "./harnesses/OwnerFacetHarness.sol"; + +contract OwnerFacetTest is Test { + OwnerFacetHarness public ownerFacet; + + address INITIAL_OWNER = makeAddr("owner"); + address NEW_OWNER = makeAddr("newOwner"); + address ALICE = makeAddr("alice"); + address BOB = makeAddr("bob"); + address ZERO_ADDRESS = address(0); + + // Events + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + function setUp() public { + ownerFacet = new OwnerFacetHarness(); + ownerFacet.initialize(INITIAL_OWNER); + } + + // ============================================ + // Ownership Getter Tests + // ============================================ + + function test_Owner_ReturnsCorrectInitialOwner() public view { + assertEq(ownerFacet.owner(), INITIAL_OWNER); + } + + function test_Owner_ReturnsZeroWhenRenounced() public { + vm.prank(INITIAL_OWNER); + ownerFacet.transferOwnership(ZERO_ADDRESS); + + assertEq(ownerFacet.owner(), ZERO_ADDRESS); + } + + // ============================================ + // Transfer Ownership Tests + // ============================================ + + function test_TransferOwnership_ImmediateTransfer() public { + vm.prank(INITIAL_OWNER); + ownerFacet.transferOwnership(NEW_OWNER); + + assertEq(ownerFacet.owner(), NEW_OWNER); + } + + function test_TransferOwnership_EmitsOwnershipTransferredEvent() public { + vm.expectEmit(true, true, false, true); + emit OwnershipTransferred(INITIAL_OWNER, NEW_OWNER); + + vm.prank(INITIAL_OWNER); + ownerFacet.transferOwnership(NEW_OWNER); + } + + function test_TransferOwnership_MultipleTransfers() public { + // First transfer + vm.prank(INITIAL_OWNER); + ownerFacet.transferOwnership(ALICE); + assertEq(ownerFacet.owner(), ALICE); + + // Second transfer + vm.prank(ALICE); + ownerFacet.transferOwnership(BOB); + assertEq(ownerFacet.owner(), BOB); + + // Third transfer + vm.prank(BOB); + ownerFacet.transferOwnership(NEW_OWNER); + assertEq(ownerFacet.owner(), NEW_OWNER); + } + + function test_TransferOwnership_ToSelf() public { + vm.prank(INITIAL_OWNER); + ownerFacet.transferOwnership(INITIAL_OWNER); + + assertEq(ownerFacet.owner(), INITIAL_OWNER); + } + + function test_RevertWhen_TransferOwnership_CalledByNonOwner() public { + vm.expectRevert(OwnerFacet.OwnerUnauthorizedAccount.selector); + vm.prank(ALICE); + ownerFacet.transferOwnership(ALICE); + } + + function test_RevertWhen_TransferOwnership_CalledByPreviousOwner() public { + vm.prank(INITIAL_OWNER); + ownerFacet.transferOwnership(NEW_OWNER); + + vm.expectRevert(OwnerFacet.OwnerUnauthorizedAccount.selector); + vm.prank(INITIAL_OWNER); + ownerFacet.transferOwnership(ALICE); + } + + // ============================================ + // Renounce Ownership Tests + // ============================================ + + function test_RenounceOwnership_SetsOwnerToZero() public { + vm.prank(INITIAL_OWNER); + ownerFacet.transferOwnership(ZERO_ADDRESS); + + assertEq(ownerFacet.owner(), ZERO_ADDRESS); + } + + function test_RenounceOwnership_EmitsEvent() public { + vm.expectEmit(true, true, false, true); + emit OwnershipTransferred(INITIAL_OWNER, ZERO_ADDRESS); + + vm.prank(INITIAL_OWNER); + ownerFacet.transferOwnership(ZERO_ADDRESS); + } + + function test_RenounceOwnership_PreventsAllFurtherTransfers() public { + vm.prank(INITIAL_OWNER); + ownerFacet.transferOwnership(ZERO_ADDRESS); + + // ALICE (non-owner) cannot transfer + vm.expectRevert(OwnerFacet.OwnerUnauthorizedAccount.selector); + vm.prank(ALICE); + ownerFacet.transferOwnership(BOB); + + // BOB (non-owner) cannot transfer + vm.expectRevert(OwnerFacet.OwnerUnauthorizedAccount.selector); + vm.prank(BOB); + ownerFacet.transferOwnership(ALICE); + + // Note: Zero address cannot make any calls since it has no private key + } + + // ============================================ + // Edge Cases + // ============================================ + + function test_TransferOwnership_EmitsCorrectPreviousOwner() public { + vm.prank(INITIAL_OWNER); + vm.expectEmit(true, true, false, true); + emit OwnershipTransferred(INITIAL_OWNER, ALICE); + ownerFacet.transferOwnership(ALICE); + + vm.prank(ALICE); + vm.expectEmit(true, true, false, true); + emit OwnershipTransferred(ALICE, BOB); + ownerFacet.transferOwnership(BOB); + } + + function test_StorageSlot_Consistency() public { + bytes32 expectedSlot = keccak256("compose.owner"); + + vm.prank(INITIAL_OWNER); + ownerFacet.transferOwnership(NEW_OWNER); + + // Read directly from storage + bytes32 storedValue = vm.load(address(ownerFacet), expectedSlot); + address storedOwner = address(uint160(uint256(storedValue))); + + assertEq(storedOwner, NEW_OWNER); + assertEq(ownerFacet.owner(), NEW_OWNER); + } + + // ============================================ + // Fuzz Tests + // ============================================ + + function test_Fuzz_TransferOwnership(address newOwner) public { + vm.prank(INITIAL_OWNER); + ownerFacet.transferOwnership(newOwner); + + assertEq(ownerFacet.owner(), newOwner); + } + + function test_Fuzz_SequentialTransfers(address owner1, address owner2, address owner3) public { + vm.assume(owner1 != address(0)); + vm.assume(owner2 != address(0)); + vm.assume(owner3 != address(0)); + + vm.prank(INITIAL_OWNER); + ownerFacet.transferOwnership(owner1); + assertEq(ownerFacet.owner(), owner1); + + vm.prank(owner1); + ownerFacet.transferOwnership(owner2); + assertEq(ownerFacet.owner(), owner2); + + vm.prank(owner2); + ownerFacet.transferOwnership(owner3); + assertEq(ownerFacet.owner(), owner3); + } + + function test_Fuzz_RevertWhen_UnauthorizedCaller(address caller, address target) public { + vm.assume(caller != INITIAL_OWNER); + + vm.prank(caller); + vm.expectRevert(OwnerFacet.OwnerUnauthorizedAccount.selector); + ownerFacet.transferOwnership(target); + } + + function test_Fuzz_RenouncePreventsAllTransfers(address caller, address target) public { + vm.assume(caller != address(0)); + + // Renounce ownership + vm.prank(INITIAL_OWNER); + ownerFacet.transferOwnership(ZERO_ADDRESS); + + // No one can transfer anymore + vm.prank(caller); + vm.expectRevert(OwnerFacet.OwnerUnauthorizedAccount.selector); + ownerFacet.transferOwnership(target); + } + + // ============================================ + // Gas Tests + // ============================================ + + function test_Gas_TransferOwnership() public { + uint256 gasBefore = gasleft(); + vm.prank(INITIAL_OWNER); + ownerFacet.transferOwnership(NEW_OWNER); + uint256 gasUsed = gasBefore - gasleft(); + + // Log gas usage for optimization tracking + console2.log("Gas used for transferOwnership:", gasUsed); + // Should be relatively low since it's just storage updates + assertTrue(gasUsed < 50000, "Transfer ownership uses too much gas"); + } + + function test_Gas_OwnerGetter() public view { + uint256 gasBefore = gasleft(); + ownerFacet.owner(); + uint256 gasUsed = gasBefore - gasleft(); + + console2.log("Gas used for owner():", gasUsed); + // Should be very low since it's just a storage read + assertTrue(gasUsed < 10000, "Owner getter uses too much gas"); + } +} diff --git a/test/access/Owner/harnesses/LibOwnerHarness.sol b/test/access/Owner/harnesses/LibOwnerHarness.sol new file mode 100644 index 00000000..21b74bac --- /dev/null +++ b/test/access/Owner/harnesses/LibOwnerHarness.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {LibOwner} from "../../../../src/access/Owner/LibOwner.sol"; + +/// @title LibOwner Test Harness +/// @notice Exposes internal LibOwner functions as external for testing +contract LibOwnerHarness { + /// @notice Initialize the owner (for testing) + function initialize(address _owner) external { + LibOwner.OwnerStorage storage s = LibOwner.getStorage(); + s.owner = _owner; + } + + /// @notice Get the current owner + function owner() external view returns (address) { + return LibOwner.owner(); + } + + /// @notice Transfer ownership + function transferOwnership(address _newOwner) external { + LibOwner.transferOwnership(_newOwner); + } + + /// @notice Renounce ownership (new function added by maintainer) + function renounceOwnership() external { + LibOwner.renounceOwnership(); + } + + /// @notice Check if caller is owner (new function added by maintainer) + function requireOwner() external view { + LibOwner.requireOwner(); + } + + /// @notice Get storage directly (for testing storage consistency) + function getStorageOwner() external view returns (address) { + return LibOwner.getStorage().owner; + } + + /// @notice Force set owner to zero without checks (for testing renounced state) + function forceRenounce() external { + LibOwner.OwnerStorage storage s = LibOwner.getStorage(); + s.owner = address(0); + } +} diff --git a/test/access/Owner/harnesses/OwnerFacetHarness.sol b/test/access/Owner/harnesses/OwnerFacetHarness.sol new file mode 100644 index 00000000..cca62dc0 --- /dev/null +++ b/test/access/Owner/harnesses/OwnerFacetHarness.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {OwnerFacet} from "../../../../src/access/Owner/OwnerFacet.sol"; + +/// @title OwnerFacet Test Harness +/// @notice Extends OwnerFacet with initialization and test-specific functions +contract OwnerFacetHarness is OwnerFacet { + /// @notice Initialize the owner for testing + /// @dev This function is only for testing purposes + function initialize(address _owner) external { + OwnerStorage storage s = getStorage(); + s.owner = _owner; + } + + /// @notice Force set owner without any checks (for testing edge cases) + /// @dev This bypasses all access control for testing purposes + function forceSetOwner(address _owner) external { + OwnerStorage storage s = getStorage(); + s.owner = _owner; + } + + /// @notice Get the raw storage owner value (for testing storage consistency) + function getStorageOwner() external view returns (address) { + return getStorage().owner; + } +}