diff --git a/test/unit/ActivationRegistry/activate_revertOrder.t.sol b/test/unit/ActivationRegistry/activate_revertOrder.t.sol new file mode 100644 index 00000000..91874a61 --- /dev/null +++ b/test/unit/ActivationRegistry/activate_revertOrder.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IActivationRegistry} from "src/interfaces/IActivationRegistry.sol"; + +import {ActivationRegistryTest} from "test/lib/ActivationRegistryTest.sol"; + +contract ActivationRegistryActivateRevertOrderTest is ActivationRegistryTest { + /// @notice Verifies Unauthorized fires before AlreadyActivated when caller is not admin + /// and the feature is already activated + /// @dev Revert order: access-control check precedes state validation; a non-admin caller + /// never reaches the AlreadyActivated guard regardless of feature state. + /// Fuzz: any caller that is not the activationAdmin, any feature id. + function test_activate_revertOrder_unauthorized_beats_alreadyActivated(address caller, bytes32 feature) public { + _assumeValidCaller(caller); + vm.assume(caller != activationAdmin); + + // Activate the feature as admin so it is already activated, establishing + // the precondition that AlreadyActivated *could* fire for an admin caller. + vm.prank(activationAdmin); + activationRegistry.activate(feature); + + // A non-admin caller must see Unauthorized, not AlreadyActivated, + // because the access-control check in activate fires first. + vm.expectRevert(abi.encodeWithSelector(IActivationRegistry.Unauthorized.selector, caller)); + vm.prank(caller); + activationRegistry.activate(feature); + } +} diff --git a/test/unit/ActivationRegistry/deactivate_revertOrder.t.sol b/test/unit/ActivationRegistry/deactivate_revertOrder.t.sol new file mode 100644 index 00000000..95e17b13 --- /dev/null +++ b/test/unit/ActivationRegistry/deactivate_revertOrder.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IActivationRegistry} from "src/interfaces/IActivationRegistry.sol"; + +import {ActivationRegistryTest} from "test/lib/ActivationRegistryTest.sol"; + +contract ActivationRegistryDeactivateRevertOrderTest is ActivationRegistryTest { + /// @notice Verifies Unauthorized fires before FeatureNotActivated when caller is not admin + /// and the feature is not currently activated + /// @dev Revert order: access-control check precedes state validation; a non-admin caller + /// never reaches the FeatureNotActivated guard regardless of feature state. + /// Fuzz: any caller that is not the activationAdmin, any feature id. + function test_deactivate_revertOrder_unauthorized_beats_featureNotActivated(address caller, bytes32 feature) + public + { + _assumeValidCaller(caller); + vm.assume(caller != activationAdmin); + + // The feature is not activated (default state), establishing the precondition + // that FeatureNotActivated *could* fire for an admin caller. + + // A non-admin caller must see Unauthorized, not FeatureNotActivated, + // because the access-control check in deactivate fires first. + vm.expectRevert(abi.encodeWithSelector(IActivationRegistry.Unauthorized.selector, caller)); + vm.prank(caller); + activationRegistry.deactivate(feature); + } +}