diff --git a/src/hooks/DefaultSecurityHook.sol b/src/hooks/DefaultSecurityHook.sol new file mode 100644 index 0000000..d65bddc --- /dev/null +++ b/src/hooks/DefaultSecurityHook.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IHook, IModule} from "src/interfaces/IERC7579Modules.sol"; +import {MODULE_TYPE_HOOK} from "src/types/Constants.sol"; +import {LibERC7579} from "solady/accounts/LibERC7579.sol"; +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {IERC721} from "openzeppelin-contracts/contracts/token/ERC721/IERC721.sol"; +import {IERC1155} from "openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sol"; + +/// @title DefaultSecurityHook +/// @notice A default security hook for ERC-7579 smart accounts that blocks dangerous operations +/// by default and provides a per-account (target, selector[]) allowlisting mechanism. +/// @author taek +contract DefaultSecurityHook is IHook { + // ========== Errors ========== + + error DelegateCallNotAllowed(); + error SelfCallNotAllowed(); + error ModuleCallNotAllowed(address target); + error ETHTransferNotAllowed(address target, uint256 value); + error TokenTransferNotAllowed(address target, bytes4 selector); + error Unauthorized(); + error UnsupportedCallType(); + + // ========== Events ========== + + event AllowlistSet(address indexed account, address indexed target, bytes4[] selectors); + event AllowlistRemoved(address indexed account, address indexed target); + event Initialized(address indexed account); + event Uninitialized(address indexed account); + + // ========== Types ========== + + struct AllowlistConfig { + address target; + bytes4[] selectors; + } + + struct AllowlistEntry { + bool allowed; + bool allSelectorsAllowed; + bytes4[] selectorList; + mapping(bytes4 => bool) selectors; + } + + // ========== Storage ========== + + mapping(address account => mapping(address target => AllowlistEntry)) internal allowlist; + mapping(address account => address[]) internal allowlistedTargets; + mapping(address account => bool) internal initialized; + + // ========== Blocked Selectors ========== + + // ERC-20 + bytes4 internal constant TRANSFER = IERC20.transfer.selector; + bytes4 internal constant APPROVE = IERC20.approve.selector; + bytes4 internal constant TRANSFER_FROM = IERC20.transferFrom.selector; + bytes4 internal constant INCREASE_ALLOWANCE = bytes4(keccak256("increaseAllowance(address,uint256)")); + bytes4 internal constant DECREASE_ALLOWANCE = bytes4(keccak256("decreaseAllowance(address,uint256)")); + + // ERC-721 (unique selectors not already covered above) + // safeTransferFrom is overloaded in IERC721, so we compute selectors from signatures directly + bytes4 internal constant SAFE_TRANSFER_FROM = bytes4(keccak256("safeTransferFrom(address,address,uint256)")); + bytes4 internal constant SAFE_TRANSFER_FROM_WITH_DATA = + bytes4(keccak256("safeTransferFrom(address,address,uint256,bytes)")); + bytes4 internal constant SET_APPROVAL_FOR_ALL = IERC721.setApprovalForAll.selector; + + // ERC-1155 + bytes4 internal constant SAFE_TRANSFER_FROM_1155 = IERC1155.safeTransferFrom.selector; + bytes4 internal constant SAFE_BATCH_TRANSFER_FROM = IERC1155.safeBatchTransferFrom.selector; + + // Gas stipend for isModuleType static call + uint256 internal constant MODULE_CHECK_GAS = 30_000; + + // ========== IModule ========== + + function onInstall(bytes calldata data) external payable override { + if (initialized[msg.sender]) revert AlreadyInitialized(msg.sender); + initialized[msg.sender] = true; + + if (data.length > 0) { + AllowlistConfig[] memory configs = abi.decode(data, (AllowlistConfig[])); + for (uint256 i; i < configs.length; i++) { + _setAllowlist(msg.sender, configs[i].target, configs[i].selectors); + } + } + + emit Initialized(msg.sender); + } + + function onUninstall(bytes calldata) external payable override { + if (!initialized[msg.sender]) revert NotInitialized(msg.sender); + + // Clear ALL allowlisted targets for the account + address[] storage targets = allowlistedTargets[msg.sender]; + for (uint256 i; i < targets.length; i++) { + _clearAllowlist(msg.sender, targets[i]); + } + delete allowlistedTargets[msg.sender]; + + initialized[msg.sender] = false; + emit Uninitialized(msg.sender); + } + + function isModuleType(uint256 moduleTypeId) external pure override returns (bool) { + return moduleTypeId == MODULE_TYPE_HOOK; + } + + // ========== IHook ========== + + function preCheck(address, uint256, bytes calldata msgData) external payable override returns (bytes memory) { + // msgData layout: [0:4] execute selector, [4:36] mode, [36:..] executionData + bytes32 mode = bytes32(msgData[4:36]); + bytes1 callType = LibERC7579.getCallType(mode); + + if (callType == LibERC7579.CALLTYPE_DELEGATECALL) { + revert DelegateCallNotAllowed(); + } + + // Extract executionData from the ABI-encoded msgData + // msgData[4:] is (bytes32 mode, bytes executionData) + // The bytes param is ABI-encoded with an offset at [36:68] then length + data + bytes calldata executionData; + assembly { + let offsetPos := add(msgData.offset, 36) + let offset := calldataload(offsetPos) + let dataStart := add(add(msgData.offset, 4), offset) + executionData.length := calldataload(dataStart) + executionData.offset := add(dataStart, 0x20) + } + + if (callType == LibERC7579.CALLTYPE_SINGLE) { + (address target, uint256 value, bytes calldata data) = LibERC7579.decodeSingle(executionData); + _checkCall(target, value, data); + } else if (callType == LibERC7579.CALLTYPE_BATCH) { + bytes32[] calldata pointers = LibERC7579.decodeBatch(executionData); + for (uint256 i; i < pointers.length; i++) { + (address target, uint256 value, bytes calldata data) = LibERC7579.getExecution(pointers, i); + _checkCall(target, value, data); + } + } else { + revert UnsupportedCallType(); + } + + return hex""; + } + + function postCheck(bytes calldata) external payable override {} + + // ========== Allowlist Management ========== + + function setAllowlist(address target, bytes4[] calldata selectors) external { + if (!initialized[msg.sender]) revert Unauthorized(); + _setAllowlist(msg.sender, target, selectors); + emit AllowlistSet(msg.sender, target, selectors); + } + + function removeAllowlist(address target) external { + if (!initialized[msg.sender]) revert Unauthorized(); + _clearAllowlist(msg.sender, target); + emit AllowlistRemoved(msg.sender, target); + } + + // ========== View ========== + + function isInitialized(address account) external view returns (bool) { + return initialized[account]; + } + + function isAllowlisted(address account, address target) external view returns (bool) { + return allowlist[account][target].allowed; + } + + function isSelectorAllowed(address account, address target, bytes4 selector) external view returns (bool) { + AllowlistEntry storage entry = allowlist[account][target]; + if (!entry.allowed) return false; + if (entry.allSelectorsAllowed) return true; + return entry.selectors[selector]; + } + + // ========== Internal ========== + + function _checkCall(address target, uint256 value, bytes calldata data) internal view { + // Check allowlist first + AllowlistEntry storage entry = allowlist[msg.sender][target]; + if (entry.allowed) { + if (entry.allSelectorsAllowed) return; + if (data.length >= 4 && entry.selectors[bytes4(data[:4])]) return; + } + + // Self-call check + if (target == msg.sender) revert SelfCallNotAllowed(); + + // Module check + if (_isModule(target)) revert ModuleCallNotAllowed(target); + + // ETH transfer check + if (value > 0) revert ETHTransferNotAllowed(target, value); + + // Blocked selector check + if (data.length >= 4) { + bytes4 selector = bytes4(data[:4]); + if (_isBlockedSelector(selector)) revert TokenTransferNotAllowed(target, selector); + } + } + + function _isModule(address target) internal view returns (bool) { + (bool success,) = + target.staticcall{gas: MODULE_CHECK_GAS}(abi.encodeWithSelector(IModule.isModuleType.selector, uint256(0))); + return success; + } + + function _isBlockedSelector(bytes4 selector) internal pure returns (bool) { + return selector == TRANSFER || selector == APPROVE || selector == TRANSFER_FROM + || selector == INCREASE_ALLOWANCE || selector == DECREASE_ALLOWANCE || selector == SAFE_TRANSFER_FROM + || selector == SAFE_TRANSFER_FROM_WITH_DATA || selector == SET_APPROVAL_FOR_ALL + || selector == SAFE_TRANSFER_FROM_1155 || selector == SAFE_BATCH_TRANSFER_FROM; + } + + function _setAllowlist(address account, address target, bytes4[] memory selectors) internal { + AllowlistEntry storage entry = allowlist[account][target]; + + // Track target for cleanup on uninstall (S-03) + if (!entry.allowed) { + allowlistedTargets[account].push(target); + } + + // Clear stale selectors from the mapping (S-01) + bytes4[] storage oldSelectors = entry.selectorList; + for (uint256 i; i < oldSelectors.length; i++) { + entry.selectors[oldSelectors[i]] = false; + } + delete entry.selectorList; + + entry.allowed = true; + if (selectors.length == 0) { + entry.allSelectorsAllowed = true; + } else { + entry.allSelectorsAllowed = false; + for (uint256 i; i < selectors.length; i++) { + entry.selectors[selectors[i]] = true; + entry.selectorList.push(selectors[i]); + } + } + } + + function _clearAllowlist(address account, address target) internal { + AllowlistEntry storage entry = allowlist[account][target]; + + // Clear all tracked selectors from the mapping + bytes4[] storage oldSelectors = entry.selectorList; + for (uint256 i; i < oldSelectors.length; i++) { + entry.selectors[oldSelectors[i]] = false; + } + delete entry.selectorList; + + entry.allowed = false; + entry.allSelectorsAllowed = false; + } +} diff --git a/test/btt/DefaultSecurityHook.t.sol b/test/btt/DefaultSecurityHook.t.sol new file mode 100644 index 0000000..9ef35bd --- /dev/null +++ b/test/btt/DefaultSecurityHook.t.sol @@ -0,0 +1,908 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {DefaultSecurityHook} from "src/hooks/DefaultSecurityHook.sol"; +import {IModule} from "src/interfaces/IERC7579Modules.sol"; +import {MODULE_TYPE_HOOK} from "src/types/Constants.sol"; +import {LibERC7579} from "solady/accounts/LibERC7579.sol"; +import {IERC7579Execution, Execution} from "openzeppelin-contracts/contracts/interfaces/draft-IERC7579.sol"; +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {IERC721} from "openzeppelin-contracts/contracts/token/ERC721/IERC721.sol"; +import {IERC1155} from "openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sol"; +import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import {ERC1155} from "openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol"; + +/// @dev Mock module that responds to isModuleType without reverting. +contract MockModule is IModule { + function onInstall(bytes calldata) external payable override {} + function onUninstall(bytes calldata) external payable override {} + + function isModuleType(uint256) external pure override returns (bool) { + return true; + } +} + +/// @dev A contract that does NOT implement isModuleType (staticcall will revert). +contract NonModuleContract { + function doSomething() external pure returns (uint256) { + return 42; + } +} + +contract MockERC20 is ERC20 { + constructor() ERC20("MockToken", "MCK") {} + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} + +contract MockERC721 is ERC721 { + constructor() ERC721("MockNFT", "MNFT") {} + + function mint(address to, uint256 tokenId) external { + _mint(to, tokenId); + } +} + +contract MockERC1155 is ERC1155 { + constructor() ERC1155("https://mock.uri/{id}") {} + + function mint(address to, uint256 id, uint256 amount) external { + _mint(to, id, amount, ""); + } +} + +contract DefaultSecurityHookBTTTest is Test { + DefaultSecurityHook public hook; + MockModule public mockModule; + NonModuleContract public nonModule; + MockERC20 public mockERC20; + MockERC721 public mockERC721; + MockERC1155 public mockERC1155; + + address public account; + address public randomTarget; + address public recipient; + + // Blocked selectors — derived from interfaces + // ERC-20 + bytes4 internal constant TRANSFER = IERC20.transfer.selector; + bytes4 internal constant APPROVE = IERC20.approve.selector; + bytes4 internal constant TRANSFER_FROM = IERC20.transferFrom.selector; + bytes4 internal constant INCREASE_ALLOWANCE = bytes4(keccak256("increaseAllowance(address,uint256)")); + bytes4 internal constant DECREASE_ALLOWANCE = bytes4(keccak256("decreaseAllowance(address,uint256)")); + + // ERC-721 (safeTransferFrom is overloaded, so compute from signatures) + bytes4 internal constant SAFE_TRANSFER_FROM = bytes4(keccak256("safeTransferFrom(address,address,uint256)")); + bytes4 internal constant SAFE_TRANSFER_FROM_WITH_DATA = + bytes4(keccak256("safeTransferFrom(address,address,uint256,bytes)")); + bytes4 internal constant SET_APPROVAL_FOR_ALL = IERC721.setApprovalForAll.selector; + + // ERC-1155 + bytes4 internal constant SAFE_TRANSFER_FROM_1155 = IERC1155.safeTransferFrom.selector; + bytes4 internal constant SAFE_BATCH_TRANSFER_FROM = IERC1155.safeBatchTransferFrom.selector; + + // An unblocked selector + bytes4 internal constant BALANCE_OF = IERC20.balanceOf.selector; + + event AllowlistSet(address indexed account, address indexed target, bytes4[] selectors); + event AllowlistRemoved(address indexed account, address indexed target); + event Initialized(address indexed account); + event Uninitialized(address indexed account); + + function setUp() public { + hook = new DefaultSecurityHook(); + mockModule = new MockModule(); + nonModule = new NonModuleContract(); + mockERC20 = new MockERC20(); + mockERC721 = new MockERC721(); + mockERC1155 = new MockERC1155(); + account = address(0xACC0); + randomTarget = address(nonModule); + recipient = address(0xBEEF); + } + + // ==================== Helper Functions ==================== + + function _install() internal { + vm.prank(account); + hook.onInstall(""); + } + + function _installWithConfig(DefaultSecurityHook.AllowlistConfig[] memory configs) internal { + vm.prank(account); + hook.onInstall(abi.encode(configs)); + } + + /// @dev Build msgData for a single-mode preCheck call. + function _singleMsgData(address target, uint256 value, bytes memory data) internal pure returns (bytes memory) { + bytes32 mode = + LibERC7579.encodeMode(LibERC7579.CALLTYPE_SINGLE, LibERC7579.EXECTYPE_DEFAULT, bytes4(0), bytes22(0)); + bytes memory executionData = abi.encodePacked(target, value, data); + return abi.encodeWithSelector(IERC7579Execution.execute.selector, mode, executionData); + } + + /// @dev Build msgData for a batch-mode preCheck call. + function _batchMsgData(Execution[] memory executions) internal pure returns (bytes memory) { + bytes32 mode = + LibERC7579.encodeMode(LibERC7579.CALLTYPE_BATCH, LibERC7579.EXECTYPE_DEFAULT, bytes4(0), bytes22(0)); + bytes memory executionData = abi.encode(executions); + return abi.encodeWithSelector(IERC7579Execution.execute.selector, mode, executionData); + } + + /// @dev Build msgData for a delegatecall-mode preCheck call. + function _delegatecallMsgData() internal pure returns (bytes memory) { + bytes32 mode = + LibERC7579.encodeMode(LibERC7579.CALLTYPE_DELEGATECALL, LibERC7579.EXECTYPE_DEFAULT, bytes4(0), bytes22(0)); + bytes memory executionData = abi.encodePacked(address(0), uint256(0)); + return abi.encodeWithSelector(IERC7579Execution.execute.selector, mode, executionData); + } + + /// @dev Helper to call preCheck from account context. + function _preCheck(bytes memory msgData) internal returns (bytes memory) { + vm.prank(account); + return hook.preCheck(address(0), 0, msgData); + } + + // ==================== onInstall Tests ==================== + + modifier whenCallingOnInstall() { + _; + } + + function test_GivenAccountIsAlreadyInitialized() external whenCallingOnInstall { + // it should revert with AlreadyInitialized + _install(); + + vm.prank(account); + vm.expectRevert(abi.encodeWithSelector(IModule.AlreadyInitialized.selector, account)); + hook.onInstall(""); + } + + function test_GivenDataIsEmpty() external whenCallingOnInstall { + // it should mark account as initialized + // it should emit Initialized event + vm.prank(account); + vm.expectEmit(true, false, false, false); + emit Initialized(account); + hook.onInstall(""); + + assertTrue(hook.isInitialized(account), "Account should be initialized"); + } + + function test_GivenDataContainsAllowlistConfigs() external whenCallingOnInstall { + // it should mark account as initialized + // it should set allowlist entries for each config + // it should emit Initialized event + DefaultSecurityHook.AllowlistConfig[] memory configs = new DefaultSecurityHook.AllowlistConfig[](2); + + bytes4[] memory selectors1 = new bytes4[](1); + selectors1[0] = TRANSFER; + configs[0] = DefaultSecurityHook.AllowlistConfig({target: randomTarget, selectors: selectors1}); + + bytes4[] memory selectors2 = new bytes4[](0); + configs[1] = DefaultSecurityHook.AllowlistConfig({target: address(0xBEEF), selectors: selectors2}); + + vm.prank(account); + vm.expectEmit(true, false, false, false); + emit Initialized(account); + hook.onInstall(abi.encode(configs)); + + assertTrue(hook.isInitialized(account), "Account should be initialized"); + assertTrue(hook.isAllowlisted(account, randomTarget), "randomTarget should be allowlisted"); + assertTrue(hook.isSelectorAllowed(account, randomTarget, TRANSFER), "TRANSFER selector should be allowed"); + assertFalse(hook.isSelectorAllowed(account, randomTarget, APPROVE), "APPROVE selector should not be allowed"); + assertTrue(hook.isAllowlisted(account, address(0xBEEF)), "0xBEEF should be allowlisted"); + assertTrue( + hook.isSelectorAllowed(account, address(0xBEEF), TRANSFER), "All selectors should be allowed for 0xBEEF" + ); + } + + // ==================== onUninstall Tests ==================== + + modifier whenCallingOnUninstall() { + _; + } + + function test_GivenAccountIsNotInitialized() external whenCallingOnUninstall { + // it should revert with NotInitialized + vm.prank(account); + vm.expectRevert(abi.encodeWithSelector(IModule.NotInitialized.selector, account)); + hook.onUninstall(""); + } + + function test_GivenDataIsEmpty_WhenCallingOnUninstall() external whenCallingOnUninstall { + // it should mark account as not initialized + // it should emit Uninitialized event + _install(); + + vm.prank(account); + vm.expectEmit(true, false, false, false); + emit Uninitialized(account); + hook.onUninstall(""); + + assertFalse(hook.isInitialized(account), "Account should not be initialized"); + } + + function test_GivenDataContainsTargetsToClean() external whenCallingOnUninstall { + // it should clear all allowlist entries automatically + // it should mark account as not initialized + // it should emit Uninitialized event + DefaultSecurityHook.AllowlistConfig[] memory configs = new DefaultSecurityHook.AllowlistConfig[](1); + bytes4[] memory sels = new bytes4[](0); + configs[0] = DefaultSecurityHook.AllowlistConfig({target: randomTarget, selectors: sels}); + _installWithConfig(configs); + + assertTrue(hook.isAllowlisted(account, randomTarget), "Should be allowlisted before uninstall"); + + vm.prank(account); + vm.expectEmit(true, false, false, false); + emit Uninitialized(account); + hook.onUninstall(""); + + assertFalse(hook.isInitialized(account), "Account should not be initialized"); + assertFalse(hook.isAllowlisted(account, randomTarget), "Allowlist should be cleared"); + } + + // ==================== isModuleType Tests ==================== + + modifier whenCallingIsModuleType() { + _; + } + + function test_GivenModuleTypeIdIsMODULE_TYPE_HOOK() external whenCallingIsModuleType { + // it should return true + assertTrue(hook.isModuleType(MODULE_TYPE_HOOK), "Should return true for MODULE_TYPE_HOOK"); + } + + function test_GivenModuleTypeIdIsNotMODULE_TYPE_HOOK() external whenCallingIsModuleType { + // it should return false + assertFalse(hook.isModuleType(1), "Should return false for MODULE_TYPE_VALIDATOR"); + assertFalse(hook.isModuleType(0), "Should return false for 0"); + assertFalse(hook.isModuleType(999), "Should return false for 999"); + } + + // ==================== preCheck DELEGATECALL Tests ==================== + + function test_WhenCallingPreCheckWithDELEGATECALLMode() external { + // it should revert with DelegateCallNotAllowed + bytes memory msgData = _delegatecallMsgData(); + + vm.prank(account); + vm.expectRevert(DefaultSecurityHook.DelegateCallNotAllowed.selector); + hook.preCheck(address(0), 0, msgData); + } + + // ==================== preCheck SINGLE Tests ==================== + + modifier whenCallingPreCheckWithSINGLEMode() { + _; + } + + function test_GivenTargetIsAllowlistedWithAllSelectors() external whenCallingPreCheckWithSINGLEMode { + // it should return empty bytes + DefaultSecurityHook.AllowlistConfig[] memory configs = new DefaultSecurityHook.AllowlistConfig[](1); + bytes4[] memory sels = new bytes4[](0); + configs[0] = DefaultSecurityHook.AllowlistConfig({target: address(mockERC20), selectors: sels}); + _installWithConfig(configs); + + // Even a blocked selector should pass when all selectors are allowed + bytes memory callData = abi.encodeCall(IERC20.transfer, (recipient, 100)); + bytes memory msgData = _singleMsgData(address(mockERC20), 0, callData); + bytes memory result = _preCheck(msgData); + assertEq(result, hex"", "Should return empty bytes"); + } + + function test_GivenTargetIsAllowlistedWithSpecificSelectorMatchingCall() + external + whenCallingPreCheckWithSINGLEMode + { + // it should return empty bytes + DefaultSecurityHook.AllowlistConfig[] memory configs = new DefaultSecurityHook.AllowlistConfig[](1); + bytes4[] memory sels = new bytes4[](1); + sels[0] = TRANSFER; + configs[0] = DefaultSecurityHook.AllowlistConfig({target: address(mockERC20), selectors: sels}); + _installWithConfig(configs); + + bytes memory callData = abi.encodeCall(IERC20.transfer, (recipient, 100)); + bytes memory msgData = _singleMsgData(address(mockERC20), 0, callData); + bytes memory result = _preCheck(msgData); + assertEq(result, hex"", "Should return empty bytes"); + } + + function test_GivenTargetIsAllowlistedWithSpecificSelectorNotMatchingCall() + external + whenCallingPreCheckWithSINGLEMode + { + // it should revert with TokenTransferNotAllowed + // Allowlist TRANSFER for the token, but call APPROVE (also blocked) + DefaultSecurityHook.AllowlistConfig[] memory configs = new DefaultSecurityHook.AllowlistConfig[](1); + bytes4[] memory sels = new bytes4[](1); + sels[0] = TRANSFER; + configs[0] = DefaultSecurityHook.AllowlistConfig({target: address(mockERC20), selectors: sels}); + _installWithConfig(configs); + + bytes memory callData = abi.encodeCall(IERC20.approve, (recipient, 100)); + bytes memory msgData = _singleMsgData(address(mockERC20), 0, callData); + vm.prank(account); + vm.expectRevert( + abi.encodeWithSelector(DefaultSecurityHook.TokenTransferNotAllowed.selector, address(mockERC20), APPROVE) + ); + hook.preCheck(address(0), 0, msgData); + } + + function test_GivenTargetIsSelf() external whenCallingPreCheckWithSINGLEMode { + // it should revert with SelfCallNotAllowed + _install(); + bytes memory msgData = _singleMsgData(account, 0, abi.encodeWithSelector(BALANCE_OF, address(1))); + vm.prank(account); + vm.expectRevert(DefaultSecurityHook.SelfCallNotAllowed.selector); + hook.preCheck(address(0), 0, msgData); + } + + function test_GivenTargetIsAModule() external whenCallingPreCheckWithSINGLEMode { + // it should revert with ModuleCallNotAllowed + _install(); + bytes memory msgData = _singleMsgData(address(mockModule), 0, abi.encodeWithSelector(BALANCE_OF, address(1))); + vm.prank(account); + vm.expectRevert(abi.encodeWithSelector(DefaultSecurityHook.ModuleCallNotAllowed.selector, address(mockModule))); + hook.preCheck(address(0), 0, msgData); + } + + function test_GivenValueIsGreaterThanZero() external whenCallingPreCheckWithSINGLEMode { + // it should revert with ETHTransferNotAllowed + _install(); + bytes memory msgData = _singleMsgData(randomTarget, 1 ether, hex""); + vm.prank(account); + vm.expectRevert( + abi.encodeWithSelector(DefaultSecurityHook.ETHTransferNotAllowed.selector, randomTarget, 1 ether) + ); + hook.preCheck(address(0), 0, msgData); + } + + function test_GivenSelectorIsERC20Transfer() external whenCallingPreCheckWithSINGLEMode { + // it should revert with TokenTransferNotAllowed + _install(); + bytes memory callData = abi.encodeCall(IERC20.transfer, (recipient, 100)); + bytes memory msgData = _singleMsgData(address(mockERC20), 0, callData); + vm.prank(account); + vm.expectRevert( + abi.encodeWithSelector(DefaultSecurityHook.TokenTransferNotAllowed.selector, address(mockERC20), TRANSFER) + ); + hook.preCheck(address(0), 0, msgData); + } + + function test_GivenSelectorIsERC20Approve() external whenCallingPreCheckWithSINGLEMode { + // it should revert with TokenTransferNotAllowed + _install(); + bytes memory callData = abi.encodeCall(IERC20.approve, (recipient, 100)); + bytes memory msgData = _singleMsgData(address(mockERC20), 0, callData); + vm.prank(account); + vm.expectRevert( + abi.encodeWithSelector(DefaultSecurityHook.TokenTransferNotAllowed.selector, address(mockERC20), APPROVE) + ); + hook.preCheck(address(0), 0, msgData); + } + + function test_GivenSelectorIsERC20TransferFrom() external whenCallingPreCheckWithSINGLEMode { + // it should revert with TokenTransferNotAllowed + _install(); + bytes memory callData = abi.encodeCall(IERC20.transferFrom, (account, recipient, 100)); + bytes memory msgData = _singleMsgData(address(mockERC20), 0, callData); + vm.prank(account); + vm.expectRevert( + abi.encodeWithSelector( + DefaultSecurityHook.TokenTransferNotAllowed.selector, address(mockERC20), TRANSFER_FROM + ) + ); + hook.preCheck(address(0), 0, msgData); + } + + function test_GivenSelectorIsERC20IncreaseAllowance() external whenCallingPreCheckWithSINGLEMode { + // it should revert with TokenTransferNotAllowed + _install(); + bytes memory callData = abi.encodeWithSelector(INCREASE_ALLOWANCE, recipient, 100); + bytes memory msgData = _singleMsgData(address(mockERC20), 0, callData); + vm.prank(account); + vm.expectRevert( + abi.encodeWithSelector( + DefaultSecurityHook.TokenTransferNotAllowed.selector, address(mockERC20), INCREASE_ALLOWANCE + ) + ); + hook.preCheck(address(0), 0, msgData); + } + + function test_GivenSelectorIsERC20DecreaseAllowance() external whenCallingPreCheckWithSINGLEMode { + // it should revert with TokenTransferNotAllowed + _install(); + bytes memory callData = abi.encodeWithSelector(DECREASE_ALLOWANCE, recipient, 100); + bytes memory msgData = _singleMsgData(address(mockERC20), 0, callData); + vm.prank(account); + vm.expectRevert( + abi.encodeWithSelector( + DefaultSecurityHook.TokenTransferNotAllowed.selector, address(mockERC20), DECREASE_ALLOWANCE + ) + ); + hook.preCheck(address(0), 0, msgData); + } + + function test_GivenSelectorIsERC721SafeTransferFrom() external whenCallingPreCheckWithSINGLEMode { + // it should revert with TokenTransferNotAllowed + _install(); + bytes memory callData = abi.encodeWithSelector(SAFE_TRANSFER_FROM, account, recipient, 1); + bytes memory msgData = _singleMsgData(address(mockERC721), 0, callData); + vm.prank(account); + vm.expectRevert( + abi.encodeWithSelector( + DefaultSecurityHook.TokenTransferNotAllowed.selector, address(mockERC721), SAFE_TRANSFER_FROM + ) + ); + hook.preCheck(address(0), 0, msgData); + } + + function test_GivenSelectorIsERC721SafeTransferFromWithData() external whenCallingPreCheckWithSINGLEMode { + // it should revert with TokenTransferNotAllowed + _install(); + bytes memory callData = abi.encodeWithSelector(SAFE_TRANSFER_FROM_WITH_DATA, account, recipient, 1, hex""); + bytes memory msgData = _singleMsgData(address(mockERC721), 0, callData); + vm.prank(account); + vm.expectRevert( + abi.encodeWithSelector( + DefaultSecurityHook.TokenTransferNotAllowed.selector, address(mockERC721), SAFE_TRANSFER_FROM_WITH_DATA + ) + ); + hook.preCheck(address(0), 0, msgData); + } + + function test_GivenSelectorIsERC721SetApprovalForAll() external whenCallingPreCheckWithSINGLEMode { + // it should revert with TokenTransferNotAllowed + _install(); + bytes memory callData = abi.encodeCall(IERC721.setApprovalForAll, (recipient, true)); + bytes memory msgData = _singleMsgData(address(mockERC721), 0, callData); + vm.prank(account); + vm.expectRevert( + abi.encodeWithSelector( + DefaultSecurityHook.TokenTransferNotAllowed.selector, address(mockERC721), SET_APPROVAL_FOR_ALL + ) + ); + hook.preCheck(address(0), 0, msgData); + } + + function test_GivenSelectorIsERC1155SafeTransferFrom() external whenCallingPreCheckWithSINGLEMode { + // it should revert with TokenTransferNotAllowed + _install(); + bytes memory callData = abi.encodeCall(IERC1155.safeTransferFrom, (account, recipient, 1, 1, hex"")); + bytes memory msgData = _singleMsgData(address(mockERC1155), 0, callData); + vm.prank(account); + vm.expectRevert( + abi.encodeWithSelector( + DefaultSecurityHook.TokenTransferNotAllowed.selector, address(mockERC1155), SAFE_TRANSFER_FROM_1155 + ) + ); + hook.preCheck(address(0), 0, msgData); + } + + function test_GivenSelectorIsERC1155SafeBatchTransferFrom() external whenCallingPreCheckWithSINGLEMode { + // it should revert with TokenTransferNotAllowed + _install(); + uint256[] memory ids = new uint256[](1); + ids[0] = 1; + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1; + bytes memory callData = + abi.encodeCall(IERC1155.safeBatchTransferFrom, (account, recipient, ids, amounts, hex"")); + bytes memory msgData = _singleMsgData(address(mockERC1155), 0, callData); + vm.prank(account); + vm.expectRevert( + abi.encodeWithSelector( + DefaultSecurityHook.TokenTransferNotAllowed.selector, address(mockERC1155), SAFE_BATCH_TRANSFER_FROM + ) + ); + hook.preCheck(address(0), 0, msgData); + } + + function test_GivenCallHasNoBlockedSelectorAndNoValueAndTargetIsClean() external whenCallingPreCheckWithSINGLEMode { + // it should return empty bytes + _install(); + bytes memory msgData = _singleMsgData(randomTarget, 0, abi.encodeWithSelector(BALANCE_OF, address(1))); + bytes memory result = _preCheck(msgData); + assertEq(result, hex"", "Should return empty bytes for clean call"); + } + + function test_GivenCalldataIsLessThan4Bytes() external whenCallingPreCheckWithSINGLEMode { + // it should return empty bytes (no selector to match, and no value, clean target) + _install(); + // 3 bytes of calldata — less than 4 + bytes memory msgData = _singleMsgData(randomTarget, 0, hex"aabbcc"); + bytes memory result = _preCheck(msgData); + assertEq(result, hex"", "Should return empty bytes for short calldata"); + } + + // ==================== preCheck BATCH Tests ==================== + + modifier whenCallingPreCheckWithBATCHMode() { + _; + } + + function test_GivenAllCallsInBatchAreClean() external whenCallingPreCheckWithBATCHMode { + // it should return empty bytes + _install(); + + Execution[] memory execs = new Execution[](2); + execs[0] = Execution({target: randomTarget, value: 0, callData: abi.encodeWithSelector(BALANCE_OF, address(1))}); + execs[1] = Execution({target: randomTarget, value: 0, callData: abi.encodeWithSelector(BALANCE_OF, address(2))}); + + bytes memory msgData = _batchMsgData(execs); + bytes memory result = _preCheck(msgData); + assertEq(result, hex"", "Should return empty bytes for clean batch"); + } + + function test_GivenOneCallInBatchHasBlockedSelector() external whenCallingPreCheckWithBATCHMode { + // it should revert with TokenTransferNotAllowed + _install(); + + Execution[] memory execs = new Execution[](2); + execs[0] = Execution({target: randomTarget, value: 0, callData: abi.encodeWithSelector(BALANCE_OF, address(1))}); + execs[1] = Execution({ + target: address(mockERC20), value: 0, callData: abi.encodeCall(IERC20.transfer, (recipient, 100)) + }); + + bytes memory msgData = _batchMsgData(execs); + vm.prank(account); + vm.expectRevert( + abi.encodeWithSelector(DefaultSecurityHook.TokenTransferNotAllowed.selector, address(mockERC20), TRANSFER) + ); + hook.preCheck(address(0), 0, msgData); + } + + function test_GivenBatchHasAllowlistedAndBlockedCalls() external whenCallingPreCheckWithBATCHMode { + // it should revert for the blocked call + // Allowlist mockERC20 but not mockERC721 + DefaultSecurityHook.AllowlistConfig[] memory configs = new DefaultSecurityHook.AllowlistConfig[](1); + bytes4[] memory sels = new bytes4[](0); + configs[0] = DefaultSecurityHook.AllowlistConfig({target: address(mockERC20), selectors: sels}); + _installWithConfig(configs); + + Execution[] memory execs = new Execution[](2); + // This call is allowlisted (mockERC20 with all selectors) + execs[0] = Execution({ + target: address(mockERC20), value: 0, callData: abi.encodeCall(IERC20.transfer, (recipient, 100)) + }); + // This call is NOT allowlisted and has a blocked selector + execs[1] = Execution({ + target: address(mockERC721), + value: 0, + callData: abi.encodeCall(IERC721.setApprovalForAll, (recipient, true)) + }); + + bytes memory msgData = _batchMsgData(execs); + vm.prank(account); + vm.expectRevert( + abi.encodeWithSelector( + DefaultSecurityHook.TokenTransferNotAllowed.selector, address(mockERC721), SET_APPROVAL_FOR_ALL + ) + ); + hook.preCheck(address(0), 0, msgData); + } + + // ==================== postCheck Tests ==================== + + function test_WhenCallingPostCheck() external { + // it should not revert + hook.postCheck(hex""); + hook.postCheck(hex"1234"); + assertTrue(true, "postCheck should not revert"); + } + + // ==================== setAllowlist Tests ==================== + + modifier whenCallingSetAllowlist() { + _; + } + + function test_GivenCallerIsNotInitialized() external whenCallingSetAllowlist { + // it should revert with Unauthorized + bytes4[] memory sels = new bytes4[](0); + vm.prank(account); + vm.expectRevert(DefaultSecurityHook.Unauthorized.selector); + hook.setAllowlist(randomTarget, sels); + } + + function test_GivenSettingWithEmptySelectors() external whenCallingSetAllowlist { + // it should set allSelectorsAllowed to true + // it should emit AllowlistSet event + _install(); + + bytes4[] memory sels = new bytes4[](0); + vm.prank(account); + vm.expectEmit(true, true, false, true); + emit AllowlistSet(account, randomTarget, sels); + hook.setAllowlist(randomTarget, sels); + + assertTrue(hook.isAllowlisted(account, randomTarget), "Target should be allowlisted"); + assertTrue(hook.isSelectorAllowed(account, randomTarget, TRANSFER), "All selectors should be allowed"); + } + + function test_GivenSettingWithSpecificSelectors() external whenCallingSetAllowlist { + // it should set each selector as allowed + // it should set allSelectorsAllowed to false + // it should emit AllowlistSet event + _install(); + + bytes4[] memory sels = new bytes4[](2); + sels[0] = TRANSFER; + sels[1] = APPROVE; + + vm.prank(account); + vm.expectEmit(true, true, false, true); + emit AllowlistSet(account, randomTarget, sels); + hook.setAllowlist(randomTarget, sels); + + assertTrue(hook.isAllowlisted(account, randomTarget), "Target should be allowlisted"); + assertTrue(hook.isSelectorAllowed(account, randomTarget, TRANSFER), "TRANSFER should be allowed"); + assertTrue(hook.isSelectorAllowed(account, randomTarget, APPROVE), "APPROVE should be allowed"); + assertFalse(hook.isSelectorAllowed(account, randomTarget, TRANSFER_FROM), "TRANSFER_FROM should not be allowed"); + } + + // ==================== removeAllowlist Tests ==================== + + modifier whenCallingRemoveAllowlist() { + _; + } + + function test_GivenCallerIsNotInitialized_WhenCallingRemoveAllowlist() external whenCallingRemoveAllowlist { + // it should revert with Unauthorized + vm.prank(account); + vm.expectRevert(DefaultSecurityHook.Unauthorized.selector); + hook.removeAllowlist(randomTarget); + } + + function test_GivenRemovingExistingTarget() external whenCallingRemoveAllowlist { + // it should set allowed to false + // it should emit AllowlistRemoved event + _install(); + + bytes4[] memory sels = new bytes4[](0); + vm.prank(account); + hook.setAllowlist(randomTarget, sels); + + assertTrue(hook.isAllowlisted(account, randomTarget), "Target should be allowlisted before removal"); + + vm.prank(account); + vm.expectEmit(true, true, false, false); + emit AllowlistRemoved(account, randomTarget); + hook.removeAllowlist(randomTarget); + + assertFalse(hook.isAllowlisted(account, randomTarget), "Target should not be allowlisted after removal"); + } + + // ==================== isInitialized Tests ==================== + + modifier whenCallingIsInitialized() { + _; + } + + function test_GivenAccountIsInitialized() external whenCallingIsInitialized { + // it should return true + _install(); + assertTrue(hook.isInitialized(account), "Should return true for initialized account"); + } + + function test_GivenAccountIsNotInitialized_WhenCallingIsInitialized() external whenCallingIsInitialized { + // it should return false + assertFalse(hook.isInitialized(account), "Should return false for uninitialized account"); + } + + // ==================== isAllowlisted Tests ==================== + + modifier whenCallingIsAllowlisted() { + _; + } + + function test_GivenTargetIsAllowlisted() external whenCallingIsAllowlisted { + // it should return true + _install(); + bytes4[] memory sels = new bytes4[](0); + vm.prank(account); + hook.setAllowlist(randomTarget, sels); + + assertTrue(hook.isAllowlisted(account, randomTarget), "Should return true for allowlisted target"); + } + + function test_GivenTargetIsNotAllowlisted() external whenCallingIsAllowlisted { + // it should return false + assertFalse(hook.isAllowlisted(account, randomTarget), "Should return false for non-allowlisted target"); + } + + // ==================== isSelectorAllowed Tests ==================== + + modifier whenCallingIsSelectorAllowed() { + _; + } + + function test_GivenTargetIsNotAllowlisted_WhenCallingIsSelectorAllowed() external whenCallingIsSelectorAllowed { + // it should return false + assertFalse( + hook.isSelectorAllowed(account, randomTarget, TRANSFER), "Should return false when target not allowlisted" + ); + } + + function test_GivenTargetIsAllowlistedWithAllSelectors_WhenCallingIsSelectorAllowed() + external + whenCallingIsSelectorAllowed + { + // it should return true for any selector + _install(); + bytes4[] memory sels = new bytes4[](0); + vm.prank(account); + hook.setAllowlist(randomTarget, sels); + + assertTrue( + hook.isSelectorAllowed(account, randomTarget, TRANSFER), + "Should return true for any selector when all allowed" + ); + assertTrue( + hook.isSelectorAllowed(account, randomTarget, bytes4(0xdeadbeef)), + "Should return true for arbitrary selector when all allowed" + ); + } + + modifier givenTargetIsAllowlistedWithSpecificSelectors() { + _install(); + bytes4[] memory sels = new bytes4[](1); + sels[0] = TRANSFER; + vm.prank(account); + hook.setAllowlist(randomTarget, sels); + _; + } + + function test_GivenQueriedSelectorIsInTheList() + external + whenCallingIsSelectorAllowed + givenTargetIsAllowlistedWithSpecificSelectors + { + // it should return true + assertTrue(hook.isSelectorAllowed(account, randomTarget, TRANSFER), "Should return true for allowed selector"); + } + + function test_GivenQueriedSelectorIsNotInTheList() + external + whenCallingIsSelectorAllowed + givenTargetIsAllowlistedWithSpecificSelectors + { + // it should return false + assertFalse( + hook.isSelectorAllowed(account, randomTarget, APPROVE), "Should return false for non-allowed selector" + ); + } + + // ==================== S-01 Regression: Stale selectors cleared on update ==================== + + function test_S01_StaleSelectorsAreClearedOnAllowlistUpdate() external { + _install(); + + // Step 1: Allowlist with TRANSFER and APPROVE + bytes4[] memory sels1 = new bytes4[](2); + sels1[0] = TRANSFER; + sels1[1] = APPROVE; + vm.prank(account); + hook.setAllowlist(randomTarget, sels1); + + assertTrue(hook.isSelectorAllowed(account, randomTarget, TRANSFER), "TRANSFER should be allowed"); + assertTrue(hook.isSelectorAllowed(account, randomTarget, APPROVE), "APPROVE should be allowed"); + + // Step 2: Update to only TRANSFER + bytes4[] memory sels2 = new bytes4[](1); + sels2[0] = TRANSFER; + vm.prank(account); + hook.setAllowlist(randomTarget, sels2); + + // APPROVE must be cleared + assertTrue(hook.isSelectorAllowed(account, randomTarget, TRANSFER), "TRANSFER should still be allowed"); + assertFalse(hook.isSelectorAllowed(account, randomTarget, APPROVE), "APPROVE should be cleared after update"); + } + + function test_S01_StaleSelectorsAreClearedOnRemoveAllowlist() external { + _install(); + + // Allowlist with specific selectors + bytes4[] memory sels = new bytes4[](1); + sels[0] = TRANSFER; + vm.prank(account); + hook.setAllowlist(randomTarget, sels); + + assertTrue(hook.isSelectorAllowed(account, randomTarget, TRANSFER), "TRANSFER should be allowed"); + + // Remove allowlist + vm.prank(account); + hook.removeAllowlist(randomTarget); + + // Re-add allowlist with different selectors + bytes4[] memory sels2 = new bytes4[](1); + sels2[0] = APPROVE; + vm.prank(account); + hook.setAllowlist(randomTarget, sels2); + + // TRANSFER must not persist from the old allowlist + assertFalse( + hook.isSelectorAllowed(account, randomTarget, TRANSFER), "TRANSFER should not persist after remove+re-add" + ); + assertTrue(hook.isSelectorAllowed(account, randomTarget, APPROVE), "APPROVE should be allowed"); + } + + // ==================== S-02 Regression: Unknown call types revert ==================== + + function test_S02_UnknownCallTypeReverts() external { + _install(); + + // Build msgData with CALLTYPE_STATICCALL (0xfe) + bytes32 mode = + LibERC7579.encodeMode(LibERC7579.CALLTYPE_STATICCALL, LibERC7579.EXECTYPE_DEFAULT, bytes4(0), bytes22(0)); + bytes memory executionData = abi.encodePacked(randomTarget, uint256(0)); + bytes memory msgData = abi.encodeWithSelector(IERC7579Execution.execute.selector, mode, executionData); + + vm.prank(account); + vm.expectRevert(DefaultSecurityHook.UnsupportedCallType.selector); + hook.preCheck(address(0), 0, msgData); + } + + // ==================== S-03 Regression: onUninstall clears ALL state ==================== + + function test_S03_OnUninstallClearsAllTargetsWithoutCallerData() external { + // Install with two targets + DefaultSecurityHook.AllowlistConfig[] memory configs = new DefaultSecurityHook.AllowlistConfig[](2); + bytes4[] memory sels1 = new bytes4[](1); + sels1[0] = TRANSFER; + configs[0] = DefaultSecurityHook.AllowlistConfig({target: randomTarget, selectors: sels1}); + + address target2 = address(0xBEEF); + bytes4[] memory sels2 = new bytes4[](0); + configs[1] = DefaultSecurityHook.AllowlistConfig({target: target2, selectors: sels2}); + _installWithConfig(configs); + + assertTrue(hook.isAllowlisted(account, randomTarget), "randomTarget should be allowlisted"); + assertTrue(hook.isAllowlisted(account, target2), "target2 should be allowlisted"); + + // Uninstall with empty data -- should still clear everything + vm.prank(account); + hook.onUninstall(""); + + assertFalse(hook.isAllowlisted(account, randomTarget), "randomTarget should be cleared after uninstall"); + assertFalse(hook.isAllowlisted(account, target2), "target2 should be cleared after uninstall"); + assertFalse( + hook.isSelectorAllowed(account, randomTarget, TRANSFER), + "TRANSFER selector should be cleared after uninstall" + ); + } + + function test_S03_StaleStateDoesNotPersistAcrossReinstall() external { + // Install with target allowlisted + DefaultSecurityHook.AllowlistConfig[] memory configs1 = new DefaultSecurityHook.AllowlistConfig[](1); + bytes4[] memory sels1 = new bytes4[](1); + sels1[0] = TRANSFER; + configs1[0] = DefaultSecurityHook.AllowlistConfig({target: randomTarget, selectors: sels1}); + _installWithConfig(configs1); + + assertTrue(hook.isSelectorAllowed(account, randomTarget, TRANSFER), "TRANSFER should be allowed"); + + // Uninstall (empty data) + vm.prank(account); + hook.onUninstall(""); + + // Reinstall with different config (APPROVE only) + DefaultSecurityHook.AllowlistConfig[] memory configs2 = new DefaultSecurityHook.AllowlistConfig[](1); + bytes4[] memory sels2 = new bytes4[](1); + sels2[0] = APPROVE; + configs2[0] = DefaultSecurityHook.AllowlistConfig({target: randomTarget, selectors: sels2}); + _installWithConfig(configs2); + + // TRANSFER must NOT persist from the first installation + assertFalse( + hook.isSelectorAllowed(account, randomTarget, TRANSFER), "TRANSFER should not persist across reinstall" + ); + assertTrue(hook.isSelectorAllowed(account, randomTarget, APPROVE), "APPROVE should be allowed after reinstall"); + } +} diff --git a/test/btt/DefaultSecurityHook.t.tree b/test/btt/DefaultSecurityHook.t.tree new file mode 100644 index 0000000..1ad9536 --- /dev/null +++ b/test/btt/DefaultSecurityHook.t.tree @@ -0,0 +1,110 @@ +DefaultSecurityHookBTTTest +├── when calling onInstall +│ ├── given account is already initialized +│ │ └── it should revert with AlreadyInitialized +│ ├── given data is empty +│ │ ├── it should mark account as initialized +│ │ └── it should emit Initialized event +│ └── given data contains allowlist configs +│ ├── it should mark account as initialized +│ ├── it should set allowlist entries for each config +│ └── it should emit Initialized event +├── when calling onUninstall +│ ├── given account is not initialized +│ │ └── it should revert with NotInitialized +│ ├── given data is empty +│ │ ├── it should mark account as not initialized +│ │ └── it should emit Uninitialized event +│ └── given data contains targets to clean +│ ├── it should clear allowlist entries for each target +│ ├── it should mark account as not initialized +│ └── it should emit Uninitialized event +├── when calling isModuleType +│ ├── given moduleTypeId is MODULE_TYPE_HOOK +│ │ └── it should return true +│ └── given moduleTypeId is not MODULE_TYPE_HOOK +│ └── it should return false +├── when calling preCheck with DELEGATECALL mode +│ └── it should revert with DelegateCallNotAllowed +├── when calling preCheck with SINGLE mode +│ ├── given target is allowlisted with all selectors +│ │ └── it should return empty bytes +│ ├── given target is allowlisted with specific selector matching call +│ │ └── it should return empty bytes +│ ├── given target is allowlisted with specific selector not matching call +│ │ └── it should revert with TokenTransferNotAllowed +│ ├── given target is self +│ │ └── it should revert with SelfCallNotAllowed +│ ├── given target is a module +│ │ └── it should revert with ModuleCallNotAllowed +│ ├── given value is greater than zero +│ │ └── it should revert with ETHTransferNotAllowed +│ ├── given selector is ERC20 transfer +│ │ └── it should revert with TokenTransferNotAllowed +│ ├── given selector is ERC20 approve +│ │ └── it should revert with TokenTransferNotAllowed +│ ├── given selector is ERC20 transferFrom +│ │ └── it should revert with TokenTransferNotAllowed +│ ├── given selector is ERC20 increaseAllowance +│ │ └── it should revert with TokenTransferNotAllowed +│ ├── given selector is ERC20 decreaseAllowance +│ │ └── it should revert with TokenTransferNotAllowed +│ ├── given selector is ERC721 safeTransferFrom +│ │ └── it should revert with TokenTransferNotAllowed +│ ├── given selector is ERC721 safeTransferFromWithData +│ │ └── it should revert with TokenTransferNotAllowed +│ ├── given selector is ERC721 setApprovalForAll +│ │ └── it should revert with TokenTransferNotAllowed +│ ├── given selector is ERC1155 safeTransferFrom +│ │ └── it should revert with TokenTransferNotAllowed +│ ├── given selector is ERC1155 safeBatchTransferFrom +│ │ └── it should revert with TokenTransferNotAllowed +│ ├── given call has no blocked selector and no value and target is clean +│ │ └── it should return empty bytes +│ └── given calldata is less than 4 bytes +│ └── it should return empty bytes +├── when calling preCheck with BATCH mode +│ ├── given all calls in batch are clean +│ │ └── it should return empty bytes +│ ├── given one call in batch has blocked selector +│ │ └── it should revert with TokenTransferNotAllowed +│ └── given batch has allowlisted and blocked calls +│ └── it should revert for the blocked call +├── when calling postCheck +│ └── it should not revert +├── when calling setAllowlist +│ ├── given caller is not initialized +│ │ └── it should revert with Unauthorized +│ ├── given setting with empty selectors +│ │ ├── it should set allSelectorsAllowed to true +│ │ └── it should emit AllowlistSet event +│ └── given setting with specific selectors +│ ├── it should set each selector as allowed +│ ├── it should set allSelectorsAllowed to false +│ └── it should emit AllowlistSet event +├── when calling removeAllowlist +│ ├── given caller is not initialized +│ │ └── it should revert with Unauthorized +│ └── given removing existing target +│ ├── it should set allowed to false +│ └── it should emit AllowlistRemoved event +├── when calling isInitialized +│ ├── given account is initialized +│ │ └── it should return true +│ └── given account is not initialized +│ └── it should return false +├── when calling isAllowlisted +│ ├── given target is allowlisted +│ │ └── it should return true +│ └── given target is not allowlisted +│ └── it should return false +└── when calling isSelectorAllowed + ├── given target is not allowlisted + │ └── it should return false + ├── given target is allowlisted with all selectors + │ └── it should return true for any selector + └── given target is allowlisted with specific selectors + ├── given queried selector is in the list + │ └── it should return true + └── given queried selector is not in the list + └── it should return false diff --git a/test/btt/ECDSASigner.t.sol b/test/btt/ECDSASigner.t.sol index dfde846..415f8cd 100644 --- a/test/btt/ECDSASigner.t.sol +++ b/test/btt/ECDSASigner.t.sol @@ -394,9 +394,7 @@ contract ECDSASignerBTTTest is Test { vm.prank(wallet); uint256 result = ecdsaSigner.checkUserOpSignature(signerId, userOp, userOpHash); assertEq( - result, - SIG_VALIDATION_FAILED_UINT, - "Should return SIG_VALIDATION_FAILED_UINT after both branches fail" + result, SIG_VALIDATION_FAILED_UINT, "Should return SIG_VALIDATION_FAILED_UINT after both branches fail" ); } diff --git a/test/btt/ECDSAValidator.t.sol b/test/btt/ECDSAValidator.t.sol index 286dfd9..ad84506 100644 --- a/test/btt/ECDSAValidator.t.sol +++ b/test/btt/ECDSAValidator.t.sol @@ -440,9 +440,7 @@ contract ECDSAValidatorBTTTest is Test { vm.prank(wallet); uint256 result = ecdsaValidator.validateUserOp(userOp, userOpHash); assertEq( - result, - SIG_VALIDATION_FAILED_UINT, - "Should return SIG_VALIDATION_FAILED_UINT after both branches fail" + result, SIG_VALIDATION_FAILED_UINT, "Should return SIG_VALIDATION_FAILED_UINT after both branches fail" ); } diff --git a/test/btt/WeightedECDSAGasGriefing.t.sol b/test/btt/WeightedECDSAGasGriefing.t.sol index b888845..9a6d159 100644 --- a/test/btt/WeightedECDSAGasGriefing.t.sol +++ b/test/btt/WeightedECDSAGasGriefing.t.sol @@ -125,11 +125,7 @@ contract WeightedECDSAGasGriefingTest is Test { }); } - function _signUserOp(PackedUserOperation memory userOp, uint256 numSigners) - internal - view - returns (bytes memory) - { + function _signUserOp(PackedUserOperation memory userOp, uint256 numSigners) internal view returns (bytes memory) { bytes32 proposalHash = _computeProposalHash(userOp); bytes32 userOpHash = entrypoint.getUserOpHash(userOp); @@ -497,10 +493,7 @@ contract WeightedECDSAGasGriefingTest is Test { assertEq(result, SIG_VALIDATION_SUCCESS_UINT); } - function test_RevertWhen_Non_lastSignersAreNotInSortedOrder() - external - whenValidatingERC4337UserOp - { + function test_RevertWhen_Non_lastSignersAreNotInSortedOrder() external whenValidatingERC4337UserOp { _installSigner(5); PackedUserOperation memory userOp = _createUserOp(); diff --git a/test/btt/WeightedECDSAUserOpHash.t.sol b/test/btt/WeightedECDSAUserOpHash.t.sol index f53b6bd..aef1233 100644 --- a/test/btt/WeightedECDSAUserOpHash.t.sol +++ b/test/btt/WeightedECDSAUserOpHash.t.sol @@ -7,10 +7,7 @@ import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOper import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol"; import {EntryPointLib} from "../utils/EntryPointLib.sol"; import {ECDSA} from "solady/utils/ECDSA.sol"; -import { - SIG_VALIDATION_FAILED_UINT, - SIG_VALIDATION_SUCCESS_UINT -} from "src/types/Constants.sol"; +import {SIG_VALIDATION_FAILED_UINT, SIG_VALIDATION_SUCCESS_UINT} from "src/types/Constants.sol"; /// @title WeightedECDSAUserOpHashTest /// @notice BTT tests for the security fix on branch fix/tob-kernel-16 @@ -270,7 +267,11 @@ contract WeightedECDSAUserOpHashTest is Test { vm.prank(WALLET); uint256 result = testModule.checkUserOpSignature(SIGNER_ID, userOp, userOpHash); - assertEq(result, SIG_VALIDATION_SUCCESS_UINT, "Should succeed when guardian weight meets threshold even without double counting"); + assertEq( + result, + SIG_VALIDATION_SUCCESS_UINT, + "Should succeed when guardian weight meets threshold even without double counting" + ); } function test_WhenProposalHashSignaturesReachThresholdButUserOpHashIsMissing() external { @@ -469,6 +470,10 @@ contract WeightedECDSAUserOpHashTest is Test { vm.prank(WALLET); uint256 result = signerModule.checkUserOpSignature(SIGNER_ID, userOp, userOpHash); - assertEq(result, SIG_VALIDATION_SUCCESS_UINT, "Should succeed when lower address guardian signs userOpHash (no sorted order required for last signer)"); + assertEq( + result, + SIG_VALIDATION_SUCCESS_UINT, + "Should succeed when lower address guardian signs userOpHash (no sorted order required for last signer)" + ); } }