diff --git a/package.json b/package.json index 6dca193..766a079 100644 --- a/package.json +++ b/package.json @@ -19,12 +19,12 @@ "@openzeppelin/contracts": "5.0.2", "@openzeppelin/contracts-upgradeable": "5.0.2", "fcl": "github:rdubois-crypto/FreshCryptoLib#8179e08cac72072bd260796633fec41fdfd5b441", - "forge-std": "github:foundry-rs/forge-std#v1.9.2", + "forge-std": "github:foundry-rs/forge-std#v1.9.5", "solady": "0.0.243", - "@erc6900/reference-implementation": "github:erc6900/reference-implementation#v0.8.0-rc.6" + "@erc6900/reference-implementation": "github:erc6900/reference-implementation#v0.8.0" }, "devDependencies": { - "solhint": "^5.0.3" + "solhint": "^5.0.5" }, "directories": { "test": "test" diff --git a/script/011_DeployTokenCallbackPlugin.s.sol b/script/011_DeployTokenCallbackPlugin.s.sol deleted file mode 100644 index 97f8360..0000000 --- a/script/011_DeployTokenCallbackPlugin.s.sol +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2024 Circle Internet Group, Inc. All rights reserved. - - * SPDX-License-Identifier: GPL-3.0-or-later - - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -pragma solidity 0.8.24; - -import {DefaultTokenCallbackPlugin} from "../src/msca/6900/v0.7/plugins/v1_0_0/utility/DefaultTokenCallbackPlugin.sol"; - -import {DEFAULT_TOKEN_CALLBACK_PLUGIN_ADDRESS} from "./000_ContractAddress.sol"; -import {Script, console} from "forge-std/src/Script.sol"; - -contract DeployTokenCallbackPluginScript is Script { - // Safety check to avoid accidental deploy. Change address to 0x0 if you want to deploy a new version. - address payable internal constant EXPECTED_PLUGIN_ADDRESS = payable(DEFAULT_TOKEN_CALLBACK_PLUGIN_ADDRESS); - - function run() public { - uint256 key = vm.envUint("DEPLOYER_PRIVATE_KEY"); - vm.startBroadcast(key); - DefaultTokenCallbackPlugin plugin; - if (EXPECTED_PLUGIN_ADDRESS.code.length == 0) { - plugin = new DefaultTokenCallbackPlugin{salt: 0}(); - } else { - plugin = DefaultTokenCallbackPlugin(EXPECTED_PLUGIN_ADDRESS); - } - console.log("Plugin address: %s", address(plugin)); - console.log("Default token callback manifest hash: "); - console.logBytes32(keccak256(abi.encode(plugin.pluginManifest()))); - vm.stopBroadcast(); - } -} diff --git a/src/common/Errors.sol b/src/common/Errors.sol index 7d1820d..36a0dab 100644 --- a/src/common/Errors.sol +++ b/src/common/Errors.sol @@ -26,3 +26,5 @@ error UnauthorizedCaller(); error InvalidLength(); error Unsupported(); + +error InvalidPublicKey(uint256 x, uint256 y); diff --git a/src/libs/PublicKeyLib.sol b/src/libs/PublicKeyLib.sol index dd54e89..f2d6841 100644 --- a/src/libs/PublicKeyLib.sol +++ b/src/libs/PublicKeyLib.sol @@ -19,16 +19,15 @@ pragma solidity 0.8.24; import {PublicKey} from "../common/CommonStructs.sol"; +import {InvalidPublicKey} from "../common/Errors.sol"; /** * @dev Util functions for public key. */ library PublicKeyLib { - error InvalidPublicKey(uint256 x, uint256 y); - function toBytes30(uint256 x, uint256 y) internal pure returns (bytes30) { // (0, 0) is point at infinity and not on the curve and should therefore be rejected - if (x == 0 && y == 0) { + if (!isValidPublicKey(x, y)) { revert InvalidPublicKey(x, y); } return bytes30(uint240(uint256(keccak256(abi.encode(x, y))))); @@ -40,4 +39,12 @@ library PublicKeyLib { uint256 y = publicKey.y; return toBytes30(x, y); } + + function isValidPublicKey(uint256 x, uint256 y) internal pure returns (bool) { + return x != 0 || y != 0; + } + + function isValidPublicKey(PublicKey memory publicKey) internal pure returns (bool) { + return publicKey.x != 0 || publicKey.y != 0; + } } diff --git a/src/msca/6900/shared/erc712/BaseERC712CompliantModule.sol b/src/msca/6900/shared/erc712/BaseERC712CompliantModule.sol index 68f18da..4cd1a09 100644 --- a/src/msca/6900/shared/erc712/BaseERC712CompliantModule.sol +++ b/src/msca/6900/shared/erc712/BaseERC712CompliantModule.sol @@ -46,16 +46,24 @@ abstract contract BaseERC712CompliantModule { return MessageHashUtils.toTypedDataHash({ domainSeparator: keccak256( abi.encode( - _DOMAIN_SEPARATOR_TYPEHASH, _getModuleIdHash(), block.chainid, address(this), bytes32(bytes20(account)) + _DOMAIN_SEPARATOR_TYPEHASH, + _getModuleNameHash(), + _getModuleVersionHash(), + block.chainid, + address(this), + bytes32(bytes20(account)) ) ), structHash: keccak256(abi.encode(_getModuleTypeHash(), hash)) }); } - /// @dev Returns the module typehash. + /// @dev Returns the module message typehash. function _getModuleTypeHash() internal pure virtual returns (bytes32); - /// @dev Returns the module id. - function _getModuleIdHash() internal pure virtual returns (bytes32); + /// @dev Returns the module name hash. + function _getModuleNameHash() internal pure virtual returns (bytes32); + + // @dev Returns the module version hash. + function _getModuleVersionHash() internal pure virtual returns (bytes32); } diff --git a/src/msca/6900/shared/libs/AddressDLLLib.sol b/src/msca/6900/shared/libs/AddressDLLLib.sol index 5028cec..4720360 100644 --- a/src/msca/6900/shared/libs/AddressDLLLib.sol +++ b/src/msca/6900/shared/libs/AddressDLLLib.sol @@ -45,7 +45,11 @@ library AddressDLLLib { * @dev Check if an item exists or not. O(1). */ function contains(AddressDLL storage dll, address item) internal view returns (bool) { - return getHead(dll) == item || dll.next[item] != address(0) || dll.prev[item] != address(0); + if (item == SENTINEL_ADDRESS) { + // SENTINEL_ADDRESS is not a valid item + return false; + } + return getHead(dll) == item || dll.next[item] != SENTINEL_ADDRESS || dll.prev[item] != SENTINEL_ADDRESS; } /** @@ -109,7 +113,7 @@ library AddressDLLLib { } address[] memory results = new address[](limit); address current = start; - if (start == address(0)) { + if (start == SENTINEL_ADDRESS) { current = getHead(dll); } uint256 count = 0; @@ -131,17 +135,11 @@ library AddressDLLLib { function getAll(AddressDLL storage dll) internal view returns (address[] memory results) { uint256 totalCount = size(dll); results = new address[](totalCount); - uint256 accumulatedCount = 0; - address startAddr = address(0x0); - for (uint256 i = 0; i < totalCount; ++i) { - (address[] memory currentResults, address nextAddr) = getPaginated(dll, startAddr, 10); - for (uint256 j = 0; j < currentResults.length; ++j) { - results[accumulatedCount++] = currentResults[j]; - } - if (nextAddr == SENTINEL_ADDRESS) { - break; - } - startAddr = nextAddr; + address current = getHead(dll); + uint256 count = 0; + for (; count < totalCount && uint160(current) > SENTINEL_ADDRESS_UINT; ++count) { + results[count] = current; + current = dll.next[current]; } return results; } diff --git a/src/msca/6900/shared/libs/Bytes32DLLLib.sol b/src/msca/6900/shared/libs/Bytes32DLLLib.sol index d684197..cc183ab 100644 --- a/src/msca/6900/shared/libs/Bytes32DLLLib.sol +++ b/src/msca/6900/shared/libs/Bytes32DLLLib.sol @@ -38,6 +38,10 @@ library Bytes32DLLLib { * @dev Check if an item exists or not. O(1). */ function contains(Bytes32DLL storage dll, bytes32 item) internal view returns (bool) { + if (item == SENTINEL_BYTES32) { + // SENTINEL_BYTES32 is not a valid item + return false; + } return getHead(dll) == item || dll.next[item] != SENTINEL_BYTES32 || dll.prev[item] != SENTINEL_BYTES32; } @@ -121,17 +125,11 @@ library Bytes32DLLLib { function getAll(Bytes32DLL storage dll) internal view returns (bytes32[] memory results) { uint256 totalCount = size(dll); results = new bytes32[](totalCount); - uint256 accumulatedCount = 0; - bytes32 start = SENTINEL_BYTES32; - for (uint256 i = 0; i < totalCount; ++i) { - (bytes32[] memory currentResults, bytes32 next) = getPaginated(dll, start, 10); - for (uint256 j = 0; j < currentResults.length; ++j) { - results[accumulatedCount++] = currentResults[j]; - } - if (next == SENTINEL_BYTES32) { - break; - } - start = next; + bytes32 current = getHead(dll); + uint256 count = 0; + for (; count < totalCount && current > SENTINEL_BYTES32; ++count) { + results[count] = current; + current = dll.next[current]; } return results; } diff --git a/src/msca/6900/shared/libs/Bytes4DLLLib.sol b/src/msca/6900/shared/libs/Bytes4DLLLib.sol index 79d8c3d..abcb569 100644 --- a/src/msca/6900/shared/libs/Bytes4DLLLib.sol +++ b/src/msca/6900/shared/libs/Bytes4DLLLib.sol @@ -40,6 +40,10 @@ library Bytes4DLLLib { * @dev Check if an item exists or not. O(1). */ function contains(Bytes4DLL storage dll, bytes4 item) internal view returns (bool) { + if (item == SENTINEL_BYTES4) { + // SENTINEL_BYTES4 is not a valid item + return false; + } return getHead(dll) == item || dll.next[item] != bytes4(0) || dll.prev[item] != bytes4(0); } @@ -123,17 +127,11 @@ library Bytes4DLLLib { function getAll(Bytes4DLL storage dll) internal view returns (bytes4[] memory results) { uint256 totalCount = size(dll); results = new bytes4[](totalCount); - uint256 accumulatedCount = 0; - bytes4 startVal = bytes4(0x0); - for (uint256 i = 0; i < totalCount; ++i) { - (bytes4[] memory currentResults, bytes4 nextVal) = getPaginated(dll, startVal, 10); - for (uint256 j = 0; j < currentResults.length; ++j) { - results[accumulatedCount++] = currentResults[j]; - } - if (nextVal == SENTINEL_BYTES4) { - break; - } - startVal = nextVal; + bytes4 current = getHead(dll); + uint256 count = 0; + for (; count < totalCount && current > SENTINEL_BYTES4; ++count) { + results[count] = current; + current = dll.next[current]; } return results; } diff --git a/src/msca/6900/shared/libs/ValidationDataLib.sol b/src/msca/6900/shared/libs/ValidationDataLib.sol index 2474566..56324b2 100644 --- a/src/msca/6900/shared/libs/ValidationDataLib.sol +++ b/src/msca/6900/shared/libs/ValidationDataLib.sol @@ -24,8 +24,12 @@ library ValidationDataLib { error WrongTimeBounds(); /** - * @dev Intercept the time bounds [validAfter, validUntil], as well as signature validation result (favoring the - * failure). + * @dev Intercept the time bounds `[validAfter, validUntil]` and the signature validation result, + * prioritizing the invalid authorizer (`!=0 && !=1`), followed by prioritizing failure (`==1`), + * and finally returning success (`==0`). Please note that both `authorizer(2)` and `authorizer(3)` are invalid, + * and calling this function with `(2, 3)` ensures that only one invalid authorizer will be returned. + * @notice address(0) is a successful validation, address(1) is a failed validation, + * and address(2), address(3) and others are invalid authorizers (also failed). */ function _intersectValidationData(ValidationData memory a, uint256 uintb) internal @@ -40,10 +44,16 @@ library ValidationDataLib { revert WrongTimeBounds(); } // 0 is successful validation - if (a.authorizer == address(0)) { + if (!_isValidAuthorizer(a.authorizer)) { + validationData.authorizer = a.authorizer; + } else if (!_isValidAuthorizer(b.authorizer)) { validationData.authorizer = b.authorizer; } else { - validationData.authorizer = a.authorizer; + if (a.authorizer == address(0)) { + validationData.authorizer = b.authorizer; + } else { + validationData.authorizer = a.authorizer; + } } if (a.validAfter > b.validAfter) { validationData.validAfter = a.validAfter; @@ -56,7 +66,9 @@ library ValidationDataLib { validationData.validUntil = b.validUntil; } // make sure the caller (e.g. entryPoint) reverts - if (validationData.validAfter >= validationData.validUntil) { + // set to address(1) if and only if the authorizer is address(0) (successful validation) + // we don't want to set to address(1) if the authorizer is invalid such as address(2) + if (validationData.validAfter >= validationData.validUntil && validationData.authorizer == address(0)) { validationData.authorizer = address(1); } return validationData; @@ -82,4 +94,8 @@ library ValidationDataLib { function _packValidationData(ValidationData memory data) internal pure returns (uint256) { return uint160(data.authorizer) | (uint256(data.validUntil) << 160) | (uint256(data.validAfter) << (160 + 48)); } + + function _isValidAuthorizer(address authorizer) internal pure returns (bool) { + return authorizer == address(0) || authorizer == address(1); + } } diff --git a/src/msca/6900/v0.7/account/BaseMSCA.sol b/src/msca/6900/v0.7/account/BaseMSCA.sol index 3c62b51..17206a6 100644 --- a/src/msca/6900/v0.7/account/BaseMSCA.sol +++ b/src/msca/6900/v0.7/account/BaseMSCA.sol @@ -143,6 +143,9 @@ abstract contract BaseMSCA is /// If there's no plugin associated with this function selector, revert // solhint-disable-next-line no-complex-fallback fallback(bytes calldata) external payable returns (bytes memory result) { + if (msg.data.length < 4) { + revert NotFoundSelector(); + } // run runtime validation before we load the executionDetail because validation may update account state if (msg.sender != address(ENTRY_POINT)) { // ENTRY_POINT should go through validateUserOp flow which calls userOpValidationFunction diff --git a/src/msca/6900/v0.7/libs/FunctionReferenceDLLLib.sol b/src/msca/6900/v0.7/libs/FunctionReferenceDLLLib.sol index ff42498..5924c52 100644 --- a/src/msca/6900/v0.7/libs/FunctionReferenceDLLLib.sol +++ b/src/msca/6900/v0.7/libs/FunctionReferenceDLLLib.sol @@ -18,7 +18,7 @@ */ pragma solidity 0.8.24; -import {EMPTY_FUNCTION_REFERENCE, SENTINEL_BYTES21} from "../../../../common/Constants.sol"; +import {SENTINEL_BYTES21} from "../../../../common/Constants.sol"; import { InvalidFunctionReference, InvalidLimit, ItemAlreadyExists, ItemDoesNotExist } from "../../shared/common/Errors.sol"; @@ -48,6 +48,9 @@ library FunctionReferenceDLLLib { } function contains(Bytes21DLL storage dll, bytes21 item) internal view returns (bool) { + if (item == SENTINEL_BYTES21) { + return false; + } return getHeadWithoutUnpack(dll) == item || dll.next[item] != SENTINEL_BYTES21 || dll.prev[item] != SENTINEL_BYTES21; } @@ -143,18 +146,11 @@ library FunctionReferenceDLLLib { function getAll(Bytes21DLL storage dll) internal view returns (FunctionReference[] memory results) { uint256 totalCount = size(dll); results = new FunctionReference[](totalCount); - uint256 accumulatedCount = 0; - FunctionReference memory startFR = EMPTY_FUNCTION_REFERENCE.unpack(); - for (uint256 i = 0; i < totalCount; ++i) { - (FunctionReference[] memory currentResults, FunctionReference memory nextFR) = - getPaginated(dll, startFR, 10); - for (uint256 j = 0; j < currentResults.length; ++j) { - results[accumulatedCount++] = currentResults[j]; - } - if (nextFR.pack() == SENTINEL_BYTES21) { - break; - } - startFR = nextFR; + bytes21 current = getHeadWithoutUnpack(dll); + uint256 count = 0; + for (; count < totalCount && current > SENTINEL_BYTES21; ++count) { + results[count] = current.unpack(); + current = dll.next[current]; } return results; } diff --git a/src/msca/6900/v0.7/libs/RepeatableFunctionReferenceDLLLib.sol b/src/msca/6900/v0.7/libs/RepeatableFunctionReferenceDLLLib.sol index 7936790..61c94e0 100644 --- a/src/msca/6900/v0.7/libs/RepeatableFunctionReferenceDLLLib.sol +++ b/src/msca/6900/v0.7/libs/RepeatableFunctionReferenceDLLLib.sol @@ -18,7 +18,7 @@ */ pragma solidity 0.8.24; -import {EMPTY_FUNCTION_REFERENCE, SENTINEL_BYTES21} from "../../../../common/Constants.sol"; +import {SENTINEL_BYTES21} from "../../../../common/Constants.sol"; import {InvalidFunctionReference, InvalidLimit, ItemDoesNotExist} from "../../shared/common/Errors.sol"; import {FunctionReference, RepeatableBytes21DLL} from "../common/Structs.sol"; import {FunctionReferenceLib} from "./FunctionReferenceLib.sol"; @@ -49,7 +49,8 @@ library RepeatableFunctionReferenceDLLLib { { bytes21 item = fr.pack(); if (item == SENTINEL_BYTES21) { - return 1; + // sentinel should not considered as the value of the list + return 0; } return dll.counter[item]; } @@ -191,18 +192,11 @@ library RepeatableFunctionReferenceDLLLib { function getAll(RepeatableBytes21DLL storage dll) internal view returns (FunctionReference[] memory results) { uint256 totalUniqueCount = getUniqueItems(dll); results = new FunctionReference[](totalUniqueCount); - uint256 accumulatedCount = 0; - FunctionReference memory startFR = EMPTY_FUNCTION_REFERENCE.unpack(); - for (uint256 i = 0; i < totalUniqueCount; ++i) { - (FunctionReference[] memory currentResults, FunctionReference memory nextFR) = - getPaginated(dll, startFR, 10); - for (uint256 j = 0; j < currentResults.length; ++j) { - results[accumulatedCount++] = currentResults[j]; - } - if (nextFR.pack() == SENTINEL_BYTES21) { - break; - } - startFR = nextFR; + bytes21 current = getHeadWithoutUnpack(dll); + uint256 count = 0; + for (; count < totalUniqueCount && current > SENTINEL_BYTES21; ++count) { + results[count] = current.unpack(); + current = dll.next[current]; } return results; } diff --git a/src/msca/6900/v0.7/libs/SelectorRegistryLib.sol b/src/msca/6900/v0.7/libs/SelectorRegistryLib.sol index 0fbf4ef..21a3249 100644 --- a/src/msca/6900/v0.7/libs/SelectorRegistryLib.sol +++ b/src/msca/6900/v0.7/libs/SelectorRegistryLib.sol @@ -28,6 +28,7 @@ import {IAggregator} from "@account-abstraction/contracts/interfaces/IAggregator import {IPaymaster} from "@account-abstraction/contracts/interfaces/IPaymaster.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {IAccount} from "@account-abstraction/contracts/interfaces/IAccount.sol"; import {IERC777Recipient} from "@openzeppelin/contracts/interfaces/IERC777Recipient.sol"; import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; @@ -40,7 +41,6 @@ library SelectorRegistryLib { bytes4 internal constant TRANSFER_NATIVE_OWNERSHIP = bytes4(keccak256("transferNativeOwnership(address)")); bytes4 internal constant RENOUNCE_NATIVE_OWNERSHIP = bytes4(keccak256("renounceNativeOwnership()")); bytes4 internal constant GET_NATIVE_OWNER = bytes4(keccak256("getNativeOwner()")); - bytes4 internal constant VALIDATE_USER_OP = bytes4(keccak256("validateUserOp(UserOperation,bytes32,uint256)")); bytes4 internal constant GET_ENTRYPOINT = bytes4(keccak256("getEntryPoint()")); bytes4 internal constant GET_NONCE = bytes4(keccak256("getNonce()")); @@ -61,7 +61,7 @@ library SelectorRegistryLib { || selector == IAccountLoupe.getExecutionFunctionConfig.selector || selector == IAccountLoupe.getExecutionHooks.selector || selector == IAccountLoupe.getPreValidationHooks.selector - || selector == IAccountLoupe.getInstalledPlugins.selector || selector == VALIDATE_USER_OP + || selector == IAccountLoupe.getInstalledPlugins.selector || selector == IAccount.validateUserOp.selector || selector == GET_ENTRYPOINT || selector == GET_NONCE || selector == INITIALIZE_UPGRADABLE_MSCA || selector == INITIALIZE_SINGLE_OWNER_MSCA || selector == TRANSFER_NATIVE_OWNERSHIP || selector == RENOUNCE_NATIVE_OWNERSHIP || selector == GET_NATIVE_OWNER diff --git a/src/msca/6900/v0.7/libs/WalletStorageV1Lib.sol b/src/msca/6900/v0.7/libs/WalletStorageV1Lib.sol index 741ae8e..67e5deb 100644 --- a/src/msca/6900/v0.7/libs/WalletStorageV1Lib.sol +++ b/src/msca/6900/v0.7/libs/WalletStorageV1Lib.sol @@ -23,7 +23,12 @@ import {ExecutionDetail, PermittedExternalCall, PluginDetail} from "../common/St /// @dev The same storage will be used for v1.x.y of MSCAs. library WalletStorageV1Lib { - // keccak256 hash of "circle.msca.v1.storage" subtracted by 1 + // @notice When we initially calculated the storage slot, EIP-7201 was still under active discussion, + // so we didn’t fully adopt the storage alignment proposed by the EIP, which reduces gas costs + // for subsequent operations, as a single cold storage access warms all 256 slots within the group. + // To avoid introducing breaking changes and the complexity of migration, we chose not to make changes midway. + // For v2 accounts, which will feature a different storage layout, we will adopt EIP-7201. + // keccak256(abi.encode(uint256(keccak256(abi.encode("circle.msca.v1.storage"))) - 1)) bytes32 private constant WALLET_STORAGE_SLOT = 0xc6a0cc20c824c4eecc4b0fbb7fb297d07492a7bd12c83d4fa4d27b4249f9bfc8; struct Layout { diff --git a/src/msca/6900/v0.7/managers/PluginManager.sol b/src/msca/6900/v0.7/managers/PluginManager.sol index 3ead74a..48b827b 100644 --- a/src/msca/6900/v0.7/managers/PluginManager.sol +++ b/src/msca/6900/v0.7/managers/PluginManager.sol @@ -81,6 +81,7 @@ contract PluginManager { error OnlyDelegated(); error HookDependencyNotPermitted(); error InvalidExecutionSelector(address plugin, bytes4 selector); + error InvalidSelfDependency(); modifier onlyDelegated() { if (address(this) == SELF) { @@ -110,8 +111,6 @@ contract PluginManager { if (manifestHash != keccak256(abi.encode(pluginManifest))) { revert InvalidPluginManifestHash(); } - // store the plugin manifest hash - storageLayout.pluginDetails[plugin].manifestHash = manifestHash; uint256 length = pluginManifest.interfaceIds.length; for (uint256 i = 0; i < length; ++i) { storageLayout.supportedInterfaces[pluginManifest.interfaceIds[i]] += 1; @@ -131,11 +130,11 @@ contract PluginManager { if (dependencyPluginAddr == msca) { continue; } - if (!ERC165Checker.supportsInterface(dependencyPluginAddr, pluginManifest.dependencyInterfaceIds[i])) { + // verify that the dependency is installed, which also prevents self-dependencies + if (storageLayout.pluginDetails[dependencyPluginAddr].manifestHash == bytes32(0)) { revert InvalidPluginDependency(dependencyPluginAddr); } - // the dependency plugin needs to be installed first - if (!storageLayout.installedPlugins.contains(dependencyPluginAddr)) { + if (!ERC165Checker.supportsInterface(dependencyPluginAddr, pluginManifest.dependencyInterfaceIds[i])) { revert InvalidPluginDependency(dependencyPluginAddr); } // each dependency’s record MUST also be updated to reflect that it has a new dependent @@ -297,6 +296,8 @@ contract PluginManager { } } + // store the plugin manifest hash at the end, which serves to prevent self-dependencies + storageLayout.pluginDetails[plugin].manifestHash = manifestHash; // call onInstall to initialize plugin data for the modular account // solhint-disable-next-line no-empty-blocks try IPlugin(plugin).onInstall(pluginInstallData) {} @@ -544,7 +545,9 @@ contract PluginManager { FunctionReference memory startFR = EMPTY_FUNCTION_REFERENCE.unpack(); FunctionReference[] memory dependencies; for (uint256 i = 0; i < length; ++i) { - (dependencies, startFR) = pluginDependencies.getPaginated(startFR, 100); + // If the max length of dependencies is reached, the loop will break early, + // 10 is the default limit because we only had one plugin that needs dependency so far. + (dependencies, startFR) = pluginDependencies.getPaginated(startFR, 10); for (uint256 j = 0; j < dependencies.length; ++j) { storageLayout.pluginDetails[dependencies[j].plugin].dependentCounter -= 1; storageLayout.pluginDetails[plugin].dependencies.remove(dependencies[j]); diff --git a/src/msca/6900/v0.7/plugins/v1_0_0/acl/SingleOwnerPlugin.sol b/src/msca/6900/v0.7/plugins/v1_0_0/acl/SingleOwnerPlugin.sol index 6c57331..dfdc36b 100644 --- a/src/msca/6900/v0.7/plugins/v1_0_0/acl/SingleOwnerPlugin.sol +++ b/src/msca/6900/v0.7/plugins/v1_0_0/acl/SingleOwnerPlugin.sol @@ -69,7 +69,12 @@ contract SingleOwnerPlugin is BasePlugin, ISingleOwnerPlugin, IERC1271, BaseERC7 using MessageHashUtils for bytes32; string public constant _NAME = "Single Owner Plugin"; - bytes32 private constant _PLUGIN_TYPEHASH = keccak256("CircleSingleOwnerPluginMessage(bytes32 hash)"); + // keccak256("Single Owner Plugin") + bytes32 private constant _HASHED_NAME = 0x08c8cf10d2d0bf39cc82fa5a1ebaa119cc25f363c1f8283e24234f97cf6ba1b3; + // keccak256("1.0.0") + bytes32 private constant _HASHED_MODULE_VERSION = 0x06c015bd22b4c69690933c1058878ebdfef31f9aaae40bbe86d8a09fe1b2972c; + // keccak256("CircleSingleOwnerPluginMessage(bytes32 hash)") + bytes32 private constant _PLUGIN_TYPEHASH = 0xe9e3c1d4aef0f2df05fca86fe9de193cdd9cfec08c6a2b4ea169c3a816c171e7; string internal constant TRANSFER_OWNERSHIP = "Transfer_Ownership"; // MSCA => owner mapping(address => address) internal _mscaOwners; @@ -265,7 +270,12 @@ contract SingleOwnerPlugin is BasePlugin, ISingleOwnerPlugin, IERC1271, BaseERC7 } /// @inheritdoc BaseERC712CompliantModule - function _getModuleIdHash() internal pure override returns (bytes32) { - return keccak256(abi.encodePacked(_NAME, PLUGIN_VERSION_1)); + function _getModuleNameHash() internal pure override returns (bytes32) { + return _HASHED_NAME; + } + + /// @inheritdoc BaseERC712CompliantModule + function _getModuleVersionHash() internal pure override returns (bytes32) { + return _HASHED_MODULE_VERSION; } } diff --git a/src/msca/6900/v0.7/plugins/v1_0_0/multisig/BaseMultisigPlugin.sol b/src/msca/6900/v0.7/plugins/v1_0_0/multisig/BaseMultisigPlugin.sol index c136ca1..da0a54b 100644 --- a/src/msca/6900/v0.7/plugins/v1_0_0/multisig/BaseMultisigPlugin.sol +++ b/src/msca/6900/v0.7/plugins/v1_0_0/multisig/BaseMultisigPlugin.sol @@ -31,6 +31,7 @@ import {NotImplemented} from "../../../../shared/common/Errors.sol"; import {BasePlugin} from "../../BasePlugin.sol"; import {IWeightedMultisigPlugin} from "./IWeightedMultisigPlugin.sol"; +import {CalldataUtils} from "../../../../../../utils/CalldataUtils.sol"; import {PackedUserOperation} from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; import { AssociatedLinkedListSet, @@ -48,6 +49,7 @@ abstract contract BaseMultisigPlugin is BasePlugin { using ECDSA for bytes32; using MessageHashUtils for bytes32; using AssociatedLinkedListSetLib for AssociatedLinkedListSet; + using CalldataUtils for bytes; error EmptyOwnersNotAllowed(); error InvalidAddress(); @@ -63,6 +65,7 @@ abstract contract BaseMultisigPlugin is BasePlugin { error TooManyOwners(uint256 currentNumOwners, uint256 numOwnersToAdd); error ZeroOwnersInputNotAllowed(); error InvalidUserOpDigest(); + error UnsupportedSigType(uint8 sigType); enum FunctionId { USER_OP_VALIDATION_OWNER // require owner access @@ -88,7 +91,7 @@ abstract contract BaseMultisigPlugin is BasePlugin { public view virtual - returns (bool success, uint256 firstFailure); + returns (bool success, uint256 firstFailure, IWeightedMultisigPlugin.CheckNSignatureError returnError); /// @inheritdoc BasePlugin function userOpValidationFunction(uint8 functionId, PackedUserOperation calldata userOp, bytes32 userOpHash) @@ -114,7 +117,7 @@ abstract contract BaseMultisigPlugin is BasePlugin { account: msg.sender, signatures: userOp.signature }); - (bool success,) = checkNSignatures(input); + (bool success,,) = checkNSignatures(input); return success ? SIG_VALIDATION_SUCCEEDED : SIG_VALIDATION_FAILED; } @@ -132,8 +135,8 @@ abstract contract BaseMultisigPlugin is BasePlugin { sender := calldataload(userOp) } uint256 nonce = userOp.nonce; - bytes32 hashInitCode = _calldataKeccak(userOp.initCode); - bytes32 hashCallData = _calldataKeccak(userOp.callData); + bytes32 hashInitCode = userOp.initCode.calldataKeccak(); + bytes32 hashCallData = userOp.callData.calldataKeccak(); bytes32 userOpHash = keccak256( abi.encode( @@ -151,17 +154,6 @@ abstract contract BaseMultisigPlugin is BasePlugin { return keccak256(abi.encode(userOpHash, ENTRYPOINT, block.chainid)); } - /// @param data calldata to hash - function _calldataKeccak(bytes calldata data) internal pure returns (bytes32 ret) { - // solhint-disable-next-line no-inline-assembly - assembly ("memory-safe") { - let mem := mload(0x40) - let len := data.length - calldatacopy(mem, data.offset, len) - ret := keccak256(mem, len) - } - } - /// @notice Check if the account has initialized this plugin yet /// @param account The account to check /// @return True if the account has initialized this plugin diff --git a/src/msca/6900/v0.7/plugins/v1_0_0/multisig/BaseWeightedMultisigPlugin.sol b/src/msca/6900/v0.7/plugins/v1_0_0/multisig/BaseWeightedMultisigPlugin.sol index 4a3a118..f308314 100644 --- a/src/msca/6900/v0.7/plugins/v1_0_0/multisig/BaseWeightedMultisigPlugin.sol +++ b/src/msca/6900/v0.7/plugins/v1_0_0/multisig/BaseWeightedMultisigPlugin.sol @@ -180,7 +180,7 @@ abstract contract BaseWeightedMultisigPlugin is BaseMultisigPlugin, IWeightedMul view virtual override(BaseMultisigPlugin, IWeightedMultisigPlugin) - returns (bool success, uint256 firstFailure); + returns (bool success, uint256 firstFailure, CheckNSignatureError returnError); // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ Internal Functions ┃ @@ -209,7 +209,9 @@ abstract contract BaseWeightedMultisigPlugin is BaseMultisigPlugin, IWeightedMul emit OwnersAdded(msg.sender, ownersToAdd, ownersDataToAdd); } - function _removeOwners(bytes30[] memory ownersToRemove, uint256 newThresholdWeight) internal { + function _removeOwnersAndUpdateMultisigMetadata(bytes30[] memory ownersToRemove, uint256 newThresholdWeight) + internal + { uint256 toRemoveLen = ownersToRemove.length; if (toRemoveLen == 0) { diff --git a/src/msca/6900/v0.7/plugins/v1_0_0/multisig/IWeightedMultisigPlugin.sol b/src/msca/6900/v0.7/plugins/v1_0_0/multisig/IWeightedMultisigPlugin.sol index 0648346..67430d9 100644 --- a/src/msca/6900/v0.7/plugins/v1_0_0/multisig/IWeightedMultisigPlugin.sol +++ b/src/msca/6900/v0.7/plugins/v1_0_0/multisig/IWeightedMultisigPlugin.sol @@ -30,25 +30,25 @@ interface IWeightedMultisigPlugin { /// @param account account plugin is installed on /// @param owners list of owners added /// @param weights list of weights corresponding to added owners - event OwnersAdded(address account, bytes30[] owners, OwnerData[] weights); + event OwnersAdded(address indexed account, bytes30[] owners, OwnerData[] weights); /// @notice This event is emitted when owners of the account are removed. /// @param account account plugin is installed on /// @param owners list of owners removed /// @param totalWeightRemoved total weight removed - event OwnersRemoved(address account, bytes30[] owners, uint256 totalWeightRemoved); + event OwnersRemoved(address indexed account, bytes30[] owners, uint256 totalWeightRemoved); /// @notice This event is emitted when weights of account owners updated. /// @param account account plugin is installed on /// @param owners list of owners updated /// @param weights list of updated weights corresponding to owners - event OwnersUpdated(address account, bytes30[] owners, OwnerData[] weights); + event OwnersUpdated(address indexed account, bytes30[] owners, OwnerData[] weights); /// @notice This event is emitted when the threshold weight is updated /// @param account account plugin is installed on /// @param oldThresholdWeight the old threshold weight required to perform an action /// @param newThresholdWeight the new threshold weight required to perform an action - event ThresholdUpdated(address account, uint256 oldThresholdWeight, uint256 newThresholdWeight); + event ThresholdUpdated(address indexed account, uint256 oldThresholdWeight, uint256 newThresholdWeight); error InvalidThresholdWeight(); error InvalidWeight(bytes30 owner, address account, uint256 weight); @@ -62,6 +62,14 @@ interface IWeightedMultisigPlugin { bytes signatures; } + enum CheckNSignatureError { + NONE, + SIG_PARTS_OVERLAP, // constant part and dynamic part overlap or constant part is too long + SIGS_OUT_OF_ORDER, // signatures are unsorted + INVALID_CONTRACT_ADDRESS, + INVALID_SIG + } + /// @notice Add owners and their associated weights for the account, and optionally update threshold weight required /// to perform an action. /// @dev Constraints: @@ -158,12 +166,13 @@ interface IWeightedMultisigPlugin { /// signatures The signatures to check. /// @return success True if the signatures are valid. /// @return firstFailure first failure, if failed is true. + /// @return returnError for debugging. /// (Note: if all signatures are individually valid but do not satisfy the /// multisig, firstFailure will be set to the last signature's index.) function checkNSignatures(CheckNSignatureInput memory input) external view - returns (bool success, uint256 firstFailure); + returns (bool success, uint256 firstFailure, CheckNSignatureError returnError); /// @notice Check if an address is an owner of `account`. /// @param account The account to check. diff --git a/src/msca/6900/v0.7/plugins/v1_0_0/multisig/WeightedWebauthnMultisigPlugin.sol b/src/msca/6900/v0.7/plugins/v1_0_0/multisig/WeightedWebauthnMultisigPlugin.sol index c77f692..a441c2b 100644 --- a/src/msca/6900/v0.7/plugins/v1_0_0/multisig/WeightedWebauthnMultisigPlugin.sol +++ b/src/msca/6900/v0.7/plugins/v1_0_0/multisig/WeightedWebauthnMultisigPlugin.sol @@ -51,8 +51,10 @@ import {AddressBytesLib} from "../../../../../../libs/AddressBytesLib.sol"; import {PublicKeyLib} from "../../../../../../libs/PublicKeyLib.sol"; import {WebAuthnLib} from "../../../../../../libs/WebAuthnLib.sol"; +import {InvalidPublicKey} from "../../../../../../common/Errors.sol"; import {BaseERC712CompliantModule} from "../../../../shared/erc712/BaseERC712CompliantModule.sol"; import {IStandardExecutor} from "../../../interfaces/IStandardExecutor.sol"; +import {FCL_Elliptic_ZZ} from "@fcl/FCL_elliptic.sol"; import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; /// @title Weighted Multisig Plugin That Supports Additional Webauthn Authentication @@ -66,8 +68,13 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 using AddressBytesLib for address; string internal constant _NAME = "Weighted Multisig Webauthn Plugin"; + // keccak256("Weighted Multisig Webauthn Plugin") + bytes32 private constant _HASHED_NAME = 0xbb3dcb6be63dc1bed586f19f310ff9be992f42a915600fdf31872f5f02d4e705; + // keccak256("1.0.0") + bytes32 private constant _HASHED_MODULE_VERSION = 0x06c015bd22b4c69690933c1058878ebdfef31f9aaae40bbe86d8a09fe1b2972c; + // keccak256("CircleWeightedWebauthnMultisigMessage(bytes32 hash)") bytes32 private constant _MULTISIG_PLUGIN_TYPEHASH = - keccak256("CircleWeightedWebauthnMultisigMessage(bytes32 hash)"); + 0x7dc7da30a002512936154da01baa79a3d1e54e35034ffdfa7c53c28b3091236d; struct CheckNSignatureData { // lowestOffset of signature dynamic part, must locate after the signature constant part @@ -81,6 +88,7 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 bytes32 first32Bytes; // second32Bytes of signature constant part bytes32 second32Bytes; + CheckNSignatureError returnError; } constructor(address entryPoint) BaseWeightedMultisigPlugin(entryPoint) {} @@ -96,6 +104,7 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 uint256[] calldata pubicKeyWeightsToAdd, uint256 newThresholdWeight ) external override isInitialized(msg.sender) { + _validatePublicKeys(publicKeyOwnersToAdd); (bytes30[] memory _totalOwners, OwnerData[] memory _ownersData) = _mergeOwnersData(ownersToAdd, weightsToAdd, publicKeyOwnersToAdd, pubicKeyWeightsToAdd); _addOwnersAndUpdateMultisigMetadata(_totalOwners, _ownersData, newThresholdWeight); @@ -108,7 +117,7 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 uint256 newThresholdWeight ) external override isInitialized(msg.sender) { bytes30[] memory _totalOwners = _mergeOwners(ownersToRemove, publicKeyOwnersToRemove); - _removeOwners(_totalOwners, newThresholdWeight); + _removeOwnersAndUpdateMultisigMetadata(_totalOwners, newThresholdWeight); } /// @inheritdoc IWeightedMultisigPlugin @@ -132,7 +141,7 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 function isValidSignature(bytes32 digest, bytes memory signature) external view override returns (bytes4) { bytes32 wrappedDigest = getReplaySafeMessageHash(msg.sender, digest); CheckNSignatureInput memory input = CheckNSignatureInput(wrappedDigest, wrappedDigest, msg.sender, signature); - (bool success,) = checkNSignatures(input); + (bool success,,) = checkNSignatures(input); return success ? EIP1271_VALID_SIGNATURE : EIP1271_INVALID_SIGNATURE; } @@ -161,6 +170,7 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 uint256[] memory publicKeyOwnerWeights, uint256 thresholdWeight ) = abi.decode(data, (address[], uint256[], PublicKey[], uint256[], uint256)); + _validatePublicKeys(initialPublicKeyOwners); (bytes30[] memory _totalOwners, OwnerData[] memory _ownersData) = _mergeOwnersData(initialOwners, ownerWeights, initialPublicKeyOwners, publicKeyOwnerWeights); _onInstall(_totalOwners, _ownersData, thresholdWeight); @@ -319,7 +329,7 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 public view override - returns (bool success, uint256 firstFailure) + returns (bool success, uint256 firstFailure, CheckNSignatureError returnError) { if (input.signatures.length < _INDIVIDUAL_SIGNATURE_BYTES_LEN) { revert InvalidSigLength(); @@ -341,6 +351,7 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 // tracks whether `signatures` is a complete and valid multisig signature checkNSignatureData.success = true; + checkNSignatureData.returnError = CheckNSignatureError.NONE; while (accumulatedWeight < thresholdWeight) { // Fail if the next 65 bytes would exceed signature length // or lowest dynamic part signature offset, where next 65 bytes is defined as @@ -357,9 +368,9 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 ) || sigConstantPartEndPos > input.signatures.length ) { if (checkNSignatureData.success) { - return (false, signatureCount); + return (false, signatureCount, CheckNSignatureError.SIG_PARTS_OVERLAP); } else { - return (false, checkNSignatureData.firstFailure); + return (false, checkNSignatureData.firstFailure, CheckNSignatureError.SIG_PARTS_OVERLAP); } } @@ -387,9 +398,11 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 _validateContractSignature(checkNSignatureData, input, digest, signatureCount); } else if (sigType == 2) { _validateWebauthnSignature(checkNSignatureData, input, digest, signatureCount); - } else { + } else if (sigType == 27 || sigType == 28) { // reverts if signature has the wrong s value, wrong v value, or if it's a bad point on the k1 curve _validateEOASignature(checkNSignatureData, input, digest, signatureCount, sigType); + } else { + revert UnsupportedSigType(sigType); } if ( @@ -401,6 +414,7 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 if (checkNSignatureData.success) { checkNSignatureData.firstFailure = signatureCount; checkNSignatureData.success = false; + checkNSignatureData.returnError = CheckNSignatureError.SIGS_OUT_OF_ORDER; } } @@ -414,7 +428,7 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 if (numSigsOnActualDigest != 0) { revert InvalidNumSigsOnActualDigest(numSigsOnActualDigest); } - return (checkNSignatureData.success, checkNSignatureData.firstFailure); + return (checkNSignatureData.success, checkNSignatureData.firstFailure, checkNSignatureData.returnError); } function _validateContractSignature( @@ -434,6 +448,7 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 if (checkNSignatureData.success) { checkNSignatureData.firstFailure = signatureCount; checkNSignatureData.success = false; + checkNSignatureData.returnError = CheckNSignatureError.INVALID_CONTRACT_ADDRESS; } } // offset of current signature dynamic part @@ -475,6 +490,7 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 if (checkNSignatureData.success) { checkNSignatureData.firstFailure = signatureCount; checkNSignatureData.success = false; + checkNSignatureData.returnError = CheckNSignatureError.INVALID_SIG; } } } @@ -535,6 +551,7 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 if (checkNSignatureData.success) { checkNSignatureData.firstFailure = signatureCount; checkNSignatureData.success = false; + checkNSignatureData.returnError = CheckNSignatureError.INVALID_SIG; } } } @@ -553,6 +570,7 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 if (checkNSignatureData.success) { checkNSignatureData.firstFailure = signatureCount; checkNSignatureData.success = false; + checkNSignatureData.returnError = CheckNSignatureError.INVALID_SIG; } } } @@ -563,7 +581,20 @@ contract WeightedWebauthnMultisigPlugin is BaseWeightedMultisigPlugin, BaseERC71 } /// @inheritdoc BaseERC712CompliantModule - function _getModuleIdHash() internal pure override returns (bytes32) { - return keccak256(abi.encodePacked(_NAME, PLUGIN_VERSION_1)); + function _getModuleNameHash() internal pure override returns (bytes32) { + return _HASHED_NAME; + } + + /// @inheritdoc BaseERC712CompliantModule + function _getModuleVersionHash() internal pure override returns (bytes32) { + return _HASHED_MODULE_VERSION; + } + + function _validatePublicKeys(PublicKey[] memory publicKeys) internal pure { + for (uint256 i = 0; i < publicKeys.length; ++i) { + if (!FCL_Elliptic_ZZ.ecAff_isOnCurve(publicKeys[i].x, publicKeys[i].y)) { + revert InvalidPublicKey(publicKeys[i].x, publicKeys[i].y); + } + } } } diff --git a/src/msca/6900/v0.7/plugins/v1_0_0/utility/DefaultTokenCallbackPlugin.sol b/src/msca/6900/v0.7/plugins/v1_0_0/utility/DefaultTokenCallbackPlugin.sol deleted file mode 100644 index 13af141..0000000 --- a/src/msca/6900/v0.7/plugins/v1_0_0/utility/DefaultTokenCallbackPlugin.sol +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2024 Circle Internet Group, Inc. All rights reserved. - - * SPDX-License-Identifier: GPL-3.0-or-later - - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -pragma solidity 0.8.24; - -import {PLUGIN_AUTHOR, PLUGIN_VERSION_1} from "../../../../../../common/Constants.sol"; -import { - ManifestAssociatedFunction, - ManifestAssociatedFunctionType, - ManifestFunction, - PluginManifest, - PluginMetadata -} from "../../../common/PluginManifest.sol"; -import {BasePlugin} from "../../BasePlugin.sol"; -import {IERC1155Receiver} from "@openzeppelin/contracts/interfaces/IERC1155Receiver.sol"; -import {IERC777Recipient} from "@openzeppelin/contracts/interfaces/IERC777Recipient.sol"; -import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; - -/** - * @dev Default token callback handler plugin. Similar to DefaultCallbackHandler. - * This plugin allows MSCA to receive tokens such as 721, 1155 and 777. - * @notice The user will have to register itself in the ERC1820 global registry - * in order to fully support ERC777 token operations upon the installation of this plugin. - */ -contract DefaultTokenCallbackPlugin is BasePlugin, IERC721Receiver, IERC1155Receiver, IERC777Recipient { - string public constant NAME = "Default Token Callback Plugin"; - - function onInstall(bytes calldata data) external pure override { - (data); - } - - function onUninstall(bytes calldata data) external pure override { - (data); - } - - function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) { - return IERC721Receiver.onERC721Received.selector; - } - - function onERC1155Received(address, address, uint256, uint256, bytes calldata) - external - pure - override - returns (bytes4) - { - return IERC1155Receiver.onERC1155Received.selector; - } - - function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) - external - pure - override - returns (bytes4) - { - return IERC1155Receiver.onERC1155BatchReceived.selector; - } - - // ERC777 - function tokensReceived( - address operator, - address from, - address to, - uint256 amount, - bytes calldata userData, - bytes calldata operatorData - ) external pure override - // solhint-disable-next-line no-empty-blocks - {} - - /// @inheritdoc BasePlugin - function pluginManifest() external pure override returns (PluginManifest memory) { - PluginManifest memory manifest; - manifest.executionFunctions = new bytes4[](4); - manifest.executionFunctions[0] = this.onERC721Received.selector; - manifest.executionFunctions[1] = this.onERC1155Received.selector; - manifest.executionFunctions[2] = this.onERC1155BatchReceived.selector; - manifest.executionFunctions[3] = this.tokensReceived.selector; - // only runtimeValidationFunctions is needed since token contracts callback to the plugin - manifest.runtimeValidationFunctions = new ManifestAssociatedFunction[](4); - // we can consider implementing more complex logic to reject the scamming tokens in the future - ManifestFunction memory runtimeValidationAlwaysAllow = - ManifestFunction(ManifestAssociatedFunctionType.RUNTIME_VALIDATION_ALWAYS_ALLOW, 0, 0); - manifest.runtimeValidationFunctions[0] = - ManifestAssociatedFunction(this.onERC721Received.selector, runtimeValidationAlwaysAllow); - manifest.runtimeValidationFunctions[1] = - ManifestAssociatedFunction(this.onERC1155Received.selector, runtimeValidationAlwaysAllow); - manifest.runtimeValidationFunctions[2] = - ManifestAssociatedFunction(this.onERC1155BatchReceived.selector, runtimeValidationAlwaysAllow); - manifest.runtimeValidationFunctions[3] = - ManifestAssociatedFunction(this.tokensReceived.selector, runtimeValidationAlwaysAllow); - manifest.interfaceIds = new bytes4[](3); - manifest.interfaceIds[0] = type(IERC721Receiver).interfaceId; - manifest.interfaceIds[1] = type(IERC1155Receiver).interfaceId; - manifest.interfaceIds[2] = type(IERC777Recipient).interfaceId; - return manifest; - } - - /// @inheritdoc BasePlugin - function pluginMetadata() external pure virtual override returns (PluginMetadata memory) { - PluginMetadata memory metadata; - metadata.name = NAME; - metadata.version = PLUGIN_VERSION_1; - metadata.author = PLUGIN_AUTHOR; - return metadata; - } -} diff --git a/src/msca/6900/v0.8/account/BaseMSCA.sol b/src/msca/6900/v0.8/account/BaseMSCA.sol index d9aa479..dacdcc3 100644 --- a/src/msca/6900/v0.8/account/BaseMSCA.sol +++ b/src/msca/6900/v0.8/account/BaseMSCA.sol @@ -45,7 +45,8 @@ import { Call, HookConfig, ModuleEntity, - ValidationConfig + ValidationConfig, + ValidationFlags } from "@erc6900/reference-implementation/interfaces/IModularAccount.sol"; import {IExecutionHookModule} from "@erc6900/reference-implementation/interfaces/IExecutionHookModule.sol"; @@ -112,6 +113,7 @@ abstract contract BaseMSCA is using SparseCalldataSegmentLib for bytes; using Bytes4DLLLib for Bytes4DLL; using ValidationConfigLib for ValidationConfig; + using ValidationConfigLib for ValidationFlags; using HookConfigLib for HookConfig; using HookLib for HookConfig; using HookLib for bytes32; @@ -191,6 +193,9 @@ abstract contract BaseMSCA is /// If there's no module associated with this function selector, revert // solhint-disable-next-line no-complex-fallback fallback(bytes calldata) external payable returns (bytes memory result) { + if (msg.data.length < 4) { + revert NotFoundSelector(); + } address executionFunctionModule = WalletStorageLib.getLayout().executionStorage[msg.sig].module; // valid module address should not be address(0) if (executionFunctionModule == address(0)) { @@ -252,7 +257,7 @@ abstract contract BaseMSCA is /// @inheritdoc IERC1271 function isValidSignature(bytes32 hash, bytes calldata signature) public view override returns (bytes4) { ModuleEntity sigValidation = ModuleEntity.wrap(bytes24(signature)); - if (!WalletStorageLib.getLayout().validationStorage[sigValidation].isSignatureValidation) { + if (!WalletStorageLib.getLayout().validationStorage[sigValidation].validationFlags.isSignatureValidation()) { revert InvalidSignatureValidation(sigValidation); } signature = signature[24:]; @@ -446,9 +451,7 @@ abstract contract BaseMSCA is returns (ValidationDataView memory validationData) { ValidationStorage storage validationStorage = WalletStorageLib.getLayout().validationStorage[validationFunction]; - validationData.isGlobal = validationStorage.isGlobal; - validationData.isSignatureValidation = validationStorage.isSignatureValidation; - validationData.isUserOpValidation = validationStorage.isUserOpValidation; + validationData.validationFlags = validationStorage.validationFlags; validationData.validationHooks = validationStorage.validationHooks._toHookConfigs(); validationData.executionHooks = validationStorage.executionHooks._toHookConfigs(); validationData.selectors = validationStorage.selectors.getAll(); @@ -531,7 +534,7 @@ abstract contract BaseMSCA is bytes32 userOpHash ) internal returns (uint256 validationData) { ValidationStorage storage validationStorage = WalletStorageLib.getLayout().validationStorage[validationFunction]; - if (!validationStorage.isUserOpValidation) { + if (!validationStorage.validationFlags.isUserOpValidation()) { revert InvalidUserOpValidation(validationFunction); } Bytes32DLL storage validationHookFunctions = validationStorage.validationHooks; @@ -751,7 +754,7 @@ abstract contract BaseMSCA is bytes calldata installData, bytes[] calldata hooks ) internal { - ModuleEntity validationModuleEntity = validationConfig.moduleEntity(); + (ModuleEntity validationModuleEntity, ValidationFlags validationFlags) = validationConfig.unpack(); ValidationStorage storage validationStorage = WalletStorageLib.getLayout().validationStorage[validationModuleEntity]; uint256 hooksLength = hooks.length; @@ -780,9 +783,7 @@ abstract contract BaseMSCA is validationStorage.selectors.append(selectors[i]); } - validationStorage.isGlobal = validationConfig.isGlobal(); - validationStorage.isSignatureValidation = validationConfig.isSignatureValidation(); - validationStorage.isUserOpValidation = validationConfig.isUserOpValidation(); + validationStorage.validationFlags = validationFlags; // call onInstall to initialize module data for the modular account (address moduleAddr,) = validationModuleEntity.unpack(); _onInstall(moduleAddr, installData, type(IValidationModule).interfaceId); @@ -794,9 +795,7 @@ abstract contract BaseMSCA is bytes[] calldata hookUninstallData ) internal returns (bool) { ValidationStorage storage validationStorage = WalletStorageLib.getLayout().validationStorage[validationFunction]; - validationStorage.isGlobal = false; - validationStorage.isSignatureValidation = false; - validationStorage.isUserOpValidation = false; + validationStorage.validationFlags = ValidationFlags.wrap(0); bool onUninstallSucceeded = true; if (hookUninstallData.length > 0) { @@ -1047,7 +1046,7 @@ abstract contract BaseMSCA is return ( selector._isNativeExecutionFunction() || WalletStorageLib.getLayout().executionStorage[selector].allowGlobalValidation - ) && WalletStorageLib.getLayout().validationStorage[validationFunction].isGlobal; + ) && WalletStorageLib.getLayout().validationStorage[validationFunction].validationFlags.isGlobal(); } function _isAllowedForSelectorValidation(bytes4 selector, ModuleEntity validationFunction) diff --git a/src/msca/6900/v0.8/account/UpgradableMSCA.sol b/src/msca/6900/v0.8/account/UpgradableMSCA.sol index 97a878a..a3511b1 100644 --- a/src/msca/6900/v0.8/account/UpgradableMSCA.sol +++ b/src/msca/6900/v0.8/account/UpgradableMSCA.sol @@ -47,10 +47,7 @@ contract UpgradableMSCA is BaseMSCA, DefaultCallbackHandler, UUPSUpgradeable { event UpgradableMSCAInitialized(address indexed account, address indexed entryPointAddress); - constructor(IEntryPoint _newEntryPoint) BaseMSCA(_newEntryPoint) { - // lock the implementation contract so it can only be called from proxies - _disableWalletStorageInitializers(); - } + constructor(IEntryPoint _newEntryPoint) BaseMSCA(_newEntryPoint) {} /// @notice Initializes the account with a validation function. /// @dev This function is only callable once. It is expected to be called by the factory that deploys the account. diff --git a/src/msca/6900/v0.8/common/Structs.sol b/src/msca/6900/v0.8/common/Structs.sol index db796aa..145dc9c 100644 --- a/src/msca/6900/v0.8/common/Structs.sol +++ b/src/msca/6900/v0.8/common/Structs.sol @@ -19,7 +19,7 @@ pragma solidity 0.8.24; import {Bytes32DLL, Bytes4DLL} from "../../shared/common/Structs.sol"; -import {ModuleEntity} from "@erc6900/reference-implementation/interfaces/IModularAccount.sol"; +import {ModuleEntity, ValidationFlags} from "@erc6900/reference-implementation/interfaces/IModularAccount.sol"; // Standard executor struct Call { @@ -38,12 +38,12 @@ struct PostExecHookToRun { /// @notice Represents stored data associated with a specific validation function. struct ValidationStorage { - // Whether or not this validation can be used as a global validation function. - bool isGlobal; - // Whether or not this validation is allowed to validate ERC-1271 signatures. - bool isSignatureValidation; - // Whether or not this validation is allowed to validate ERC-4337 user operations. - bool isUserOpValidation; + // ValidationFlags layout: + // 0b00000___ // unused + // 0b_____A__ // isGlobal + // 0b______B_ // isSignatureValidation + // 0b_______C // isUserOpValidation + ValidationFlags validationFlags; // The validation hooks for this validation function. Bytes32DLL validationHooks; // Execution hooks to run with this validation function. diff --git a/src/msca/6900/v0.8/factories/UpgradableMSCAFactory.sol b/src/msca/6900/v0.8/factories/UpgradableMSCAFactory.sol index f391b97..2b28b20 100644 --- a/src/msca/6900/v0.8/factories/UpgradableMSCAFactory.sol +++ b/src/msca/6900/v0.8/factories/UpgradableMSCAFactory.sol @@ -65,6 +65,9 @@ contract UpgradableMSCAFactory is Ownable2Step { revert InvalidLength(); } for (uint256 i = 0; i < _modules.length; ++i) { + if (_modules[i] == address(0)) { + revert ModuleIsNotAllowed(_modules[i]); + } isModuleAllowed[_modules[i]] = _permissions[i]; } } @@ -149,6 +152,7 @@ contract UpgradableMSCAFactory is Ownable2Step { bytes[] memory _hooks ) internal view returns (address addr, bytes32 mixedSalt) { address module = _validationConfig.module(); + // would not allow address(0), indicating that at least one validation required if (!isModuleAllowed[module]) { revert ModuleIsNotAllowed(module); } diff --git a/src/msca/6900/v0.8/modules/multisig/IWeightedMultisigValidationModule.sol b/src/msca/6900/v0.8/modules/multisig/IWeightedMultisigValidationModule.sol index 36c3ecd..5b29202 100644 --- a/src/msca/6900/v0.8/modules/multisig/IWeightedMultisigValidationModule.sol +++ b/src/msca/6900/v0.8/modules/multisig/IWeightedMultisigValidationModule.sol @@ -19,138 +19,105 @@ pragma solidity 0.8.24; import {PublicKey} from "../../../../../common/CommonStructs.sol"; -import {AccountMetadata, CheckNSignaturesInput, SignerData} from "./MultisigStructs.sol"; +import {AccountMetadata, CheckNSignaturesRequest, SignerMetadata, SignerMetadataWithId} from "./MultisigStructs.sol"; import {IValidationModule} from "@erc6900/reference-implementation/interfaces/IValidationModule.sol"; -/// @title Weighted Multisig Module Interface +/// @title Weighted Multisig Validation Module Interface /// @author Circle /// @notice This module adds a weighted threshold validation module to a ERC6900 smart contract account. interface IWeightedMultisigValidationModule is IValidationModule { - /// @notice This event is emitted when the threshold weight is updated - /// @param account account module is installed on - /// @param oldThresholdWeight the old threshold weight required to perform an action - /// @param newThresholdWeight the new threshold weight required to perform an action - event ThresholdUpdated(address account, uint256 oldThresholdWeight, uint256 newThresholdWeight); - - error InvalidThresholdWeight(); - error SignersWeightsMismatch(); + event AccountMetadataUpdated( + address indexed account, uint32 indexed entityId, AccountMetadata oldMetadata, AccountMetadata newMetadata + ); + event SignersAdded(address indexed account, uint32 indexed entityId, SignerMetadataWithId[] addedSigners); + event SignersRemoved(address indexed account, uint32 indexed entityId, SignerMetadataWithId[] removedSigners); + event SignersUpdated(address indexed account, uint32 indexed entityId, SignerMetadataWithId[] updatedSigners); + + error InvalidSignerWeight(uint32 entityId, address account, bytes30 signerId, uint256 weight); + error ZeroThresholdWeight(uint32 entityId, address account); + error SignerWeightsLengthMismatch(uint32 entityId, address account); error ThresholdWeightExceedsTotalWeight(uint256 thresholdWeight, uint256 totalWeight); - - /// @notice Add address signers (e.g. EOA) and their associated weights for the account, and optionally update - /// threshold weight. + error TooManySigners(uint256 numSigners); + error TooFewSigners(uint256 numSigners); + error InvalidSignerMetadata(uint32 entityId, address account, SignerMetadata signerMetadata); + error SignerIdAlreadyExists(uint32 entityId, address account, bytes30 signerId); + error SignerIdDoesNotExist(uint32 entityId, address account, bytes30 signerId); + error SignerMetadataDoesNotExist(uint32 entityId, address account, bytes30 signerId); + error SignerMetadataAlreadyExists(uint32 entityId, address account, SignerMetadata signerMetaData); + error AlreadyInitialized(uint32 entityId, address account); + error Uninitialized(uint32 entityId, address account); + error EmptyThresholdWeightAndSigners(uint32 entityId, address account); + error InvalidSigLength(uint32 entityId, address account, uint256 length); + error InvalidAddress(uint32 entityId, address account, address addr); + error InvalidSigOffset(uint32 entityId, address account, uint256 offset); + error InvalidNumSigsOnActualDigest(uint32 entityId, address account, uint256 numSigs); + error InvalidUserOpDigest(uint32 entityId, address account); + + /// @notice Get the signer id. + /// @param signer The signer to check. + /// @return signer id in bytes30. + function getSignerId(address signer) external pure returns (bytes30); + + /// @notice Get the signer id. + /// @param signer The signer to check. + /// @return signer id in bytes30. + function getSignerId(PublicKey calldata signer) external pure returns (bytes30); + + /// @notice Add signers, their signers metadata for the account (msg.sender) given entity id, and + /// optionally update + /// threshold weight. Account metadata will be updated with the new signers and threshold weight. /// @dev Constraints: - /// - msg.sender must be initialized for this module - /// - signers must be non-empty + /// - msg.sender must be initialized for this module. + /// - signersToAdd must be non-empty. /// - total signers after adding new signers must not exceed MAX_SIGNERS. - /// - length of weights must be equal to length of signers. - /// - each weight must be between [1, MAX_WEIGHT], inclusive. - /// - each signer in signers must not be address(0) or an existing signer. + /// - each weight must be between [MIN_WEIGHT, MAX_WEIGHT], inclusive. + /// - each signer in signersToAdd must not be address(0), PublicKey(0,0) or an existing signer. + /// - each signer in signersToAdd must be either a valid address or a valid PublicKey. + /// - each signer in signersToAdd does not need to have signerId as it will be calculated and returned. /// - If newThresholdWeight is not equal to 0 or the current threshold, and signers are added successfully, /// the threshold weight will be updated. The threshold weight must be <= the new total weight after adding signers. - /// @param entityIds entity ids for the account and signer - /// @param signers array of address signers to be added - /// @param weights corresponding array of weights for signers to be added (must be the same length as signers). - /// @param newThresholdWeight new threshold weight to set as required to perform an action, or 0 to leave - /// unmodified. - function addSigners( - uint32[] calldata entityIds, - address[] calldata signers, - uint256[] calldata weights, - uint256 newThresholdWeight - ) external; - - /// @notice Add public key signers (e.g. passkey) and their associated weights for the account, and optionally - /// update threshold weight. - /// @dev Constraints: - /// - msg.sender must be initialized for this module - /// - signers must be non-empty - /// - total signers after adding new signers must not exceed MAX_SIGNERS. - /// - length of weights must be equal to length of signers. - /// - each weight must be between [1, MAX_WEIGHT], inclusive. - /// - each signer in signers must not be (0, 0) or an existing signer. - /// - If newThresholdWeight is not equal to 0 or the current threshold, and signers are added successfully, - /// the threshold weight will be updated. The threshold weight must be <= the new total weight after adding signers. - /// @param entityIds entity ids for the account and signer - /// @param signers array of public key signers to be added - /// @param weights corresponding array of weights for signers to be added (must be the same length as signers) - /// @param newThresholdWeight new threshold weight to set as required to perform an action, or 0 to leave - /// unmodified. - function addSigners( - uint32[] calldata entityIds, - PublicKey[] calldata signers, - uint256[] calldata weights, - uint256 newThresholdWeight - ) external; - - /// @notice Remove given address signers and set their associated weights to 0 for the account, - /// and optionally update threshold weight. - /// @dev Constraints: - /// - msg.sender must be initialized for this module - /// - signers must be non-empty - /// - Removal of signers must not set total number of signers to 0. - /// - If newThresholdWeight is not equal to 0 or the current threshold, and signers are removed successfully, - /// the threshold weight will be updated. The threshold weight must be <= the new total weight after removing - /// signers. - /// @param entityIds entity ids for the account and signer - /// @param signers array of address signers to be removed + /// @param entityId entity id for the account and signers. + /// @param signersToAdd a list of signer information to be added. Please note that the signerId field is empty. /// @param newThresholdWeight new threshold weight to set as required to perform an action, or 0 to leave /// unmodified. - function removeSigners(uint32[] calldata entityIds, address[] calldata signers, uint256 newThresholdWeight) - external; + /// @return added signers with their ids. + function addSigners(uint32 entityId, SignerMetadata[] calldata signersToAdd, uint256 newThresholdWeight) + external + returns (SignerMetadataWithId[] memory); - /// @notice Remove given public key signers and set their associated weights to 0 for the account, - /// and optionally update threshold weight. + /// @notice Remove certain (but not all) signers and their metadata for the account (msg.sender) given their + /// entityId and signer ids + /// and optionally update threshold weight. Account metadata will be updated with the new signers and threshold + /// weight. /// @dev Constraints: - /// - msg.sender must be initialized for this module - /// - signers must be non-empty + /// - msg.sender must be initialized for this module. + /// - signersToRemove must be non-empty. /// - Removal of signers must not set total number of signers to 0. /// - If newThresholdWeight is not equal to 0 or the current threshold, and signers are removed successfully, /// the threshold weight will be updated. The threshold weight must be <= the new total weight after removing /// signers. - /// @param entityIds entity ids for the account and signer - /// @param signers array of public key signers to be removed + /// @param entityId entity id for the account and signers. + /// @param signersToRemove a list of signer ids to be removed. /// @param newThresholdWeight new threshold weight to set as required to perform an action, or 0 to leave /// unmodified. - function removeSigners(uint32[] calldata entityIds, PublicKey[] calldata signers, uint256 newThresholdWeight) - external; + function removeSigners(uint32 entityId, bytes30[] calldata signersToRemove, uint256 newThresholdWeight) external; - /// @notice Update address signers' weights for the account, and/or update threshold weight. + /// @notice Update the signers' weights for the account along with the threshold weight, + /// or update only the threshold weight. /// @dev Constraints: - /// - all signers updated must currently have non-zero weight - /// - all new weights must be in range [1, MAX_WEIGHT] + /// - All signers updated must currently have non-zero weight. + /// - each signer in non-empty signersToUpdate must have a signerId. + /// - each signer in non-empty signersToUpdate must have a valid new weight. + /// - each signer in signersToUpdate does not need to have addr or publicKey as signerId will be used. + /// - All new weights must be in range [MIN_WEIGHT, MAX_WEIGHT]. /// - If a newThresholdWeight is nonzero, the threshold weight will be updated. Updating threshold weight does not - /// require modifying signers. - /// The newThresholdWeight must be <= the new total weight. - /// @param entityIds entity ids for the account and signer - /// @param signers array of address signers to be updated - /// @param weights corresponding array of weights for signers + /// require modifying signer weight. The newThresholdWeight must be <= the new total weight. + /// @param entityId entity id for the account and signers. + /// @param signersToUpdate a list of signer weights to be updated given its id. /// @param newThresholdWeight new threshold weight to set as required to perform an action, or 0 to leave /// unmodified. - function updateWeights( - uint32[] calldata entityIds, - address[] calldata signers, - uint256[] calldata weights, - uint256 newThresholdWeight - ) external; - - /// @notice Update public key signers' weights for the account, and/or update threshold weight. - /// @dev Constraints: - /// - all signers updated must currently have non-zero weight - /// - all new weights must be in range [1, MAX_WEIGHT] - /// - If a newThresholdWeight is nonzero, the threshold weight will be updated. Updating threshold weight does not - /// require modifying signers. - /// The newThresholdWeight must be <= the new total weight. - /// @param entityIds entity ids for the account and signer - /// @param signers array of public key signers to be updated - /// @param weights corresponding array of weights for signers - /// @param newThresholdWeight new threshold weight to set as required to perform an action, or 0 to leave - /// unmodified. - function updateWeights( - uint32[] calldata entityIds, - PublicKey[] calldata signers, - uint256[] calldata weights, - uint256 newThresholdWeight - ) external; + function updateWeights(uint32 entityId, SignerMetadataWithId[] calldata signersToUpdate, uint256 newThresholdWeight) + external; /// @notice Check if the nSignaturesInput is valid for the account. /// @param nSignaturesInput has the following fields: @@ -167,19 +134,26 @@ interface IWeightedMultisigValidationModule is IValidationModule { /// @return firstFailure first failure, if failed is true. /// (Note: if all signatures are individually valid but do not satisfy the /// multisig, firstFailure will be set to the last signature's index.) - function checkNSignatures(CheckNSignaturesInput calldata nSignaturesInput) + function checkNSignatures(CheckNSignaturesRequest calldata nSignaturesInput) external view returns (bool success, uint256 firstFailure); - /// @notice Return signer data of an account. - /// @param entityId entity id for the account and signer. - /// @param account the account to get signerData of. - /// @return signerData signersData[entityId][account]. - function signerDataOf(uint32 entityId, address account) external view returns (SignerData memory signerData); + /// @notice Return all the signer metadata of an account. + /// @param entityId entity id for the account and signers. + /// @param account the account to return signerMetaData of. + /// @return signersMetadataWithId a list of signer metadata with ids. + function signersMetadataOf(uint32 entityId, address account) + external + view + returns (SignerMetadataWithId[] memory signersMetadataWithId); /// @notice Get the metadata of an account, their respective number of signers, threshold weight, and total weight. - /// @param account the account to get the metadata of. + /// @param entityId entity id for the account and signers. + /// @param account the account to return the metadata of. /// @return accountMetadata account metadata. - function accountMetadataOf(address account) external view returns (AccountMetadata memory accountMetadata); + function accountMetadataOf(uint32 entityId, address account) + external + view + returns (AccountMetadata memory accountMetadata); } diff --git a/src/msca/6900/v0.8/modules/multisig/MultisigConstants.sol b/src/msca/6900/v0.8/modules/multisig/MultisigConstants.sol index 64e3e30..9ee189e 100644 --- a/src/msca/6900/v0.8/modules/multisig/MultisigConstants.sol +++ b/src/msca/6900/v0.8/modules/multisig/MultisigConstants.sol @@ -20,4 +20,6 @@ pragma solidity 0.8.24; uint256 constant MAX_SIGNERS = 1000; +uint256 constant MIN_WEIGHT = 1; + uint256 constant MAX_WEIGHT = 1000000; diff --git a/src/msca/6900/v0.8/modules/multisig/MultisigStructs.sol b/src/msca/6900/v0.8/modules/multisig/MultisigStructs.sol index c92345a..85692b5 100644 --- a/src/msca/6900/v0.8/modules/multisig/MultisigStructs.sol +++ b/src/msca/6900/v0.8/modules/multisig/MultisigStructs.sol @@ -20,17 +20,25 @@ pragma solidity 0.8.24; import {PublicKey} from "../../../../../common/CommonStructs.sol"; -/// @notice For credential, we either store public key or address but not both. -/// @param addr, signer address +/// @notice We either store public key or address but not both for the convenience of lookup. /// @param weight, weightage on each signer -/// @param publicKeyX, x coordinate of public key -/// @param publicKeyY, y coordinate of public key -struct SignerData { +/// @param addr, signer address +/// @param publicKey, x and y coordinate of public key +struct SignerMetadata { uint256 weight; address addr; + // OR PublicKey publicKey; } +/// @notice Return the id along with the signer metadata. This struct is not persisted in storage. +/// @param signerMetadata, metadata of the signer +/// @param signer id, unique identifier for the signer +struct SignerMetadataWithId { + SignerMetadata signerMetadata; + bytes30 signerId; +} + /// @notice Metadata of an account. /// @param numSigners number of signers on the account /// @param thresholdWeight weight of signatures required to perform an action @@ -41,16 +49,34 @@ struct AccountMetadata { uint256 totalWeight; } -/// @notice Data for verifying signatures. +/// @notice Request data for verifying signatures. /// @param entityId entity id for the account and signer /// @param actualDigest actual digest signed /// @param minimalDigest minimal digest signed +/// @param requiredNumSigsOnActualDigest number of signatures required on actual digest, if the actual and minimal +/// digests differ, make sure we have exactly 1 sig on the actual digest /// @param account account address /// @param signatures encoded signatures -struct CheckNSignaturesInput { +struct CheckNSignaturesRequest { uint32 entityId; bytes32 actualDigest; bytes32 minimalDigest; + uint256 requiredNumSigsOnActualDigest; address account; bytes signatures; } + +/// @notice Context for checkNSignatures. +struct CheckNSignaturesContext { + // lowestOffset of signature dynamic part, must locate after the signature constant part + // 0 means we only have EOA signer so far + uint256 lowestSigDynamicPartOffset; + bytes30 lastSigner; + bytes30 currentSigner; + bool success; + uint256 firstFailure; + // first32Bytes of signature constant part + bytes32 first32Bytes; + // second32Bytes of signature constant part + bytes32 second32Bytes; +} diff --git a/src/msca/6900/v0.8/modules/multisig/README.md b/src/msca/6900/v0.8/modules/multisig/README.md index 37b56fe..e05dbc2 100644 --- a/src/msca/6900/v0.8/modules/multisig/README.md +++ b/src/msca/6900/v0.8/modules/multisig/README.md @@ -4,7 +4,7 @@ Weighted Multisig Module is an ERC6900-compatible weighted multisig validation m ## Core Functionalities Core features include: 1. Weighted multisig validation on execution functions. -2. Execution functions that modify account by adding signers, removing signers, or updating signer weights, and/or modifying the threshold weight. These functions are guarded by the above validation function. +2. Execution functions that modify account by adding signers, removing signers, updating signer weights, and/or modifying the threshold weight. These functions are guarded by the above validation function. 3. Support for EOA signers, ERC-1271 smart contract signers and public key signers. ## Technical Decisions diff --git a/src/msca/6900/v0.8/modules/multisig/WeightedMultisigValidationModule.sol b/src/msca/6900/v0.8/modules/multisig/WeightedMultisigValidationModule.sol new file mode 100644 index 0000000..abbec82 --- /dev/null +++ b/src/msca/6900/v0.8/modules/multisig/WeightedMultisigValidationModule.sol @@ -0,0 +1,945 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + + * SPDX-License-Identifier: GPL-3.0-or-later + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +pragma solidity 0.8.24; + +import {CalldataUtils} from "../../../../../utils/CalldataUtils.sol"; +import { + AccountMetadata, + CheckNSignaturesContext, + CheckNSignaturesRequest, + SignerMetadata, + SignerMetadataWithId +} from "./MultisigStructs.sol"; + +import {BaseERC712CompliantModule} from "../../../shared/erc712/BaseERC712CompliantModule.sol"; +import { + AssociatedLinkedListSet, + AssociatedLinkedListSetLib +} from "@modular-account-libs/libraries/AssociatedLinkedListSetLib.sol"; + +import {CredentialType, PublicKey, WebAuthnSigDynamicPart} from "../../../../../common/CommonStructs.sol"; + +import { + EIP1271_INVALID_SIGNATURE, + EIP1271_VALID_SIGNATURE, + EMPTY_HASH, + SIG_VALIDATION_FAILED, + SIG_VALIDATION_SUCCEEDED, + ZERO, + ZERO_BYTES32 +} from "../../../../../common/Constants.sol"; +import {BaseModule} from "../BaseModule.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +import {IModule} from "@erc6900/reference-implementation/interfaces/IModule.sol"; +import {IValidationModule} from "@erc6900/reference-implementation/interfaces/IValidationModule.sol"; +import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol"; + +import {IWeightedMultisigValidationModule} from "./IWeightedMultisigValidationModule.sol"; +import {MAX_SIGNERS, MAX_WEIGHT, MIN_WEIGHT} from "./MultisigConstants.sol"; + +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +import {PublicKeyLib} from "../../../../../libs/PublicKeyLib.sol"; +import {NotImplementedFunction} from "../../../shared/common/Errors.sol"; +import {PackedUserOperation} from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; + +import {SetValueLib} from "../../../../../libs/SetValueLib.sol"; + +import {WebAuthnLib} from "../../../../../libs/WebAuthnLib.sol"; +import {CalldataUtils} from "../../../../../utils/CalldataUtils.sol"; +import {UserOperationLib} from "@account-abstraction/contracts/core/UserOperationLib.sol"; +import {SetValue} from "@modular-account-libs/libraries/Constants.sol"; +import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; + +/// @title Weighted Multisig Module. +/// @author Circle +/// @notice We support different weighting rules based on entityId. If you have a gas spending use case +/// that only requires one signature, you can assign your desired signers group to entityId(0). +/// However, if you need more than two signatures for any spending over $1000, you can assign +/// the desired signers group to a different entityId (1). +/// signerId is a unique identifier for each signer in the multisig group. If the signer is an address, +/// then signerId == bytes30(keccak256(abi.encode(CredentialType.ADDRESS, addr))). If the signer is a public key, then +/// signerId == bytes30(keccak256(abi.encode(CredentialType.PUBLIC_KEY, publicKey.x, publicKey.y))). +contract WeightedMultisigValidationModule is + IWeightedMultisigValidationModule, + BaseERC712CompliantModule, + BaseModule +{ + using ECDSA for bytes32; + using PublicKeyLib for PublicKey[]; + using PublicKeyLib for PublicKey; + using MessageHashUtils for bytes32; + using SetValueLib for SetValue[]; + using AssociatedLinkedListSetLib for AssociatedLinkedListSet; + using CalldataUtils for bytes; + using UserOperationLib for PackedUserOperation; + + // a unique identifier in the format "vendor.module.semver" for the account implementation + string public constant MODULE_ID = "circle.weighted-multisig-module.1.0.0"; + // keccak256("CircleWeightedMultisigMessage(bytes message)") + bytes32 private constant _MODULE_TYPEHASH = 0x77086513965446054aa0ac031b0cbbd4f343b25cbe864d787c5102e28d6b40bc; + // keccak256("circle.weighted-multisig-module.1.0.0") + bytes32 private constant _HASHED_MODULE_ID = 0x224dce5084de9b5d64cd245a83e348c785d73b74ff216928f9c4276e52d60a1a; + // keccak256("1.0.0") + bytes32 private constant _HASHED_MODULE_VERSION = 0x06c015bd22b4c69690933c1058878ebdfef31f9aaae40bbe86d8a09fe1b2972c; + + uint256 internal constant _INDIVIDUAL_SIGNATURE_BYTES_LEN = 65; + + address public immutable ENTRYPOINT; + // AssociatedLinkedListSet has an internal mapping that goes from + // associated account => address signer, + // so signersPerEntityId[entityId] still remains within account associated storage + // this stores the signers for each entity id associated with the account. + // entityId(0) => [address(1), address(2) ...] + mapping(uint32 entityId => AssociatedLinkedListSet signers) public signersPerEntity; + // signersMetadataPerEntityId stores the signer metadata such as weight, optional address or public key information + // for each entityId, signerId and account + mapping(uint32 entityId => mapping(bytes30 signerId => mapping(address account => SignerMetadata))) public + signersMetadataPerEntity; + /// accountMetadata stores the metadata for each account and entity id, + /// this allows for different weighting rules, even for the same account + mapping(uint32 entityId => mapping(address account => AccountMetadata)) public accountMetadataPerEntity; + + constructor(address entryPoint) { + ENTRYPOINT = entryPoint; + } + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Execution functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + /// @inheritdoc IValidationModule + function validateUserOp(uint32 entityId, PackedUserOperation calldata userOp, bytes32 userOpHash) + external + view + override + returns (uint256) + { + // userOp.sig format: + // 0 to n: Constant parts of k signatures, with each constant part being 65 bytes + // n onward: Dynamic parts of the signatures, if any + bytes32 actualUserOpDigest = userOpHash.toEthSignedMessageHash(); + (bytes32 minimalUserOpDigest, address sender) = _getMinimalUserOpDigest(userOp); + // actualUserOpDigest must differ from minimalUserOpDigest in userOp + // requiredNumSigsOnActualDigest is always one for validateUserOp + if (actualUserOpDigest == minimalUserOpDigest) { + revert InvalidUserOpDigest(entityId, sender); + } + bool success; + uint256 firstFailure; + (success, firstFailure) = checkNSignatures( + CheckNSignaturesRequest({ + entityId: entityId, + actualDigest: actualUserOpDigest, + minimalDigest: minimalUserOpDigest, + requiredNumSigsOnActualDigest: 1, + account: sender, + signatures: userOp.signature + }) + ); + return success ? SIG_VALIDATION_SUCCEEDED : SIG_VALIDATION_FAILED; + } + + /// @inheritdoc IValidationModule + function validateRuntime( + address account, + uint32 entityId, + address sender, + uint256 value, + bytes calldata data, + bytes calldata authorization + ) external pure override { + // TODO: implement this - the signatures can be put in the validationData field of the runtime validation + // function + (account, sender, value, data, authorization); + revert NotImplementedFunction(msg.sig, entityId); + } + + /// @inheritdoc IWeightedMultisigValidationModule + function addSigners(uint32 entityId, SignerMetadata[] calldata signersToAdd, uint256 newThresholdWeight) + external + override + returns (SignerMetadataWithId[] memory) + { + // data input validations + if (signersToAdd.length == 0) { + revert TooFewSigners(signersToAdd.length); + } + if (signersToAdd.length > MAX_SIGNERS) { + revert TooManySigners(signersToAdd.length); + } + AccountMetadata memory currentAccountMetadata = accountMetadataPerEntity[entityId][msg.sender]; + if (_isUninitializedAccountMetadata(currentAccountMetadata)) { + revert Uninitialized(entityId, msg.sender); + } + (uint256 totalWeightAdded, SignerMetadataWithId[] memory signersAdded) = + _addSigners(msg.sender, entityId, signersToAdd); + // update the numSigners, totalWeight and thresholdWeight + _updateAccountMetadata({ + account: msg.sender, + entityId: entityId, + numSigners: currentAccountMetadata.numSigners + signersAdded.length, // existing + new signers + totalWeight: currentAccountMetadata.totalWeight + totalWeightAdded, // existing + new total weight + thresholdWeight: newThresholdWeight, // new threshold weight + isUninstall: false, // not called by uninstall + currentAccountMetadata: currentAccountMetadata + }); + return signersAdded; + } + + /// @inheritdoc IWeightedMultisigValidationModule + function removeSigners(uint32 entityId, bytes30[] calldata signersToRemove, uint256 newThresholdWeight) + external + override + { + // data input validations + if (signersToRemove.length == 0) { + revert TooFewSigners(signersToRemove.length); + } + if (signersToRemove.length > MAX_SIGNERS) { + revert TooManySigners(signersToRemove.length); + } + AccountMetadata memory currentAccountMetadata = accountMetadataPerEntity[entityId][msg.sender]; + if (_isUninitializedAccountMetadata(currentAccountMetadata)) { + revert Uninitialized(entityId, msg.sender); + } + uint256 totalWeightRemoved = _removeSigners(msg.sender, entityId, signersToRemove); + // update the numSigners, totalWeight and thresholdWeight + _updateAccountMetadata({ + account: msg.sender, + entityId: entityId, + numSigners: currentAccountMetadata.numSigners - signersToRemove.length, // existing - deleted + // signers + totalWeight: currentAccountMetadata.totalWeight - totalWeightRemoved, // existing - deleted total weight + thresholdWeight: newThresholdWeight, // keep the current threshold weight + isUninstall: false, // not called by uninstall + currentAccountMetadata: currentAccountMetadata + }); + } + + /// @inheritdoc IWeightedMultisigValidationModule + function updateWeights(uint32 entityId, SignerMetadataWithId[] calldata signersToUpdate, uint256 newThresholdWeight) + external + override + { + AccountMetadata memory currentAccountMetadata = accountMetadataPerEntity[entityId][msg.sender]; + if (_isUninitializedAccountMetadata(currentAccountMetadata)) { + revert Uninitialized(entityId, msg.sender); + } + if (newThresholdWeight == 0 && signersToUpdate.length == 0) { + revert EmptyThresholdWeightAndSigners(entityId, msg.sender); + } + uint256 totalWeightAdded = 0; + uint256 totalWeightRemoved = 0; + // update the signer weights + if (signersToUpdate.length > 0) { + (totalWeightAdded, totalWeightRemoved) = _updateSignerWeights(msg.sender, entityId, signersToUpdate); + } + // update the totalWeight and thresholdWeight + _updateAccountMetadata({ + account: msg.sender, + entityId: entityId, + numSigners: currentAccountMetadata.numSigners, + totalWeight: currentAccountMetadata.totalWeight + totalWeightAdded - totalWeightRemoved, // existing + delta + // of updated total weight + thresholdWeight: newThresholdWeight, // new threshold weight + isUninstall: false, // not called by uninstall + currentAccountMetadata: currentAccountMetadata + }); + } + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Execution view functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + /// @inheritdoc IValidationModule + function validateSignature(address account, uint32 entityId, address sender, bytes32 hash, bytes memory signature) + external + view + override + returns (bytes4) + { + (sender); + bytes32 replaySafeHash = getReplaySafeMessageHash(account, hash); + bool success; + uint256 firstFailure; + (success, firstFailure) = checkNSignatures( + CheckNSignaturesRequest({ + entityId: entityId, + actualDigest: replaySafeHash, + minimalDigest: replaySafeHash, + requiredNumSigsOnActualDigest: 0, + account: account, + signatures: signature + }) + ); + return success ? EIP1271_VALID_SIGNATURE : EIP1271_INVALID_SIGNATURE; + } + + /// @inheritdoc IWeightedMultisigValidationModule + function checkNSignatures(CheckNSignaturesRequest memory request) + public + view + override + returns (bool success, uint256 firstFailure) + { + if (request.signatures.length < _INDIVIDUAL_SIGNATURE_BYTES_LEN) { + revert InvalidSigLength(request.entityId, request.account, request.signatures.length); + } + AccountMetadata memory currentAccountMetadata = accountMetadataPerEntity[request.entityId][request.account]; + if (_isUninitializedAccountMetadata(currentAccountMetadata)) { + revert Uninitialized(request.entityId, request.account); + } + // `thresholdWeight` represents the minimum weight needed to perform an action and must not be zero, as verified + // in `_updateAccountMetadata` + uint256 thresholdWeight = currentAccountMetadata.thresholdWeight; + CheckNSignaturesContext memory context; + uint256 accumulatedWeight; + // located in the lastByte of signature constant part + uint8 sigType; + uint256 signatureCount; + // tracks whether `signatures` is a complete and valid multisig signature + context.success = true; + uint256 currentWeight; + while (accumulatedWeight < thresholdWeight) { + // check signature constant part length + _checkNextSigConstantPartLength(context, signatureCount, request.signatures.length); + if (context.success == false) { + return (false, context.firstFailure); + } + + (sigType, context.first32Bytes, context.second32Bytes) = + _splitSigConstantPart(request.signatures, signatureCount); + bytes32 digest; + // sigType is normalized for actualDigest + (digest, sigType) = _getDigestAndNormalizeSigType(sigType, request); + // verify each signature + if (sigType == 0) { + currentWeight = _validateContractSignature(context, request, digest, signatureCount); + } else if (sigType == 2) { + currentWeight = _validateWebauthnSignature(context, request, digest, signatureCount); + } else { + // reverts if signature has the wrong s value, wrong v value, or if it's a bad point on the k1 curve + currentWeight = _validateEOASignature(context, request, digest, signatureCount, sigType); + } + if ( + // fail if the signature is out of order or duplicate or is from an unknown signer + context.currentSigner <= context.lastSigner + || !signersPerEntity[request.entityId].contains(request.account, SetValue.wrap(context.currentSigner)) + ) { + if (context.success) { + context.firstFailure = signatureCount; + context.success = false; + } + } + + accumulatedWeight += currentWeight; + context.lastSigner = context.currentSigner; + signatureCount++; + } + + // if we need a signature on the actual digest, and we didn't get exactly one, revert, + // we avoid reverting early to facilitate fee estimation + if (request.requiredNumSigsOnActualDigest != 0) { + revert InvalidNumSigsOnActualDigest( + request.entityId, request.account, request.requiredNumSigsOnActualDigest + ); + } + return (context.success, context.firstFailure); + } + + /// @inheritdoc IWeightedMultisigValidationModule + function getSignerId(address signer) external pure returns (bytes30) { + return _getSignerId(signer); + } + + /// @inheritdoc IWeightedMultisigValidationModule + function getSignerId(PublicKey calldata signer) external pure returns (bytes30) { + return _getSignerId(signer); + } + + /// @inheritdoc IWeightedMultisigValidationModule + function signersMetadataOf(uint32 entityId, address account) + external + view + override + returns (SignerMetadataWithId[] memory signersMetadataWithId) + { + // return the most recent signer first + bytes30[] memory signerIds = signersPerEntity[entityId].getAll(account).toBytes30Array(); + signersMetadataWithId = new SignerMetadataWithId[](signerIds.length); + for (uint256 i = 0; i < signerIds.length; ++i) { + signersMetadataWithId[signerIds.length - i - 1].signerMetadata = + signersMetadataPerEntity[entityId][signerIds[i]][account]; + signersMetadataWithId[signerIds.length - i - 1].signerId = signerIds[i]; + } + return signersMetadataWithId; + } + + /// @inheritdoc IWeightedMultisigValidationModule + function accountMetadataOf(uint32 entityId, address account) + external + view + override + returns (AccountMetadata memory) + { + return accountMetadataPerEntity[entityId][account]; + } + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Module interface functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + /// @inheritdoc IModule + function onInstall(bytes calldata data) external override { + if (data.length == 0) { + // the caller already checks before calling into onInstall, this is just a safety check + return; + } + (uint32 entityId, SignerMetadata[] memory signersToAdd, uint256 thresholdWeight) = + abi.decode(data, (uint32, SignerMetadata[], uint256)); + // data input validations + if (thresholdWeight == 0) { + revert ZeroThresholdWeight(entityId, msg.sender); + } + if (signersToAdd.length == 0) { + revert TooFewSigners(signersToAdd.length); + } + if (signersToAdd.length > MAX_SIGNERS) { + revert TooManySigners(signersToAdd.length); + } + AccountMetadata memory currentAccountMetadata = accountMetadataPerEntity[entityId][msg.sender]; + if (!_isUninitializedAccountMetadata(currentAccountMetadata)) { + revert AlreadyInitialized(entityId, msg.sender); + } + // add signers metadata + // onInstall does not return any values for now, so the caller need to call the view functions getSignerId() to + // return the signerId + (uint256 totalWeightAdded,) = _addSigners(msg.sender, entityId, signersToAdd); + // add the numSigners, totalWeight and thresholdWeight + _updateAccountMetadata({ + account: msg.sender, + entityId: entityId, + numSigners: signersToAdd.length, + totalWeight: totalWeightAdded, + thresholdWeight: thresholdWeight, + isUninstall: false, // not called by uninstall + currentAccountMetadata: currentAccountMetadata + }); + } + + /// @inheritdoc IModule + function onUninstall(bytes calldata data) external override { + if (data.length == 0) { + // the caller already checks before calling into it, this is just a safety check + return; + } + uint32 entityId = abi.decode(data, (uint32)); + AccountMetadata memory currentAccountMetadata = accountMetadataPerEntity[entityId][msg.sender]; + if (_isUninitializedAccountMetadata(currentAccountMetadata)) { + revert Uninitialized(entityId, msg.sender); + } + // delete the numSigners, totalWeight and thresholdWeight + _updateAccountMetadata({ + account: msg.sender, + entityId: entityId, + numSigners: 0, + totalWeight: 0, + thresholdWeight: 0, + isUninstall: true, // called by uninstall, we do not require any signers after uninstallation + currentAccountMetadata: currentAccountMetadata + }); + // remove all signers + _removeAllSigners(msg.sender, entityId); + } + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Module only view functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + /// @inheritdoc IModule + function moduleId() external pure returns (string memory) { + return MODULE_ID; + } + + /// @inheritdoc BaseERC712CompliantModule + function _getModuleTypeHash() internal pure override returns (bytes32) { + return _MODULE_TYPEHASH; + } + + /// @inheritdoc BaseERC712CompliantModule + function _getModuleNameHash() internal pure override returns (bytes32) { + return _HASHED_MODULE_ID; + } + + /// @inheritdoc BaseERC712CompliantModule + function _getModuleVersionHash() internal pure override returns (bytes32) { + return _HASHED_MODULE_VERSION; + } + + // ┏━━━━━━━━━━━━━━━┓ + // ┃ EIP-165 ┃ + // ┗━━━━━━━━━━━━━━━┛ + /// @inheritdoc BaseModule + function supportsInterface(bytes4 interfaceId) public view override(BaseModule, IERC165) returns (bool) { + return interfaceId == type(IValidationModule).interfaceId + || interfaceId == type(IWeightedMultisigValidationModule).interfaceId || super.supportsInterface(interfaceId); + } + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Internal Functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + /// @notice Adds signer metadata (weight, address or publicKey) for an account and entity + /// id, or reverts if any of them cannot be added. + /// @param account account to add signers and metadata to. + /// @param entityId entity id for the account. + /// @param signersMetadata a list of signer metadata. + /// @return total weight added and a list of signer metadata updated with id. + function _addSigners(address account, uint32 entityId, SignerMetadata[] memory signersMetadata) + internal + returns (uint256, SignerMetadataWithId[] memory) + { + uint256 totalWeightAdded = 0; + SignerMetadataWithId[] memory signersMetadataWithId = new SignerMetadataWithId[](signersMetadata.length); + for (uint256 i = 0; i < signersMetadata.length; ++i) { + signersMetadataWithId[i].signerId = _addSignerMetadata(account, entityId, signersMetadata[i]); + signersMetadataWithId[i].signerMetadata = signersMetadata[i]; + totalWeightAdded += signersMetadata[i].weight; + // store the signerId, revert if it's already added + if (!signersPerEntity[entityId].tryAdd(account, SetValue.wrap(signersMetadataWithId[i].signerId))) { + // shouldn't happen because SignerMetadataAlreadyExists reverts first + revert SignerIdAlreadyExists(entityId, account, signersMetadataWithId[i].signerId); + } + } + // emit event + emit SignersAdded(account, entityId, signersMetadataWithId); + return (totalWeightAdded, signersMetadataWithId); + } + + /// @notice Removes signers and their metadata for an account, entity id. + /// @param account account to remove signers and metadata from. + /// @param entityId entity id for the account and signers. + /// @param signersToRemove a list of signer ids to be removed. + function _removeSigners(address account, uint32 entityId, bytes30[] calldata signersToRemove) + internal + returns (uint256 totalWeightRemoved) + { + // remove signers metadata + SignerMetadataWithId[] memory deletedSignersMetadata = new SignerMetadataWithId[](signersToRemove.length); + for (uint256 i = 0; i < signersToRemove.length; ++i) { + // this is O(n) operation for the time being + if (!signersPerEntity[entityId].tryRemove(account, SetValue.wrap(signersToRemove[i]))) { + revert SignerIdDoesNotExist(entityId, account, signersToRemove[i]); + } + deletedSignersMetadata[i].signerMetadata = signersMetadataPerEntity[entityId][signersToRemove[i]][account]; + deletedSignersMetadata[i].signerId = signersToRemove[i]; + totalWeightRemoved += deletedSignersMetadata[i].signerMetadata.weight; + delete signersMetadataPerEntity[entityId][signersToRemove[i]][account]; + } + emit SignersRemoved(account, entityId, deletedSignersMetadata); + return totalWeightRemoved; + } + + /// @notice Removes all signers and their metadata for an account and entity id. + /// @param account account to remove signers and metadata from. + /// @param entityId entity id for the account and signers. + function _removeAllSigners(address account, uint32 entityId) internal { + bytes30[] memory signerIds = signersPerEntity[entityId].getAll(account).toBytes30Array(); + signersPerEntity[entityId].clear(account); + SignerMetadataWithId[] memory deletedSignersMetadata = new SignerMetadataWithId[](signerIds.length); + for (uint256 i = 0; i < signerIds.length; ++i) { + deletedSignersMetadata[i].signerMetadata = signersMetadataPerEntity[entityId][signerIds[i]][account]; + deletedSignersMetadata[i].signerId = signerIds[i]; + delete signersMetadataPerEntity[entityId][signerIds[i]][account]; + } + // emit event + emit SignersRemoved(account, entityId, deletedSignersMetadata); + } + + /// @notice Updates signer weights for an account, entity id and a list of signers. + /// @param account account to update weight from. + /// @param entityId entity id for the account and signers. + /// @param signersToUpdate a list of signer weights to be updated given its id. + /// @return total weight added and total weight removed. + function _updateSignerWeights(address account, uint32 entityId, SignerMetadataWithId[] calldata signersToUpdate) + internal + returns (uint256, uint256) + { + uint256 totalWeightAdded = 0; + uint256 totalWeightRemoved = 0; + uint256 currentWeight = 0; + uint256 newWeight = 0; + // we don't need to check (signersToUpdate.length == 0) + // because the caller already checks before calling into it, + // also signersToUpdate.length == 0 is allowed in the caller + // update signer weights based on the signer ids + for (uint256 i = 0; i < signersToUpdate.length; ++i) { + _validateWeight(account, entityId, signersToUpdate[i].signerId, signersToUpdate[i].signerMetadata.weight); + // `currentWeight` will never be zero because zero weights are not allowed during writes. + // If the signerId is not found, it will revert as well. + currentWeight = signersMetadataPerEntity[entityId][signersToUpdate[i].signerId][account].weight; + if (currentWeight == 0) { + revert SignerMetadataDoesNotExist(entityId, account, signersToUpdate[i].signerId); + } + newWeight = signersToUpdate[i].signerMetadata.weight; + if (newWeight > currentWeight) { + totalWeightAdded += newWeight - currentWeight; + signersMetadataPerEntity[entityId][signersToUpdate[i].signerId][account].weight = newWeight; + } else if (newWeight < currentWeight) { + totalWeightRemoved += currentWeight - newWeight; + signersMetadataPerEntity[entityId][signersToUpdate[i].signerId][account].weight = newWeight; + } + // no update if currentWeight == signersToUpdate[i].weight + } + emit SignersUpdated(account, entityId, signersToUpdate); + return (totalWeightAdded, totalWeightRemoved); + } + + /// @notice Updates account metadata (numSigners, totalWeight and thresholdWeight), or reverts if any of them cannot + /// be added. + /// @param account account to update account metadata for. + /// @param entityId entity id for the account and signers. + /// @param numSigners new num of signers. + /// @param totalWeight new total weight of the signers. + /// @param thresholdWeight new threshold weight for the account and entityId, 0 to leave unmodified for + /// non-uninstall cases. + /// @param isUninstall is called by uninstall function. + /// @param currentAccountMetadata current account metadata before update. + function _updateAccountMetadata( + address account, + uint32 entityId, + uint256 numSigners, + uint256 totalWeight, + uint256 thresholdWeight, + bool isUninstall, + AccountMetadata memory currentAccountMetadata + ) internal { + if (numSigners > MAX_SIGNERS) { + revert TooManySigners(numSigners); + } + // we don't allow 0 signers for non-uninstall cases + if (!isUninstall && numSigners == 0) { + revert TooFewSigners(numSigners); + } + // update account metadata only if there is a change + if (numSigners != currentAccountMetadata.numSigners) { + accountMetadataPerEntity[entityId][account].numSigners = numSigners; + } + if (totalWeight != currentAccountMetadata.totalWeight) { + accountMetadataPerEntity[entityId][account].totalWeight = totalWeight; + } + if (isUninstall) { + accountMetadataPerEntity[entityId][account].thresholdWeight = 0; + } else { + // for non-uninstall cases, 0 means unmodified, so we don't update the threshold weight, + // we set to the new threshold weight if it's different from the current value and non-zero (modified) + if (thresholdWeight != 0 && thresholdWeight != currentAccountMetadata.thresholdWeight) { + accountMetadataPerEntity[entityId][account].thresholdWeight = thresholdWeight; + } + } + // ensure that the updated threshold weight (if modified) does not exceed the total weight + if ( + accountMetadataPerEntity[entityId][account].totalWeight + < accountMetadataPerEntity[entityId][account].thresholdWeight + ) { + revert ThresholdWeightExceedsTotalWeight( + accountMetadataPerEntity[entityId][account].thresholdWeight, + accountMetadataPerEntity[entityId][account].totalWeight + ); + } + emit AccountMetadataUpdated( + account, entityId, currentAccountMetadata, accountMetadataPerEntity[entityId][account] + ); + } + + /// @notice Add signer metadata. Revert if the signer metadata is invalid or already added. + /// @param account account to add signer metadata to + /// @param entityId entity id for the account and signer + /// @param signerMetadata signer metadata + /// @return the signer id + function _addSignerMetadata(address account, uint32 entityId, SignerMetadata memory signerMetadata) + internal + returns (bytes30) + { + // we only allow either the address or public key to be set in the same input + if ( + (signerMetadata.addr == address(0) && signerMetadata.publicKey.isValidPublicKey()) + || (signerMetadata.addr != address(0) && !signerMetadata.publicKey.isValidPublicKey()) + ) { + // check if the signer metadata is already added + bytes30 signerId; + // only an address or public key is permitted, and it has already been validated + if (signerMetadata.addr != address(0)) { + signerId = _getSignerId(signerMetadata.addr); + } else { + signerId = _getSignerId(signerMetadata.publicKey); + } + _validateWeight(account, entityId, signerId, signerMetadata.weight); + SignerMetadata memory existingSignerMetadata = signersMetadataPerEntity[entityId][signerId][account]; + if ( + existingSignerMetadata.addr != address(0) || existingSignerMetadata.publicKey.isValidPublicKey() + || existingSignerMetadata.weight != 0 + ) { + revert SignerMetadataAlreadyExists(entityId, account, existingSignerMetadata); + } + signersMetadataPerEntity[entityId][signerId][account].weight = signerMetadata.weight; + if (signerMetadata.addr != address(0)) { + signersMetadataPerEntity[entityId][signerId][account].addr = signerMetadata.addr; + } else { + signersMetadataPerEntity[entityId][signerId][account].publicKey = signerMetadata.publicKey; + } + return signerId; + } else { + revert InvalidSignerMetadata(entityId, account, signerMetadata); + } + } + + function _validateWeight(address account, uint32 entityId, bytes30 signerId, uint256 weight) internal pure { + if (weight < MIN_WEIGHT || weight > MAX_WEIGHT) { + revert InvalidSignerWeight(entityId, account, signerId, weight); + } + } + + function _getSignerId(address signer) internal pure returns (bytes30) { + return bytes30(keccak256(abi.encode(CredentialType.ADDRESS, signer))); + } + + function _getSignerId(PublicKey memory signer) internal pure returns (bytes30) { + return bytes30(keccak256(abi.encode(CredentialType.PUBLIC_KEY, signer.x, signer.y))); + } + + function _isUninitializedAccountMetadata(AccountMetadata memory acctMetadata) internal pure returns (bool) { + return acctMetadata.numSigners == 0; + } + + function _checkNextSigConstantPartLength( + CheckNSignaturesContext memory context, + uint256 signatureCount, + uint256 sigLength + ) internal pure { + // Fail if the next 65 bytes would exceed signature length + // or lowest dynamic part signature offset, where next 65 bytes is defined as + // [signatureCount * _INDIVIDUAL_SIGNATURE_BYTES_LEN, signatureCount * _INDIVIDUAL_SIGNATURE_BYTES_LEN + + // _INDIVIDUAL_SIGNATURE_BYTES_LEN) + // exclusive + uint256 sigConstantPartEndPos = + signatureCount * _INDIVIDUAL_SIGNATURE_BYTES_LEN + _INDIVIDUAL_SIGNATURE_BYTES_LEN; + if ( + // do not fail if we only have EOA signer so far + (context.lowestSigDynamicPartOffset != 0 && sigConstantPartEndPos > context.lowestSigDynamicPartOffset) + || sigConstantPartEndPos > sigLength + ) { + if (context.success) { + // 1st failure + context.firstFailure = signatureCount; + context.success = false; + } + } + } + + /// @dev Helper function to get a 65 byte signature constant part from a multi-signature + /// @dev Functions using this must make sure signatures is long enough to contain + /// the signature (65 * pos + 65 bytes.) + /// @param signatures signatures to split + /// @param pos position in signatures + function _splitSigConstantPart(bytes memory signatures, uint256 pos) + internal + pure + returns (uint8 v, bytes32 r, bytes32 s) + { + // solhint-disable-next-line no-inline-assembly + assembly ("memory-safe") { + let signaturePos := mul(0x41, pos) + r := mload(add(signatures, add(signaturePos, 0x20))) + s := mload(add(signatures, add(signaturePos, 0x40))) + v := byte(0, mload(add(signatures, add(signaturePos, 0x60)))) + } + } + + function _getDigestAndNormalizeSigType(uint8 sigType, CheckNSignaturesRequest memory input) + internal + pure + returns (bytes32, uint8) + { + // sigType >= 32 implies it's signed over the actual digest, so we deduct it according to encoding rule + // if sigType > 60, it will eventually fail the ecdsa recover check below + bytes32 digest; + if (sigType >= 32) { + digest = input.actualDigest; + sigType -= 32; + // can have unchecked since we check against zero at the end + // underflow would wrap the value to 2 ^ 256 - 1 + unchecked { + // we now have one sig on actual digest + input.requiredNumSigsOnActualDigest -= 1; + } + } else { + digest = input.minimalDigest; + } + return (digest, sigType); + } + + /// @dev Helper function to get the dynamic part of a signature. This function works for sigType == 0 and sigType == + /// 2. + /// Please check the signature encoding scheme before using this function. + function _getSigDynamicPart(CheckNSignaturesContext memory context, CheckNSignaturesRequest memory request) + internal + pure + returns (bytes memory sigDynamicPartBytes) + { + // offset of current signature dynamic part + // second32Bytes is the memory offset containing the signature + uint256 sigDynamicPartOffset = uint256(context.second32Bytes); + if (sigDynamicPartOffset > request.signatures.length || sigDynamicPartOffset < _INDIVIDUAL_SIGNATURE_BYTES_LEN) + { + revert InvalidSigOffset(request.entityId, request.account, sigDynamicPartOffset); + } + // total length of current signature dynamic part + uint256 sigDynamicPartTotalLen; + // 0. load the signatures from CheckNSignaturesRequest struct + // 1. load contractSignature content starting from the correct memory offset + // 2. calculate total length including the content and the prefix storing the length + // solhint-disable-next-line no-inline-assembly + assembly ("memory-safe") { + let signatures := mload(add(request, 0xa0)) + sigDynamicPartBytes := add(add(signatures, sigDynamicPartOffset), 0x20) + sigDynamicPartTotalLen := add(mload(sigDynamicPartBytes), 0x20) + } + // signature dynamic part should not exceed the total signature length + if (sigDynamicPartOffset + sigDynamicPartTotalLen > request.signatures.length) { + revert InvalidSigLength(request.entityId, request.account, (sigDynamicPartOffset + sigDynamicPartTotalLen)); + } + // Signer 1 appends its signature's dynamic part after signer 2's. + // The recommended encoding format is: constant part 1, constant part 2, dynamic part 1, dynamic part 2. + // However, encoding as: constant part 1, constant part 2, dynamic part 2, dynamic part 1 is also valid, + // since the dynamic part of the signature is indexed by its offset. + if (sigDynamicPartOffset < context.lowestSigDynamicPartOffset || context.lowestSigDynamicPartOffset == 0) { + context.lowestSigDynamicPartOffset = sigDynamicPartOffset; + } + return sigDynamicPartBytes; + } + + function _validateContractSignature( + CheckNSignaturesContext memory context, + CheckNSignaturesRequest memory request, + bytes32 digest, + uint256 signatureCount + ) internal view returns (uint256) { + // first32Bytes contains the address to perform 1271 validation on + address contractAddress = address(uint160(uint256(context.first32Bytes))); + // make sure upper bits are clean + if (uint256(context.first32Bytes) > uint256(uint160(contractAddress))) { + revert InvalidAddress(request.entityId, request.account, contractAddress); + } + context.currentSigner = _getSignerId(contractAddress); + SignerMetadata memory currentSignerMetadata = + signersMetadataPerEntity[request.entityId][context.currentSigner][request.account]; + if (currentSignerMetadata.addr != contractAddress) { + if (context.success) { + context.firstFailure = signatureCount; + context.success = false; + } + } + // retrieve contract signature + bytes memory sigDynamicPartBytes = _getSigDynamicPart(context, request); + if (!SignatureChecker.isValidERC1271SignatureNow(contractAddress, digest, sigDynamicPartBytes)) { + if (context.success) { + context.firstFailure = signatureCount; + context.success = false; + } + } + return currentSignerMetadata.weight; + } + + function _validateWebauthnSignature( + CheckNSignaturesContext memory context, + CheckNSignaturesRequest memory request, + bytes32 digest, + uint256 signatureCount + ) internal view returns (uint256) { + // first32Bytes stores public key on-chain identifier + context.currentSigner = bytes30(uint240(uint256(context.first32Bytes))); + SignerMetadata memory currentSignerMetadata = + signersMetadataPerEntity[request.entityId][context.currentSigner][request.account]; + // retrieve sig dynamic part bytes + bytes memory sigDynamicPartBytes = _getSigDynamicPart(context, request); + WebAuthnSigDynamicPart memory sigDynamicPart = abi.decode(sigDynamicPartBytes, (WebAuthnSigDynamicPart)); + if ( + !WebAuthnLib.verify({ + challenge: abi.encode(digest), + webAuthnData: sigDynamicPart.webAuthnData, + r: sigDynamicPart.r, + s: sigDynamicPart.s, + x: currentSignerMetadata.publicKey.x, + y: currentSignerMetadata.publicKey.y + }) + ) { + if (context.success) { + context.firstFailure = signatureCount; + context.success = false; + } + } + return currentSignerMetadata.weight; + } + + function _validateEOASignature( + CheckNSignaturesContext memory context, + CheckNSignaturesRequest memory request, + bytes32 digest, + uint256 signatureCount, + uint8 sigType + ) internal view returns (uint256) { + // reverts if signature has the wrong s value, wrong v value, or if it's a bad point on the k1 curve + address signer = digest.recover(sigType, context.first32Bytes, context.second32Bytes); + context.currentSigner = _getSignerId(signer); + SignerMetadata memory currentSignerMetadata = + signersMetadataPerEntity[request.entityId][context.currentSigner][request.account]; + if (currentSignerMetadata.addr != signer) { + if (context.success) { + context.firstFailure = signatureCount; + context.success = false; + } + } + return currentSignerMetadata.weight; + } + + /// @dev Get the minimal user op digest with user op hash with gas fields or paymasterAndData set to default values. + /// @param userOp the user operation + /// @return minimal user op hash and sender + function _getMinimalUserOpDigest(PackedUserOperation calldata userOp) internal view returns (bytes32, address) { + address sender = userOp.getSender(); + uint256 nonce = userOp.nonce; + bytes32 hashInitCode = userOp.initCode.calldataKeccak(); + bytes32 hashCallData = userOp.callData.calldataKeccak(); + bytes32 userOpHash = keccak256( + abi.encode( + sender, + nonce, + hashInitCode, + hashCallData, + ZERO_BYTES32, // accountGasLimits + ZERO, // preVerificationGas = 0 + ZERO_BYTES32, // gasFees + EMPTY_HASH // paymasterAndData = keccak256('') + ) + ); + // include chainid to prevent replay across chains + return (keccak256(abi.encode(userOpHash, ENTRYPOINT, block.chainid)).toEthSignedMessageHash(), sender); + } +} diff --git a/src/msca/6900/v0.8/modules/validation/SingleSignerValidationModule.sol b/src/msca/6900/v0.8/modules/validation/SingleSignerValidationModule.sol index 0f90f5d..99fc8c5 100644 --- a/src/msca/6900/v0.8/modules/validation/SingleSignerValidationModule.sol +++ b/src/msca/6900/v0.8/modules/validation/SingleSignerValidationModule.sol @@ -47,9 +47,13 @@ contract SingleSignerValidationModule is IValidationModule, BaseModule, BaseERC7 using MessageHashUtils for bytes32; // A string in the format "vendor.module.semver". The vendor and module names MUST NOT contain a period character. - string public constant MODULE_ID = "circle.single-signer-validation-module.2.0.0"; - bytes32 private constant _HASHED_MODULE_ID = keccak256(bytes(MODULE_ID)); - bytes32 private constant _MODULE_TYPEHASH = keccak256("SingleSignerValidationMessage(bytes message)"); + string public constant MODULE_ID = "circle.single-signer-validation-module.1.0.0"; + // keccak256("circle.single-signer-validation-module.1.0.0") + bytes32 private constant _HASHED_MODULE_ID = 0xb7a07c87bdb512080dfe47da0b1e70c517ba5218ba354e91def18b15077c58a4; + // keccak256("1.0.0") + bytes32 private constant _HASHED_MODULE_VERSION = 0x06c015bd22b4c69690933c1058878ebdfef31f9aaae40bbe86d8a09fe1b2972c; + // keccak256("SingleSignerValidationMessage(bytes message)") + bytes32 private constant _MODULE_TYPEHASH = 0x98b95f3602725ed42dccfe2fc6c94df5b37193d13336bd177cd68345785e4f47; // entityId => account => signer // this module supports composition that other validation can rely on entity id in this validation to validate // the signature @@ -69,12 +73,20 @@ contract SingleSignerValidationModule is IValidationModule, BaseModule, BaseERC7 /// @inheritdoc IModule function onInstall(bytes calldata data) external override { + if (data.length == 0) { + // the caller already checks before calling into it, this is just a safety check + return; + } (uint32 entityId, address owner) = abi.decode(data, (uint32, address)); _transferSigner(entityId, owner); } /// @inheritdoc IModule function onUninstall(bytes calldata data) external override { + if (data.length == 0) { + // the caller already checks before calling into it, this is just a safety check + return; + } uint32 entityId = abi.decode(data, (uint32)); _transferSigner(entityId, address(0)); } @@ -156,7 +168,12 @@ contract SingleSignerValidationModule is IValidationModule, BaseModule, BaseERC7 } /// @inheritdoc BaseERC712CompliantModule - function _getModuleIdHash() internal pure override returns (bytes32) { + function _getModuleNameHash() internal pure override returns (bytes32) { return _HASHED_MODULE_ID; } + + /// @inheritdoc BaseERC712CompliantModule + function _getModuleVersionHash() internal pure override returns (bytes32) { + return _HASHED_MODULE_VERSION; + } } diff --git a/src/utils/CalldataUtils.sol b/src/utils/CalldataUtils.sol new file mode 100644 index 0000000..e674845 --- /dev/null +++ b/src/utils/CalldataUtils.sol @@ -0,0 +1,36 @@ +/* + * Copyright 2025 Circle Internet Group, Inc. All rights reserved. + + * SPDX-License-Identifier: GPL-3.0-or-later + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +pragma solidity 0.8.24; + +library CalldataUtils { + /** + * Keccak function over calldata. + * @dev copy calldata into memory, do keccak and drop allocated memory. This is more efficient than letting solidity + * do it. + */ + function calldataKeccak(bytes calldata data) internal pure returns (bytes32 ret) { + // solhint-disable-next-line no-inline-assembly + assembly { + let mem := mload(0x40) + let len := data.length + calldatacopy(mem, data.offset, len) + ret := keccak256(mem, len) + } + } +} diff --git a/src/utils/PaymasterUtils.sol b/src/utils/PaymasterUtils.sol index 5faf851..893fa1b 100644 --- a/src/utils/PaymasterUtils.sol +++ b/src/utils/PaymasterUtils.sol @@ -18,6 +18,7 @@ */ pragma solidity 0.8.24; +import {CalldataUtils} from "./CalldataUtils.sol"; import {UserOperationLib} from "@account-abstraction/contracts/core/UserOperationLib.sol"; import {PackedUserOperation} from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; @@ -52,6 +53,7 @@ enum ChargeMode { */ library PaymasterUtils { using UserOperationLib for PackedUserOperation; + using CalldataUtils for bytes; /** * struct PackedUserOperation { @@ -69,26 +71,11 @@ library PaymasterUtils { function packUpToPaymasterAndData(PackedUserOperation calldata userOp) internal pure returns (bytes memory ret) { address sender = userOp.getSender(); uint256 nonce = userOp.nonce; - bytes32 hashInitCode = calldataKeccak(userOp.initCode); - bytes32 hashCallData = calldataKeccak(userOp.callData); + bytes32 hashInitCode = userOp.initCode.calldataKeccak(); + bytes32 hashCallData = userOp.callData.calldataKeccak(); bytes32 accountGasLimits = userOp.accountGasLimits; uint256 preVerificationGas = userOp.preVerificationGas; bytes32 gasFees = userOp.gasFees; return abi.encode(sender, nonce, hashInitCode, hashCallData, accountGasLimits, preVerificationGas, gasFees); } - - /** - * Keccak function over calldata. - * @dev copy calldata into memory, do keccak and drop allocated memory. This is more efficient than letting solidity - * do it. - */ - function calldataKeccak(bytes calldata data) internal pure returns (bytes32 ret) { - // solhint-disable-next-line no-inline-assembly - assembly { - let mem := mload(0x40) - let len := data.length - calldatacopy(mem, data.offset, len) - ret := keccak256(mem, len) - } - } } diff --git a/test/fixtures/p256key_11_fixture.json b/test/fixtures/p256key_11_fixture.json new file mode 100644 index 0000000..0043161 --- /dev/null +++ b/test/fixtures/p256key_11_fixture.json @@ -0,0 +1,60 @@ +{ + "numOfKeys": 11, + "results": [ + { + "private_key": "0x03d99692017473e2d631945a812607b23269d85721e0f370b8d3e7d29a874fd2", + "x": "0x1c05286fe694493eae33312f2d2e0d0abeda8db76238b7a204be1fb87f54ce42", + "y": "0x28fef61ef4ac300f631657635c28e59bfb2fe71bce1634c81c65642042f6dc4d" + }, + { + "private_key": "0xf393912cc9bd60bb78d38f399b37c64281bb5d80cedca233b2a1af24b481b348", + "x": "0xaf6fc3e6244e417241967158b5b7b85213031e75bf9fda428f52c2876dadb667", + "y": "0xf8bc5f3c31aad801dd5a289d0d37241dacde741acecf829a3afb21b6100175c5" + }, + { + "private_key": "0x7289e570dfa7941bda802c2ee69658069ba75993fde9db70dc890fa44b0bb95d", + "x": "0xe1f5f0092a062194b3457355430ba1a346398250c9c18f31ea5efcd42f34dec1", + "y": "0xeb56e12f12db9cf48a45f26853d17d9151a4c2704c026622d472599adbc99107" + }, + { + "private_key": "0x8868d07eac59b1d8be6abc35a08ca57f7feddd6e19985e98604b058fb427c6ea", + "x": "0x6cef827e77f42901b71cdd8c96ebd5ca77382207dd58b892e333d66af4714beb", + "y": "0x7250ab54b98fd38ef09c470939749d730e025970ecd01a6a8442ad0527cd5032" + }, + { + "private_key": "0xa54a073af49fee9dc7a7066cacb0d71a35ee4f77f2c452be1b011cfeaf138279", + "x": "0x39de965959ef361cfa54ba7a4c4fd36cf1e71f595b0b5c6874a19f49ce5679da", + "y": "0xe02a5985554fad99574a1f2e2e87c3c56e1e84bef9ef1c20b331ca7c0a5ef3bd" + }, + { + "private_key": "0xb48d31ed8ab058171b7050dc3678a2a2233afb97e7923b7af90959d0250a28bd", + "x": "0x49a1760b2afe26d769e6d60e867d8350fb9fba03cd674b08a3bc2d5e547f8974", + "y": "0x3e936e75fae0160b2f7fa6245cf3a13b8691c954a5a94189337cd3e486d7da6e" + }, + { + "private_key": "0xb2ce97c0d664dbb4dc9f9cd475bfc1e490d693e112f297484174d7e2843e9c1c", + "x": "0x4321c2e3c26454253aac751e0a7a5c6a5fce6a17ab0f180e4c67ea4221cd426e", + "y": "0x1230defe5825ef75c96d1d681d136c5ecf47bf7064ad6b98e52b6a49f03b8593" + }, + { + "private_key": "0x48bb205319d8e3676d8367277f9990edc43ec5e0679c3250c3d24c70946372eb", + "x": "0x9426351f5b7f619432ee76de0f01b85a7ea8b24f900657f839407da018b33a14", + "y": "0xf38b6dec893d1216c50ac0c8be769910e779abe1ee953797d3e0acf11817fbe4" + }, + { + "private_key": "0x12d9c6ad5f84d8d44485841f45300bb4c43a59fe14aa65aba3afd7ee5667095e", + "x": "0x7730e3c654670fd1f3ece4caaf1f251311b3e6af18651726e2a88e5681d06663", + "y": "0x6a0eab7af33fd44f9c5cec1a3b2f9a3c966952ff5a3b4e2083734fcfef1a50d5" + }, + { + "private_key": "0x13835dea8270add877a0b58d7eac9032e1c2d090bc95272fefd7f496fb105f6a", + "x": "0x9a0b833282f1d751c145d40d03ef1d834ca1a51f0f0772f08bc3eaa012f0ca6", + "y": "0xf9ca54b248b5dcd39cab32bbbd8eb0c417bb09694df3f365aed41e49a86086de" + }, + { + "private_key": "0x1d96163a28245f840aad145740b82d05c495544faedb6b42c6ffa6be55471b22", + "x": "0xb7c64e3a25581e2a7b2bf7034b345787193d6734a839b70c0b03f35c885020ec", + "y": "0x41d71a6b0a2c416e1229726413f038cdea3ef7086071e8a5fe04843c2ce3e1c2" + } + ] +} diff --git a/test/gas/6900/v0.7/plugins/multisig/UpgradableMSCAWithMultisigPlugin.t.sol b/test/gas/6900/v0.7/plugins/multisig/UpgradableMSCAWithMultisigPlugin.t.sol new file mode 100644 index 0000000..9973950 --- /dev/null +++ b/test/gas/6900/v0.7/plugins/multisig/UpgradableMSCAWithMultisigPlugin.t.sol @@ -0,0 +1,347 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + + * SPDX-License-Identifier: GPL-3.0-or-later + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +pragma solidity 0.8.24; + +import {OwnerData, OwnershipMetadata, PublicKey} from "../../../../../../src/common/CommonStructs.sol"; +import {FunctionReference} from "../../../../../../src/msca/6900/v0.7/common/Structs.sol"; + +import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import {PackedUserOperation} from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; + +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {Vm} from "forge-std/src/Vm.sol"; +import {console} from "forge-std/src/console.sol"; + +import { + PluginManager, + UpgradableMSCA, + UpgradableMSCAFactory +} from "../../../../../../src/msca/6900/v0.7/factories/UpgradableMSCAFactory.sol"; +import {WeightedWebauthnMultisigPlugin} from + "../../../../../../src/msca/6900/v0.7/plugins/v1_0_0/multisig/WeightedWebauthnMultisigPlugin.sol"; +import {TestLiquidityPool} from "../../../../../util/TestLiquidityPool.sol"; +import {PluginGasProfileBaseTest} from "../../../../PluginGasProfileBase.t.sol"; + +contract UpgradableMSCAWithMultisigPluginTest is PluginGasProfileBaseTest { + using Strings for uint256; + using MessageHashUtils for bytes32; + + event PluginInstalled(address indexed plugin, bytes32 manifestHash, FunctionReference[] dependencies); + // upgrade + event Upgraded(address indexed newImplementation); + // 4337 + event UserOperationEvent( + bytes32 indexed userOpHash, + address indexed sender, + address indexed paymaster, + uint256 nonce, + bool success, + uint256 actualGasCost, + uint256 actualGasUsed + ); + + PluginManager private pluginManager = new PluginManager(); + uint256 internal ownerPrivateKey; + address private ownerAddr; + + uint256 internal ownerPrivateKey1; + uint256 internal ownerPrivateKey2; + address private ownerAddr1; + address private ownerAddr2; + UpgradableMSCAFactory private factory; + WeightedWebauthnMultisigPlugin private multisigPlugin; + UpgradableMSCA private msca; + address private multisigPluginAddr; + address private mscaAddr; + string public accountAndPluginType; + TestLiquidityPool private testLiquidityPool; + + function setUp() public override { + super.setUp(); + testLiquidityPool = new TestLiquidityPool("TestERC20", "$$$"); + + accountAndPluginType = "UpgradableMSCAWithMultisigPlugin"; + address factoryOwner = makeAddr("factoryOwner"); + factory = new UpgradableMSCAFactory(factoryOwner, address(entryPoint), address(pluginManager)); + multisigPlugin = new WeightedWebauthnMultisigPlugin(address(entryPoint)); + multisigPluginAddr = address(multisigPlugin); + + address[] memory _plugins = new address[](1); + _plugins[0] = multisigPluginAddr; + bool[] memory _permissions = new bool[](1); + _permissions[0] = true; + vm.startPrank(factoryOwner); + factory.setPlugins(_plugins, _permissions); + vm.stopPrank(); + } + + function testBenchmarkAll() external override { + testBenchmarkAccountCreation(2); + testBenchmarkAccountCreation(3); + testBenchmarkAccountCreation(5); + testBenchmarkAccountCreation(10); + testBenchmarkPluginAddOwners(1); + testBenchmarkPluginAddOwners(2); + testBenchmarkPluginAddOwners(3); + testBenchmarkPluginAddOwners(5); + testBenchmarkPluginAddOwners(10); + testBenchmarkPluginUpdateMultisigWeights(); + testBenchmarkPluginRemoveOwners(); + testBenchmarkTokenTransfer(); + writeTestResult(accountAndPluginType); + } + + function testBenchmarkPluginInstall() internal pure override { + console.log("not implemented"); + } + + function testBenchmarkPluginUninstall() internal pure override { + console.log("not implemented"); + } + + function testBenchmarkAccountCreation(uint256 ownerCount) internal { + (ownerAddr, ownerPrivateKey) = + makeAddrAndKey(string(abi.encodePacked("testBenchmarkAccountCreation_", ownerCount.toString()))); + address[] memory initialOwners = new address[](ownerCount); + for (uint256 i = 0; i < ownerCount; i++) { + initialOwners[i] = makeAddr(string(abi.encodePacked("owner", i))); + } + uint256[] memory ownerWeights = new uint256[](ownerCount); + for (uint256 i = 0; i < ownerCount; i++) { + ownerWeights[i] = 1; + } + PublicKey[] memory initialPublicKeyOwners = new PublicKey[](0); + uint256[] memory initialPublicKeyWeights = new uint256[](0); + uint256 thresholdWeight = 1; + address[] memory plugins = new address[](1); + bytes32[] memory manifestHashes = new bytes32[](1); + bytes[] memory pluginInstallData = new bytes[](1); + plugins[0] = address(multisigPluginAddr); + manifestHashes[0] = keccak256(abi.encode(multisigPlugin.pluginManifest())); + pluginInstallData[0] = + abi.encode(initialOwners, ownerWeights, initialPublicKeyOwners, initialPublicKeyWeights, thresholdWeight); + vm.startPrank(ownerAddr); + uint256 gasBefore = gasleft(); + msca = factory.createAccount( + addressToBytes32(ownerAddr), + 0x0000000000000000000000000000000000000000000000000000000000000000, + abi.encode(plugins, manifestHashes, pluginInstallData) + ); + uint256 gasUsed = gasBefore - gasleft(); + vm.stopPrank(); + mscaAddr = address(msca); + vm.deal(mscaAddr, 1 ether); + + string memory testName = string(abi.encodePacked("0001_account_creation_runtime_", ownerCount.toString())); + console.log("case - %s", testName); + console.log(" gasUsed : ", gasUsed); + vm.serializeUint(jsonObj, testName, gasUsed); + sum += gasUsed; + + (,, OwnershipMetadata memory ownershipMetadata) = multisigPlugin.ownershipInfoOf(mscaAddr); + assertEq(ownershipMetadata.numOwners, ownerCount); + } + + function testBenchmarkPluginAddOwners(uint256 ownerCount) internal { + // create account first + createMultisigAccount(string(abi.encodePacked("testBenchmarkPluginAddOwners_", ownerCount.toString()))); + + (,, OwnershipMetadata memory ownershipMetadata) = multisigPlugin.ownershipInfoOf(mscaAddr); + assertEq(ownershipMetadata.numOwners, 2); + + // now add owners + uint256 acctNonce = entryPoint.getNonce(mscaAddr, 0); + address[] memory ownersToAdd = new address[](ownerCount); + for (uint256 i = 0; i < ownerCount; i++) { + ownersToAdd[i] = makeAddr(string(abi.encodePacked("owner", i))); + } + uint256[] memory weightsToAdd = new uint256[](ownerCount); + for (uint256 i = 0; i < ownerCount; i++) { + weightsToAdd[i] = 1; + } + PublicKey[] memory publicKeyOwnersToAdd = new PublicKey[](0); + uint256[] memory pubicKeyWeightsToAdd = new uint256[](0); + uint256 newThresholdWeight = 1; + bytes memory callData = abi.encodeWithSelector( + bytes4(keccak256("addOwners(address[],uint256[],(uint256,uint256)[],uint256[],uint256)")), + ownersToAdd, + weightsToAdd, + publicKeyOwnersToAdd, + pubicKeyWeightsToAdd, + newThresholdWeight + ); + + PackedUserOperation memory userOp = buildPartialUserOp(mscaAddr, acctNonce, vm.toString(callData)); + + bytes memory signatureActualDigest = signUserOpHashActualDigest(entryPoint, vm, ownerPrivateKey1, userOp); + userOp.signature = signatureActualDigest; + + string memory testName = string(abi.encodePacked("0002_addOwners_", ownerCount.toString())); + executeUserOp(mscaAddr, userOp, testName, 0); + (,, ownershipMetadata) = multisigPlugin.ownershipInfoOf(mscaAddr); + uint256 originalOwners = 2; + uint256 updatedOwners = originalOwners + ownerCount; + assertEq(ownershipMetadata.numOwners, updatedOwners); + } + + function testBenchmarkPluginUpdateMultisigWeights() internal { + // create account first + createMultisigAccount("testBenchmarkPluginUpdateMultisigWeights"); + (,, OwnershipMetadata memory ownershipMetadata) = multisigPlugin.ownershipInfoOf(mscaAddr); + assertEq(ownershipMetadata.thresholdWeight, 1); + + // now update owner weights + uint256 acctNonce = entryPoint.getNonce(mscaAddr, 0); + address[] memory ownersToUpdate = new address[](2); + ownersToUpdate[0] = ownerAddr1; + ownersToUpdate[1] = ownerAddr2; + uint256[] memory newWeightsToUpdate = new uint256[](2); + newWeightsToUpdate[0] = 2; + newWeightsToUpdate[1] = 2; + PublicKey[] memory publicKeyOwnersToUpdate = new PublicKey[](0); + uint256[] memory pubicKeyNewWeightsToUpdate = new uint256[](0); + uint256 newThresholdWeight = 2; + bytes memory callData = abi.encodeWithSelector( + bytes4(keccak256("updateMultisigWeights(address[],uint256[],(uint256,uint256)[],uint256[],uint256)")), + ownersToUpdate, + newWeightsToUpdate, + publicKeyOwnersToUpdate, + pubicKeyNewWeightsToUpdate, + newThresholdWeight + ); + + PackedUserOperation memory userOp = buildPartialUserOp(mscaAddr, acctNonce, vm.toString(callData)); + + bytes memory signatureActualDigest = signUserOpHashActualDigest(entryPoint, vm, ownerPrivateKey1, userOp); + userOp.signature = signatureActualDigest; + + string memory testName = "0003_updateMultisigWeights_updateTwo"; + executeUserOp(mscaAddr, userOp, testName, 0); + (,, ownershipMetadata) = multisigPlugin.ownershipInfoOf(mscaAddr); + assertEq(ownershipMetadata.thresholdWeight, 2); + } + + function testBenchmarkPluginRemoveOwners() internal { + // create account first + createMultisigAccount("testBenchmarkPluginRemoveOwners"); + bytes30[] memory ownerAddresses; + OwnerData[] memory ownersData; + OwnershipMetadata memory ownershipMetadata; + (ownerAddresses, ownersData, ownershipMetadata) = multisigPlugin.ownershipInfoOf(mscaAddr); + assertEq(ownershipMetadata.numOwners, 2); + + // now remove one owner + uint256 acctNonce = entryPoint.getNonce(mscaAddr, 0); + address[] memory ownersToRemove = new address[](1); + ownersToRemove[0] = ownerAddr2; + PublicKey[] memory publicKeyOwnersToRemove = new PublicKey[](0); + uint256 newThresholdWeight = 1; + bytes memory callData = abi.encodeWithSelector( + bytes4(keccak256("removeOwners(address[],(uint256,uint256)[],uint256)")), + ownersToRemove, + publicKeyOwnersToRemove, + newThresholdWeight + ); + + PackedUserOperation memory userOp = buildPartialUserOp(mscaAddr, acctNonce, vm.toString(callData)); + + bytes memory signatureActualDigest = signUserOpHashActualDigest(entryPoint, vm, ownerPrivateKey1, userOp); + userOp.signature = signatureActualDigest; + + string memory testName = "0004_removeOwners_1"; + executeUserOp(mscaAddr, userOp, testName, 0); + (ownerAddresses, ownersData, ownershipMetadata) = multisigPlugin.ownershipInfoOf(mscaAddr); + assertEq(ownershipMetadata.numOwners, 1); + } + + function testBenchmarkTokenTransfer() internal { + // create account first + createMultisigAccount("testBenchmarkTokenTransfer"); + testLiquidityPool.mint(mscaAddr, 2000000); + assertEq(testLiquidityPool.balanceOf(mscaAddr), 2000000); + + // now transfer + uint256 acctNonce = entryPoint.getNonce(mscaAddr, 0); + address recipientAddr = makeAddr("recipient"); + address liquidityPoolSpenderAddr = address(testLiquidityPool); + bytes memory transferCallData = + abi.encodeWithSelector(bytes4(keccak256("transfer(address,uint256)")), recipientAddr, 1000000); + bytes memory callData = abi.encodeWithSelector( + bytes4(keccak256("execute(address,uint256,bytes)")), liquidityPoolSpenderAddr, 0, transferCallData + ); + + PackedUserOperation memory userOp = buildPartialUserOp(mscaAddr, acctNonce, vm.toString(callData)); + + bytes memory signatureActualDigest = signUserOpHashActualDigest(entryPoint, vm, ownerPrivateKey1, userOp); + userOp.signature = signatureActualDigest; + + string memory testName = "0005_erc20_transfer"; + executeUserOp(mscaAddr, userOp, testName, 0); + assertEq(testLiquidityPool.balanceOf(recipientAddr), 1000000); + assertEq(testLiquidityPool.balanceOf(mscaAddr), 1000000); + } + + function createMultisigAccount(string memory testName) internal returns (address) { + (ownerAddr, ownerPrivateKey) = makeAddrAndKey(testName); + address[] memory initialOwners = new address[](2); + uint256[] memory ownerWeights = new uint256[](2); + PublicKey[] memory initialPublicKeyOwners = new PublicKey[](0); + uint256[] memory initialPublicKeyWeights = new uint256[](0); + uint256 thresholdWeight = 1; + (ownerAddr1, ownerPrivateKey1) = makeAddrAndKey("owner1"); + (ownerAddr2, ownerPrivateKey2) = makeAddrAndKey("owner2"); + initialOwners[0] = ownerAddr1; + initialOwners[1] = ownerAddr2; + ownerWeights[0] = 1; + ownerWeights[1] = 1; + address[] memory plugins = new address[](1); + bytes32[] memory manifestHashes = new bytes32[](1); + bytes[] memory pluginInstallData = new bytes[](1); + plugins[0] = address(multisigPluginAddr); + manifestHashes[0] = keccak256(abi.encode(multisigPlugin.pluginManifest())); + pluginInstallData[0] = + abi.encode(initialOwners, ownerWeights, initialPublicKeyOwners, initialPublicKeyWeights, thresholdWeight); + return createAccount(plugins, manifestHashes, pluginInstallData); + } + + function createAccount(address[] memory plugins, bytes32[] memory manifestHashes, bytes[] memory pluginInstallData) + internal + returns (address) + { + bytes32 salt = 0x0000000000000000000000000000000000000000000000000000000000000000; + bytes memory initializingData = abi.encode(plugins, manifestHashes, pluginInstallData); + vm.startPrank(ownerAddr); + msca = factory.createAccount(addressToBytes32(ownerAddr), salt, initializingData); + vm.stopPrank(); + mscaAddr = address(msca); + vm.deal(mscaAddr, 1 ether); + return mscaAddr; + } + + function signUserOpHashActualDigest(IEntryPoint entryPoint, Vm vm, uint256 key, PackedUserOperation memory userOp) + public + view + returns (bytes memory signature) + { + bytes32 hash = entryPoint.getUserOpHash(userOp); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, hash.toEthSignedMessageHash()); + signature = abi.encodePacked(r, s, v + 32); + } +} diff --git a/test/msca/6900/shared/libs/AddressDLLLib.t.sol b/test/msca/6900/shared/libs/AddressDLLLib.t.sol new file mode 100644 index 0000000..0c862e0 --- /dev/null +++ b/test/msca/6900/shared/libs/AddressDLLLib.t.sol @@ -0,0 +1,155 @@ +/* + * Copyright 2025 Circle Internet Group, Inc. All rights reserved. + + * SPDX-License-Identifier: GPL-3.0-or-later + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +pragma solidity 0.8.24; + +import {AddressDLL} from "../../../../../src/msca/6900/shared/common/Structs.sol"; +import {AddressDLLLib} from "../../../../../src/msca/6900/shared/libs/AddressDLLLib.sol"; +import {TestUtils} from "../../../../util/TestUtils.sol"; +import {TestAddressDLL} from "./TestAddressDLL.sol"; + +contract AddressDLLLibTest is TestUtils { + address internal constant SENTINEL_ADDRESS = address(0x0); + + using AddressDLLLib for AddressDLL; + + function testSentinelAddress() public { + TestAddressDLL dll = new TestAddressDLL(); + assertEq(dll.getHead(), SENTINEL_ADDRESS); + assertEq(dll.getTail(), SENTINEL_ADDRESS); + // sentinel should not considered as the value of the list + assertFalse(dll.contains(SENTINEL_ADDRESS)); + // add one item + assertTrue(dll.append(address(1))); + // sentinel should not considered as the value of the list + assertFalse(dll.contains(SENTINEL_ADDRESS)); + } + + function testAddRemoveGetAddressValues() public { + TestAddressDLL values = new TestAddressDLL(); + assertEq(values.size(), 0); + assertEq(values.getAll().length, 0); + // try to remove sentinel stupidly + bytes4 errorSelector = bytes4(keccak256("InvalidAddress()")); + vm.expectRevert(abi.encodeWithSelector(errorSelector)); + values.remove(SENTINEL_ADDRESS); + assertEq(values.getHead(), SENTINEL_ADDRESS); + assertEq(values.getTail(), SENTINEL_ADDRESS); + // sentinel doesn't count + assertEq(values.size(), 0); + address value1 = address(1); + address value2 = address(2); + address value3 = address(3); + address value4 = address(4); + assertTrue(values.append(value1)); + assertTrue(values.contains(value1)); + assertEq(values.getHead(), value1); + assertEq(values.getTail(), value1); + // remove it + assertTrue(values.remove(value1)); + assertEq(values.size(), 0); + assertEq(values.getHead(), SENTINEL_ADDRESS); + assertEq(values.getTail(), SENTINEL_ADDRESS); + assertFalse(values.contains(value1)); + // add value1 and value2 + assertTrue(values.append(value1)); + assertTrue(values.append(value2)); + assertEq(values.size(), 2); + assertEq(values.getHead(), value1); + assertEq(values.getTail(), value2); + // now remove value2 + assertTrue(values.remove(value2)); + assertEq(values.getHead(), value1); + assertEq(values.getTail(), value1); + // now add back value2 with three more values + assertTrue(values.append(value2)); + assertTrue(values.append(value3)); + assertTrue(values.append(value4)); + assertEq(values.size(), 4); + assertEq(values.getHead(), value1); + assertEq(values.getTail(), value4); + address[] memory results = values.getAll(); + assertEq(results.length, 4); + assertEq(results[0], value1); + assertEq(results[1], value2); + assertEq(results[2], value3); + assertEq(results[3], value4); + // now remove value1 + assertTrue(values.remove(value1)); + assertEq(values.size(), 3); + assertEq(values.getHead(), value2); + assertEq(values.getTail(), value4); + // now remove value4 + assertTrue(values.remove(value4)); + assertEq(values.size(), 2); + assertEq(values.getHead(), value2); + assertEq(values.getTail(), value3); + // now remove value3 + assertTrue(values.remove(value3)); + assertEq(values.size(), 1); + assertEq(values.getHead(), value2); + assertEq(values.getTail(), value2); + // now remove value2 + assertTrue(values.remove(value2)); + assertEq(values.size(), 0); + assertEq(values.getHead(), SENTINEL_ADDRESS); + assertEq(values.getTail(), SENTINEL_ADDRESS); + // now remove value2 again, should revert + errorSelector = bytes4(keccak256("ItemDoesNotExist()")); + vm.expectRevert(abi.encodeWithSelector(errorSelector)); + values.remove(value2); + // get zero value every time + errorSelector = bytes4(keccak256("InvalidLimit()")); + vm.expectRevert(abi.encodeWithSelector(errorSelector)); + values.getPaginated(SENTINEL_ADDRESS, 0); + } + + function testFuzz_bulkGetAddresses(uint8 limit, uint8 totalValues) public { + // try out different limits, even bigger than totalValues + bound(limit, 1, 30); + bound(totalValues, 3, 30); + TestAddressDLL dll = new TestAddressDLL(); + for (uint32 i = 1; i <= totalValues; i++) { + dll.append(address(uint160(i))); + } + bulkGetAndVerifyAddresses(dll, totalValues, limit); + } + + function bulkGetAndVerifyAddresses(TestAddressDLL dll, uint256 totalValues, uint256 limit) private view { + address[] memory results = new address[](totalValues); + address start = SENTINEL_ADDRESS; + uint32 count = 0; + uint256 j = 0; + address[] memory values; + address next; + while (count < totalValues && limit != 0) { + (values, next) = dll.getPaginated(start, limit); + for (uint256 i = 0; i < values.length; ++i) { + results[count] = values[i]; + // starts from address(1) + assertEq(results[j], address(uint160(count + 1))); + count++; + j++; + } + if (next == SENTINEL_ADDRESS) { + break; + } + start = next; + } + } +} diff --git a/test/msca/6900/shared/libs/Bytes32DLLLib.t.sol b/test/msca/6900/shared/libs/Bytes32DLLLib.t.sol index f1d1781..5b00b03 100644 --- a/test/msca/6900/shared/libs/Bytes32DLLLib.t.sol +++ b/test/msca/6900/shared/libs/Bytes32DLLLib.t.sol @@ -27,10 +27,22 @@ import {TestBytes32DLL} from "./TestBytes32DLL.sol"; contract Bytes32DLLLibTest is TestUtils { using Bytes32DLLLib for Bytes32DLL; + function testSentinelBytes32() public { + TestBytes32DLL dll = new TestBytes32DLL(); + assertEq(dll.getHead(), SENTINEL_BYTES32); + assertEq(dll.getTail(), SENTINEL_BYTES32); + // sentinel should not considered as the value of the list + assertFalse(dll.contains(SENTINEL_BYTES32)); + // add one item + assertTrue(dll.append(bytes32(uint256(1)))); + // sentinel should not considered as the value of the list + assertFalse(dll.contains(SENTINEL_BYTES32)); + } + function testAddRemoveGetBytes32Values() public { TestBytes32DLL values = new TestBytes32DLL(); - // sentinel value is initialized assertEq(values.size(), 0); + assertEq(values.getAll().length, 0); // try to remove sentinel stupidly bytes4 errorSelector = bytes4(keccak256("InvalidItem()")); vm.expectRevert(abi.encodeWithSelector(errorSelector)); @@ -71,6 +83,7 @@ contract Bytes32DLLLibTest is TestUtils { assertEq(values.getHead(), value1); assertEq(values.getTail(), value4); bytes32[] memory results = values.getAll(); + assertEq(results.length, 4); assertEq(results[0], value1); assertEq(results[1], value2); assertEq(results[2], value3); diff --git a/test/msca/6900/shared/libs/Bytes4DLLLib.t.sol b/test/msca/6900/shared/libs/Bytes4DLLLib.t.sol index 2c087ac..7264016 100644 --- a/test/msca/6900/shared/libs/Bytes4DLLLib.t.sol +++ b/test/msca/6900/shared/libs/Bytes4DLLLib.t.sol @@ -29,10 +29,22 @@ import {TestBytes4DLL} from "./TestBytes4DLL.sol"; contract Bytes4DLLLibTest is TestUtils { using Bytes4DLLLib for Bytes4DLL; + function testSentinelBytes4() public { + TestBytes4DLL dll = new TestBytes4DLL(); + assertEq(dll.getHead(), SENTINEL_BYTES4); + assertEq(dll.getTail(), SENTINEL_BYTES4); + // sentinel should not considered as the value of the list + assertFalse(dll.contains(SENTINEL_BYTES4)); + // add one item + assertTrue(dll.append(bytes4(uint32(1)))); + // sentinel should not considered as the value of the list + assertFalse(dll.contains(SENTINEL_BYTES4)); + } + function testAddRemoveGetBytes4Values() public { TestBytes4DLL values = new TestBytes4DLL(); - // sentinel value is initialized assertEq(values.size(), 0); + assertEq(values.getAll().length, 0); // try to remove sentinel stupidly bytes4 errorSelector = bytes4(keccak256("InvalidBytes4()")); vm.expectRevert(abi.encodeWithSelector(errorSelector)); @@ -73,6 +85,7 @@ contract Bytes4DLLLibTest is TestUtils { assertEq(values.getHead(), value1); assertEq(values.getTail(), value4); bytes4[] memory results = values.getAll(); + assertEq(results.length, 4); assertEq(results[0], value1); assertEq(results[1], value2); assertEq(results[2], value3); diff --git a/test/msca/6900/shared/libs/TestAddressDLL.sol b/test/msca/6900/shared/libs/TestAddressDLL.sol new file mode 100644 index 0000000..5cd0c13 --- /dev/null +++ b/test/msca/6900/shared/libs/TestAddressDLL.sol @@ -0,0 +1,64 @@ +/* + * Copyright 2025 Circle Internet Group, Inc. All rights reserved. + + * SPDX-License-Identifier: GPL-3.0-or-later + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +pragma solidity 0.8.24; + +import {AddressDLL} from "../../../../../src/msca/6900/shared/common/Structs.sol"; +import {AddressDLLLib} from "../../../../../src/msca/6900/shared/libs/AddressDLLLib.sol"; + +contract TestAddressDLL { + using AddressDLLLib for AddressDLL; + + AddressDLL private addressDLL; + + function append(address valueToAdd) external returns (bool) { + return addressDLL.append(valueToAdd); + } + + function remove(address valueToRemove) external returns (bool) { + return addressDLL.remove(valueToRemove); + } + + function size() external view returns (uint256) { + return addressDLL.size(); + } + + function contains(address value) external view returns (bool) { + return addressDLL.contains(value); + } + + function getAll() external view returns (address[] memory results) { + return addressDLL.getAll(); + } + + function getPaginated(address start, uint256 limit) + external + view + returns (address[] memory results, address next) + { + return addressDLL.getPaginated(start, limit); + } + + function getHead() external view returns (address) { + return addressDLL.getHead(); + } + + function getTail() external view returns (address) { + return addressDLL.getTail(); + } +} diff --git a/test/msca/6900/shared/libs/ValidationDataLib.t.sol b/test/msca/6900/shared/libs/ValidationDataLib.t.sol index 5607dfe..9f56e9d 100644 --- a/test/msca/6900/shared/libs/ValidationDataLib.t.sol +++ b/test/msca/6900/shared/libs/ValidationDataLib.t.sol @@ -53,6 +53,14 @@ contract ValidationDataLibTest is TestUtils { assertEq(result.validAfter, 1); assertEq(result.validUntil, 2); assertEq(result.authorizer, address(1)); + + a = ValidationData({validAfter: 1, validUntil: 2, authorizer: address(1)}); + b = ValidationData({validAfter: 1, validUntil: 2, authorizer: address(2)}); + bUint = b._packValidationData(); + result = a._intersectValidationData(bUint); + assertEq(result.validAfter, 1); + assertEq(result.validUntil, 2); + assertEq(result.authorizer, address(2)); } function testIntersectBadAuthorizer_b() public pure { @@ -63,6 +71,14 @@ contract ValidationDataLibTest is TestUtils { assertEq(result.validAfter, 1); assertEq(result.validUntil, 2); assertEq(result.authorizer, address(1)); + + a = ValidationData({validAfter: 1, validUntil: 2, authorizer: address(2)}); + b = ValidationData({validAfter: 1, validUntil: 2, authorizer: address(1)}); + bUint = b._packValidationData(); + result = a._intersectValidationData(bUint); + assertEq(result.validAfter, 1); + assertEq(result.validUntil, 2); + assertEq(result.authorizer, address(2)); } function testIntersect_equal() public pure { @@ -73,6 +89,14 @@ contract ValidationDataLibTest is TestUtils { assertEq(result.validAfter, 3); assertEq(result.validUntil, 3); assertEq(result.authorizer, address(1)); + + a = ValidationData({validAfter: 1, validUntil: 3, authorizer: address(2)}); + b = ValidationData({validAfter: 3, validUntil: 5, authorizer: address(1)}); + bUint = b._packValidationData(); + result = a._intersectValidationData(bUint); + assertEq(result.validAfter, 3); + assertEq(result.validUntil, 3); + assertEq(result.authorizer, address(2)); } function testIntersect_noOverlap() public pure { @@ -83,5 +107,13 @@ contract ValidationDataLibTest is TestUtils { assertEq(result.validAfter, 3); assertEq(result.validUntil, 2); assertEq(result.authorizer, address(1)); + + a = ValidationData({validAfter: 1, validUntil: 2, authorizer: address(1)}); + b = ValidationData({validAfter: 3, validUntil: 4, authorizer: address(2)}); + bUint = b._packValidationData(); + result = a._intersectValidationData(bUint); + assertEq(result.validAfter, 3); + assertEq(result.validUntil, 2); + assertEq(result.authorizer, address(2)); } } diff --git a/test/msca/6900/v0.7/CircularDependencyMock.sol b/test/msca/6900/v0.7/CircularDependencyMock.sol new file mode 100644 index 0000000..93400ef --- /dev/null +++ b/test/msca/6900/v0.7/CircularDependencyMock.sol @@ -0,0 +1,118 @@ +/* + * Copyright 2025 Circle Internet Group, Inc. All rights reserved. + + * SPDX-License-Identifier: GPL-3.0-or-later + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +pragma solidity 0.8.24; + +import {PLUGIN_AUTHOR, PLUGIN_VERSION_1} from "../../../../src/common/Constants.sol"; +import {UnauthorizedCaller} from "../../../../src/common/Errors.sol"; + +import {IPlugin} from "../../../../src/msca/6900/v0.7/interfaces/IPlugin.sol"; +import {BasePlugin} from "../../../../src/msca/6900/v0.7/plugins/BasePlugin.sol"; +import {PackedUserOperation} from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; +import {console} from "forge-std/src/console.sol"; +import { + ManifestAssociatedFunction, + ManifestAssociatedFunctionType, + ManifestFunction, + PluginManifest, + PluginMetadata +} from "src/msca/6900/v0.7/common/PluginManifest.sol"; + +/** + * @dev Plugin that has circular dependency to itself. + */ +contract CircularDependencyMock is BasePlugin { + string public constant _NAME = "CircularDependencyMock Plugin"; + uint256 internal constant _OWNER_RUNTIME_VALIDATION_DEPENDENCY_INDEX = 0; + + enum FunctionId { + RUNTIME_VALIDATION_SELF + } + + function foo() external pure { + console.logString("foo()"); + } + + /// @inheritdoc BasePlugin + function onInstall(bytes calldata data) external pure override { + (data); + } + + /// @inheritdoc BasePlugin + function onUninstall(bytes calldata data) external pure override { + (data); + } + + /// @inheritdoc BasePlugin + function userOpValidationFunction(uint8 functionId, PackedUserOperation calldata userOp, bytes32 userOpHash) + external + pure + override + returns (uint256 validationData) + { + (functionId, userOp, userOpHash, validationData); + } + + /// @inheritdoc BasePlugin + function runtimeValidationFunction(uint8 functionId, address sender, uint256 value, bytes calldata data) + external + view + override + { + console.logString("runtimeValidationFunction()"); + (functionId, sender, value, data); + if (sender == msg.sender) { + return; + } + revert UnauthorizedCaller(); + } + + /// @inheritdoc BasePlugin + function pluginManifest() external pure override returns (PluginManifest memory) { + PluginManifest memory manifest; + manifest.dependencyInterfaceIds = new bytes4[](1); + manifest.dependencyInterfaceIds[_OWNER_RUNTIME_VALIDATION_DEPENDENCY_INDEX] = type(IPlugin).interfaceId; + // for a correct manifest, ManifestAssociatedFunctionType.SELF should be used, + // but for the purpose of this malicious plugin, we use ManifestAssociatedFunctionType.DEPENDENCY + ManifestFunction memory ownerRuntimeValidationFunction = ManifestFunction({ + functionType: ManifestAssociatedFunctionType.DEPENDENCY, + functionId: 0, // unused for dependency + dependencyIndex: _OWNER_RUNTIME_VALIDATION_DEPENDENCY_INDEX + }); + manifest.runtimeValidationFunctions = new ManifestAssociatedFunction[](1); + manifest.runtimeValidationFunctions[0] = ManifestAssociatedFunction({ + executionSelector: this.foo.selector, + associatedFunction: ownerRuntimeValidationFunction + }); + return manifest; + } + + /// @inheritdoc BasePlugin + function pluginMetadata() external pure virtual override returns (PluginMetadata memory) { + PluginMetadata memory metadata; + metadata.name = _NAME; + metadata.version = PLUGIN_VERSION_1; + metadata.author = PLUGIN_AUTHOR; + return metadata; + } + + /// @inheritdoc BasePlugin + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + return super.supportsInterface(interfaceId); + } +} diff --git a/test/msca/6900/v0.7/FunctionReferenceDLLLib.t.sol b/test/msca/6900/v0.7/FunctionReferenceDLLLib.t.sol new file mode 100644 index 0000000..3ebd2ad --- /dev/null +++ b/test/msca/6900/v0.7/FunctionReferenceDLLLib.t.sol @@ -0,0 +1,168 @@ +/* + * Copyright 2025 Circle Internet Group, Inc. All rights reserved. + + * SPDX-License-Identifier: GPL-3.0-or-later + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +pragma solidity 0.8.24; + +import {SENTINEL_BYTES21} from "../../../../src/common/Constants.sol"; + +import {FunctionReference} from "../../../../src/msca/6900/v0.7/common/Structs.sol"; +import {FunctionReferenceLib} from "../../../../src/msca/6900/v0.7/libs/FunctionReferenceLib.sol"; +import {TestUtils} from "../../../util/TestUtils.sol"; +import {TestFunctionReferenceDLL} from "./TestFunctionReferenceDLL.sol"; + +contract FunctionReferenceDLLibTest is TestUtils { + using FunctionReferenceLib for bytes21; + using FunctionReferenceLib for FunctionReference; + + function testSentinelFunctionReference() public { + TestFunctionReferenceDLL dll = new TestFunctionReferenceDLL(); + assertEq(dll.getFirst().pack(), SENTINEL_BYTES21); + assertEq(dll.getLast().pack(), SENTINEL_BYTES21); + // sentinel should not considered as the value of the list + assertFalse(dll.contains(SENTINEL_BYTES21.unpack())); + // add one item + assertTrue(dll.append(FunctionReference(address(0x123), 0))); + // sentinel should not considered as the value of the list + assertFalse(dll.contains(SENTINEL_BYTES21.unpack())); + } + + function testAddRemoveGetFunctionReferences() public { + TestFunctionReferenceDLL dll = new TestFunctionReferenceDLL(); + assertEq(dll.getAll().length, 0); + // try to remove sentinel stupidly + bytes4 errorSelector = bytes4(keccak256("InvalidFunctionReference()")); + vm.expectRevert(abi.encodeWithSelector(errorSelector)); + dll.remove(SENTINEL_BYTES21.unpack()); + assertEq(dll.getFirst().pack(), SENTINEL_BYTES21); + assertEq(dll.getLast().pack(), SENTINEL_BYTES21); + // sentinel doesn't count + assertEq(dll.getSize(), 0); + FunctionReference memory fr123 = FunctionReference(address(0x123), 0); + FunctionReference memory fr456 = FunctionReference(address(0x456), 0); + FunctionReference memory fr789 = FunctionReference(address(0x789), 0); + FunctionReference memory frabc = FunctionReference(address(0xabc), 0); + assertTrue(dll.append(fr123)); + assertEq(dll.getFirst().pack(), fr123.pack()); + assertEq(dll.getLast().pack(), fr123.pack()); + // try to add the same function reference again, should revert + errorSelector = bytes4(keccak256("ItemAlreadyExists()")); + vm.expectRevert(abi.encodeWithSelector(errorSelector)); + dll.append(fr123); + // now remove it + assertTrue(dll.remove(fr123)); + // remove it again, should revert + errorSelector = bytes4(keccak256("ItemDoesNotExist()")); + vm.expectRevert(abi.encodeWithSelector(errorSelector)); + dll.remove(fr123); + // add fr123 back with one more fr + assertTrue(dll.append(fr123)); + assertTrue(dll.append(fr456)); + assertEq(dll.getSize(), 2); + assertEq(dll.getFirst().pack(), fr123.pack()); + assertEq(dll.getLast().pack(), fr456.pack()); + // now remove fr456 + assertTrue(dll.remove(fr456)); + assertEq(dll.getFirst().pack(), fr123.pack()); + assertEq(dll.getLast().pack(), fr123.pack()); + // now add back fr456 with three more frs + assertTrue(dll.append(fr456)); + assertTrue(dll.append(fr789)); + assertTrue(dll.append(frabc)); + assertEq(dll.getSize(), 4); + assertEq(dll.getFirst().pack(), fr123.pack()); + assertEq(dll.getLast().pack(), frabc.pack()); + FunctionReference[] memory results = dll.getAll(); + assertEq(results.length, 4); + assertEq(results[0].pack(), fr123.pack()); + assertEq(results[1].pack(), fr456.pack()); + assertEq(results[2].pack(), fr789.pack()); + assertEq(results[3].pack(), frabc.pack()); + // now remove frabc + assertTrue(dll.remove(frabc)); + assertEq(dll.getSize(), 3); + assertEq(dll.getFirst().pack(), fr123.pack()); + assertEq(dll.getLast().pack(), fr789.pack()); + // now remove fr789 + assertTrue(dll.remove(fr789)); + assertEq(dll.getSize(), 2); + assertEq(dll.getFirst().pack(), fr123.pack()); + assertEq(dll.getLast().pack(), fr456.pack()); + // now remove fr456 + assertTrue(dll.remove(fr456)); + assertEq(dll.getSize(), 1); + assertEq(dll.getFirst().pack(), fr123.pack()); + assertEq(dll.getLast().pack(), fr123.pack()); + // now remove fr123 + assertTrue(dll.remove(fr123)); + assertEq(dll.getSize(), 0); + assertEq(dll.getFirst().pack(), SENTINEL_BYTES21); + assertEq(dll.getLast().pack(), SENTINEL_BYTES21); + // now remove fr456 again, should revert + errorSelector = bytes4(keccak256("ItemDoesNotExist()")); + vm.expectRevert(abi.encodeWithSelector(errorSelector)); + dll.remove(fr456); + // get zero fr every time + errorSelector = bytes4(keccak256("InvalidLimit()")); + vm.expectRevert(abi.encodeWithSelector(errorSelector)); + dll.getPaginated(SENTINEL_BYTES21.unpack(), 0); + } + + function testBulkGetFunctionReferences() public { + // try out different limits, even bigger than totalFRs + for (uint256 limit = 1; limit <= 10; limit++) { + // 4 plugins + bulkAddAndFunctionReferences(new TestFunctionReferenceDLL(), 4, limit); + } + for (uint256 limit = 1; limit <= 25; limit++) { + bulkAddAndFunctionReferences(new TestFunctionReferenceDLL(), 20, limit); + } + for (uint256 limit = 1; limit <= 26; limit++) { + bulkAddAndFunctionReferences(new TestFunctionReferenceDLL(), 20, limit); + } + } + + function bulkAddAndFunctionReferences(TestFunctionReferenceDLL dll, uint256 totalFRs, uint256 limit) private { + for (uint256 i = 2; i <= totalFRs; i++) { + assertTrue(dll.append(FunctionReference(vm.addr(i), 0))); + } + bulkGetFunctionReferences(dll, totalFRs, limit); + } + + function bulkGetFunctionReferences(TestFunctionReferenceDLL dll, uint256 totalFRs, uint256 limit) private view { + FunctionReference[] memory results = new FunctionReference[](totalFRs); + FunctionReference memory start = SENTINEL_BYTES21.unpack(); + uint256 count = 0; + uint256 j = 0; + FunctionReference[] memory frs; + FunctionReference memory next; + while (count < totalFRs) { + (frs, next) = dll.getPaginated(start, limit); + for (uint256 i = 0; i < frs.length; i++) { + results[count] = frs[i]; + // vm.addr starts from 2 + assertEq(results[j].plugin, vm.addr(count + 2)); + count++; + j++; + } + if (next.pack() == SENTINEL_BYTES21) { + break; + } + start = next; + } + } +} diff --git a/test/msca/6900/v0.7/HookFunctionReferenceDLL.t.sol b/test/msca/6900/v0.7/HookFunctionReferenceDLL.t.sol index 678119b..6bb7718 100644 --- a/test/msca/6900/v0.7/HookFunctionReferenceDLL.t.sol +++ b/test/msca/6900/v0.7/HookFunctionReferenceDLL.t.sol @@ -30,8 +30,8 @@ contract HookFunctionReferenceDLLTest is TestUtils { function testAddRemoveGetPreValidationHooks() public { TestRepeatableFunctionReferenceDLL dll = new TestRepeatableFunctionReferenceDLL(); - // sentinel hook is initialized - assertEq(dll.getRepeatedCountOfPreValidationHook(SENTINEL_BYTES21.unpack()), 1); + // sentinel hook is not part of the list + assertEq(dll.getRepeatedCountOfPreValidationHook(SENTINEL_BYTES21.unpack()), 0); // try to remove sentinel stupidly bytes4 errorSelector = bytes4(keccak256("InvalidFunctionReference()")); vm.expectRevert(abi.encodeWithSelector(errorSelector)); diff --git a/test/msca/6900/v0.7/PluginExecutor.t.sol b/test/msca/6900/v0.7/PluginExecutor.t.sol index bc53921..2f9dd80 100644 --- a/test/msca/6900/v0.7/PluginExecutor.t.sol +++ b/test/msca/6900/v0.7/PluginExecutor.t.sol @@ -39,6 +39,7 @@ import {TestPermitAnyExternalAddressPlugin} from "./TestPermitAnyExternalAddress import {TestPermitAnyExternalAddressWithPostHookOnlyPlugin} from "./TestPermitAnyExternalAddressWithPostHookOnlyPlugin.sol"; +import {ExecutionUtils} from "../../../../src/utils/ExecutionUtils.sol"; import {TestPermitAnyExternalAddressWithPreHookOnlyPlugin} from "./TestPermitAnyExternalAddressWithPreHookOnlyPlugin.sol"; import {TestTokenPlugin} from "./TestTokenPlugin.sol"; @@ -53,6 +54,7 @@ import {console} from "forge-std/src/console.sol"; contract PluginExecutorTest is TestUtils { using FunctionReferenceLib for bytes21; using FunctionReferenceLib for FunctionReference; + using ExecutionUtils for address; // upgrade event Upgraded(address indexed newImplementation); @@ -1178,4 +1180,27 @@ contract PluginExecutorTest is TestUtils { assertEq(shortLiquidityPool.balanceOf(mscaAddr), 234); vm.stopPrank(); } + + function testShortCalldataIntoExecuteFromPluginToExternal() public { + // deployment was done in setUp + assertTrue(address(msca).code.length != 0); + // start with balance + vm.deal(address(msca), 10 ether); + bytes32 manifestHash = keccak256(abi.encode(testTokenPlugin.pluginManifest())); + // airdrop 1000 tokens + bytes memory pluginInstallData = abi.encode(1000); + FunctionReference[] memory dependencies = new FunctionReference[](1); + vm.startPrank(address(msca)); + // import SingleOwnerPlugin as dependency + dependencies[0] = + FunctionReference(address(singleOwnerPlugin), uint8(ISingleOwnerPlugin.FunctionId.USER_OP_VALIDATION_OWNER)); + msca.installPlugin(address(testTokenPlugin), manifestHash, pluginInstallData, dependencies); + + address externalTarget = testTokenPlugin.LONG_LIQUIDITY_POOL_ADDR(); + uint256 initialBal = externalTarget.balance; + bytes memory data = abi.encodeCall(testTokenPlugin.callExecuteFromPluginExternal, (1, bytes(""))); + address(msca).callWithReturnDataOrRevert(0, data); + assertEq(externalTarget.balance, initialBal + 1); + vm.stopPrank(); + } } diff --git a/test/msca/6900/v0.7/PluginManager.t.sol b/test/msca/6900/v0.7/PluginManager.t.sol index f6974be..9b51bf4 100644 --- a/test/msca/6900/v0.7/PluginManager.t.sol +++ b/test/msca/6900/v0.7/PluginManager.t.sol @@ -36,6 +36,7 @@ import {TestCircleMSCA} from "./TestCircleMSCA.sol"; import {TestCircleMSCAFactory} from "./TestCircleMSCAFactory.sol"; import {TestTokenPlugin} from "./TestTokenPlugin.sol"; +import {CircularDependencyMock} from "./CircularDependencyMock.sol"; import {TestUserOpValidatorWithDependencyHook} from "./TestUserOpValidatorWithDependencyHook.sol"; import {EntryPoint} from "@account-abstraction/contracts/core/EntryPoint.sol"; import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; @@ -59,6 +60,7 @@ contract PluginManagerTest is TestUtils { uint256 actualGasCost, uint256 actualGasUsed ); + event PluginInstalled(address indexed plugin, bytes32 manifestHash, FunctionReference[] dependencies); IEntryPoint private entryPoint = new EntryPoint(); PluginManager private pluginManager = new PluginManager(); @@ -284,4 +286,21 @@ contract PluginManagerTest is TestUtils { msca.installPlugin(address(testValidatorHook), manifestHash, "", dependencies); vm.stopPrank(); } + + function testInstallSelfReferentialPlugin() public { + CircularDependencyMock circularDependencyMock = new CircularDependencyMock(); + FunctionReference[] memory dependencies = new FunctionReference[](1); + dependencies[0] = FunctionReference( + address(circularDependencyMock), uint8(CircularDependencyMock.FunctionId.RUNTIME_VALIDATION_SELF) + ); + bytes32 manifestHash = keccak256(abi.encode(circularDependencyMock.pluginManifest())); + vm.startPrank(address(msca)); + bytes4 errorSelector = bytes4(keccak256("InvalidPluginDependency(address)")); + vm.expectRevert(abi.encodeWithSelector(errorSelector, address(circularDependencyMock))); + msca.installPlugin(address(circularDependencyMock), manifestHash, "", dependencies); + + // would fail now, confirming the right fix.. + // msca.uninstallPlugin(address(circularDependencyMock), "", ""); + vm.stopPrank(); + } } diff --git a/test/msca/6900/v0.7/RepeatableFunctionReferenceDLLLib.t.sol b/test/msca/6900/v0.7/RepeatableFunctionReferenceDLLLib.t.sol index c21c04e..4e30774 100644 --- a/test/msca/6900/v0.7/RepeatableFunctionReferenceDLLLib.t.sol +++ b/test/msca/6900/v0.7/RepeatableFunctionReferenceDLLLib.t.sol @@ -29,10 +29,23 @@ contract RepeatableFunctionReferenceDLLibTest is TestUtils { using FunctionReferenceLib for bytes21; using FunctionReferenceLib for FunctionReference; + function testSentinelRepeatableFunctionReference() public { + TestRepeatableFunctionReferenceDLL dll = new TestRepeatableFunctionReferenceDLL(); + assertEq(dll.getFirstPreValidationHook().pack(), SENTINEL_BYTES21); + assertEq(dll.getLastPreValidationHook().pack(), SENTINEL_BYTES21); + // sentinel should not considered as the value of the list + assertEq(dll.getRepeatedCountOfPreValidationHook(SENTINEL_BYTES21.unpack()), 0); + // add one item + assertEq(dll.appendPreValidationHook(FunctionReference(address(0x123), 0)), 1); + // sentinel should not considered as the value of the list + assertEq(dll.getRepeatedCountOfPreValidationHook(SENTINEL_BYTES21.unpack()), 0); + } + function testAddRemoveGetPreValidationHooks() public { TestRepeatableFunctionReferenceDLL dll = new TestRepeatableFunctionReferenceDLL(); - // sentinel hook is initialized - assertEq(dll.getRepeatedCountOfPreValidationHook(SENTINEL_BYTES21.unpack()), 1); + assertEq(dll.getAllPreValidationHooks().length, 0); + // sentinel hook is not part of the list + assertEq(dll.getRepeatedCountOfPreValidationHook(SENTINEL_BYTES21.unpack()), 0); // try to remove sentinel stupidly bytes4 errorSelector = bytes4(keccak256("InvalidFunctionReference()")); vm.expectRevert(abi.encodeWithSelector(errorSelector)); diff --git a/test/msca/6900/v0.7/SelectorRegistryLib.t.sol b/test/msca/6900/v0.7/SelectorRegistryLib.t.sol new file mode 100644 index 0000000..f020ff5 --- /dev/null +++ b/test/msca/6900/v0.7/SelectorRegistryLib.t.sol @@ -0,0 +1,26 @@ +/* + * Copyright 2025 Circle Internet Group, Inc. All rights reserved. + * SPDX-License-Identifier: GPL-3.0-or-later + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +pragma solidity 0.8.24; + +import {SelectorRegistryLib} from "../../../../src/msca/6900/v0.7/libs/SelectorRegistryLib.sol"; + +import {TestUtils} from "../../../util/TestUtils.sol"; +import {IAccount} from "@account-abstraction/contracts/interfaces/IAccount.sol"; + +contract SelectorRegistryLibTest is TestUtils { + function testValidateUserOpSelector() public pure { + assertTrue(SelectorRegistryLib._isNativeFunctionSelector(IAccount.validateUserOp.selector)); + } +} diff --git a/test/msca/6900/v0.7/SingleOwnerMSCA.t.sol b/test/msca/6900/v0.7/SingleOwnerMSCA.t.sol index 98a1702..f4c67a7 100644 --- a/test/msca/6900/v0.7/SingleOwnerMSCA.t.sol +++ b/test/msca/6900/v0.7/SingleOwnerMSCA.t.sol @@ -25,7 +25,6 @@ import { InvalidInitializationInput, InvalidValidationFunctionId } from "../../../../src/msca/6900/shared/common/Errors.sol"; import {InvalidExecutionFunction} from "../../../../src/msca/6900/shared/common/Errors.sol"; -import {ValidationData} from "../../../../src/msca/6900/shared/common/Structs.sol"; import {SingleOwnerMSCA} from "../../../../src/msca/6900/v0.7/account/semi/SingleOwnerMSCA.sol"; import { Call, @@ -86,15 +85,14 @@ contract SingleOwnerMSCATest is TestUtils { uint256 actualGasCost, uint256 actualGasUsed ); - - error FailedOp(uint256 opIndex, string reason); - event UserOperationRevertReason( bytes32 indexed userOpHash, address indexed sender, uint256 nonce, bytes revertReason ); // MSCA error WalletStorageIsInitialized(); + error FailedOp(uint256 opIndex, string reason); + error NotFoundSelector(); IEntryPoint private entryPoint = new EntryPoint(); PluginManager private pluginManager = new PluginManager(); @@ -857,6 +855,14 @@ contract SingleOwnerMSCATest is TestUtils { return sender; } + function testShortCalldataIntoFallback() public { + // deployment was done in setUp + SingleOwnerMSCA msca = factory.createAccount(address(this), 0, abi.encode(address(this))); + // fail early even before InvalidValidationFunctionId is reverted + vm.expectRevert(NotFoundSelector.selector); + address(msca).callWithReturnDataOrRevert(0, bytes("aaa")); + } + function installSingleOwnerPlugin(address semiMSCA, uint256 ownerPrivateKey, address ownerInPlugin) internal { bytes32 manifestHash = keccak256(abi.encode(singleOwnerPlugin.pluginManifest())); // nonce key is 0 @@ -983,21 +989,4 @@ contract SingleOwnerMSCATest is TestUtils { assertEq(SingleOwnerMSCA(payable(semiMSCA)).getNativeOwner(), address(0)); vm.stopPrank(); } - - /** - * @dev Unpack into the deserialized packed format from validAfter | validUntil | authorizer. - */ - function _unpackValidationData(uint256 validationDataInt) - internal - pure - returns (ValidationData memory validationData) - { - address authorizer = address(uint160(validationDataInt)); - uint48 validUntil = uint48(validationDataInt >> 160); - if (validUntil == 0) { - validUntil = type(uint48).max; - } - uint48 validAfter = uint48(validationDataInt >> (48 + 160)); - return ValidationData(validAfter, validUntil, authorizer); - } } diff --git a/test/msca/6900/v0.7/SingleOwnerPlugin.t.sol b/test/msca/6900/v0.7/SingleOwnerPlugin.t.sol index 28e9d8a..52919b6 100644 --- a/test/msca/6900/v0.7/SingleOwnerPlugin.t.sol +++ b/test/msca/6900/v0.7/SingleOwnerPlugin.t.sol @@ -48,6 +48,8 @@ import {PackedUserOperation} from "@account-abstraction/contracts/interfaces/Pac import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; + +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {console} from "forge-std/src/console.sol"; /// Tests for SingleOwnerPlugin @@ -437,4 +439,26 @@ contract SingleOwnerPluginTest is TestUtils { ); singleOwnerPlugin.postExecutionHook(functionId, data); } + + // they are also tested during signature signing + function testFuzz_relaySafeMessageHash(bytes32 hash) public view { + address account = address(msca1); + bytes32 replaySafeHash = singleOwnerPlugin.getReplaySafeMessageHash(account, hash); + bytes32 expected = MessageHashUtils.toTypedDataHash({ + domainSeparator: keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" + ), + keccak256(abi.encodePacked("Single Owner Plugin")), + keccak256(abi.encodePacked("1.0.0")), + block.chainid, + address(singleOwnerPlugin), + bytes32(bytes20(account)) + ) + ), + structHash: keccak256(abi.encode(keccak256("CircleSingleOwnerPluginMessage(bytes32 hash)"), hash)) + }); + assertEq(replaySafeHash, expected); + } } diff --git a/test/msca/6900/v0.7/TestFunctionReferenceDLL.sol b/test/msca/6900/v0.7/TestFunctionReferenceDLL.sol new file mode 100644 index 0000000..b4b309b --- /dev/null +++ b/test/msca/6900/v0.7/TestFunctionReferenceDLL.sol @@ -0,0 +1,64 @@ +/* + * Copyright 2025 Circle Internet Group, Inc. All rights reserved. + + * SPDX-License-Identifier: GPL-3.0-or-later + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +pragma solidity 0.8.24; + +import {Bytes21DLL, FunctionReference} from "../../../../src/msca/6900/v0.7/common/Structs.sol"; +import {FunctionReferenceDLLLib} from "../../../../src/msca/6900/v0.7/libs/FunctionReferenceDLLLib.sol"; + +contract TestFunctionReferenceDLL { + using FunctionReferenceDLLLib for Bytes21DLL; + + Bytes21DLL private frs; + + function append(FunctionReference memory fr) external returns (bool) { + return frs.append(fr); + } + + function remove(FunctionReference memory fr) external returns (bool) { + return frs.remove(fr); + } + + function contains(FunctionReference memory fr) external view returns (bool) { + return frs.contains(fr); + } + + function getSize() external view returns (uint256) { + return frs.size(); + } + + function getAll() external view returns (FunctionReference[] memory results) { + return frs.getAll(); + } + + function getPaginated(FunctionReference memory startFR, uint256 limit) + external + view + returns (FunctionReference[] memory results, FunctionReference memory next) + { + return frs.getPaginated(startFR, limit); + } + + function getFirst() external view returns (FunctionReference memory) { + return frs.getHead(); + } + + function getLast() external view returns (FunctionReference memory) { + return frs.getTail(); + } +} diff --git a/test/msca/6900/v0.7/TestTokenPlugin.sol b/test/msca/6900/v0.7/TestTokenPlugin.sol index a262cf0..b8e18c6 100644 --- a/test/msca/6900/v0.7/TestTokenPlugin.sol +++ b/test/msca/6900/v0.7/TestTokenPlugin.sol @@ -115,6 +115,11 @@ contract TestTokenPlugin is BasePlugin { return true; } + function callExecuteFromPluginExternal(uint256 value, bytes calldata data) external returns (bool) { + IPluginExecutor(msg.sender).executeFromPluginExternal(LONG_LIQUIDITY_POOL_ADDR, value, data); + return true; + } + // externalFromPluginExternal is allowed // supply to only long liquidity pool function supplyLiquidity(address to, uint256 value) external { @@ -249,7 +254,7 @@ contract TestTokenPlugin is BasePlugin { function pluginManifest() external pure override returns (PluginManifest memory) { PluginManifest memory manifest; /// executionFunctions - manifest.executionFunctions = new bytes4[](7); + manifest.executionFunctions = new bytes4[](8); // Execution functions defined in this plugin to be installed on the MSCA. manifest.executionFunctions[0] = this.transferToken.selector; manifest.executionFunctions[1] = this.balanceOf.selector; @@ -258,6 +263,7 @@ contract TestTokenPlugin is BasePlugin { manifest.executionFunctions[4] = this.mintToken.selector; manifest.executionFunctions[5] = this.supplyLiquidity.selector; manifest.executionFunctions[6] = this.supplyLiquidityBad.selector; + manifest.executionFunctions[7] = this.callExecuteFromPluginExternal.selector; /// permittedExecutionSelectors // Native functions or execution functions already installed on the MSCA that this plugin will be @@ -511,6 +517,7 @@ contract TestTokenPlugin is BasePlugin { }) }); + manifest.canSpendNativeToken = true; manifest.dependencyInterfaceIds = new bytes4[](1); manifest.dependencyInterfaceIds[0] = type(ISingleOwnerPlugin).interfaceId; return manifest; diff --git a/test/msca/6900/v0.7/UpgradableMSCA.t.sol b/test/msca/6900/v0.7/UpgradableMSCA.t.sol index ef929a3..e92a3a2 100644 --- a/test/msca/6900/v0.7/UpgradableMSCA.t.sol +++ b/test/msca/6900/v0.7/UpgradableMSCA.t.sol @@ -1110,6 +1110,53 @@ contract UpgradableMSCATest is TestUtils { vm.stopPrank(); } + function testOneHookPassesButTheOtherHookFailWithInvalidAuthorizer() public { + (ownerAddr, eoaPrivateKey) = makeAddrAndKey("testOneHookPassesButTheOtherHookFailWithInvalidAuthorizer"); + TestCircleMSCA msca = new TestCircleMSCA(entryPoint, pluginManager); + // 0xb61d27f6 + bytes4 functionSelector = bytes4(0xb61d27f6); + // 2 is invalid + ValidationData memory expectValidatorToFail = ValidationData(0, 1691493273, address(2)); + FunctionReference memory userOpValidator = + FunctionReference(address(new TestUserOpValidator(expectValidatorToFail)), 3); + FunctionReference memory runtimeValidator; + address executionPlugin = vm.addr(1); + ExecutionFunctionConfig memory executionFunctionConfig = + ExecutionFunctionConfig(executionPlugin, userOpValidator, runtimeValidator); + msca.initExecutionDetail(functionSelector, executionFunctionConfig); + + ValidationData memory expectValidatorHookToPass = ValidationData(1, 3, address(0)); + FunctionReference memory preUserOpValidationHook1 = + FunctionReference(address(new TestValidatorHook(expectValidatorHookToPass)), 3); + + ValidationData memory expectValidatorHookToFail = ValidationData(2, 4, address(1)); + FunctionReference memory preUserOpValidationHook2 = + FunctionReference(address(new TestValidatorHook(expectValidatorHookToFail)), 3); + msca.setPreUserOpValidationHook(functionSelector, preUserOpValidationHook1); + msca.setPreUserOpValidationHook(functionSelector, preUserOpValidationHook2); + PackedUserOperation memory userOp = buildPartialUserOp( + address(msca), + 28, + "0x", + "0xb61d27f600000000000000000000000007865c6e87b9f70255377e024ace6630c1eaa37f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044a9059cbb0000000000000000000000009005be081b8ec2a31258878409e88675cd79137600000000000000000000000000000000000000000000000000000000001e848000000000000000000000000000000000000000000000000000000000", + 83353, + 102865, + 45484, + 516219199704, + 1130000000, + "0x79cbffe6dd3c3cb46aab6ef51f1a4accb5567f4e0000000000000000000000000000000000000000000000000000000064d223990000000000000000000000000000000000000000000000000000000064398d19" + ); + + vm.startPrank(address(entryPoint)); + bytes32 userOpHash = entryPoint.getUserOpHash(userOp); + bytes memory signature = signUserOpHash(entryPoint, vm, eoaPrivateKey, userOp); + userOp.signature = signature; + bytes4 selector = bytes4(keccak256("InvalidAuthorizer()")); + vm.expectRevert(abi.encodeWithSelector(selector)); + msca.validateUserOp(userOp, userOpHash, 0); + vm.stopPrank(); + } + /** * @dev Unpack into the deserialized packed format from validAfter | validUntil | authorizer. */ diff --git a/test/msca/6900/v0.7/plugins/WeightedWebauthnMultisigPlugin.t.sol b/test/msca/6900/v0.7/plugins/WeightedWebauthnMultisigPlugin.t.sol index 2e0e5a9..1122fd7 100644 --- a/test/msca/6900/v0.7/plugins/WeightedWebauthnMultisigPlugin.t.sol +++ b/test/msca/6900/v0.7/plugins/WeightedWebauthnMultisigPlugin.t.sol @@ -65,6 +65,7 @@ import {WebAuthnLib} from "../../../../../src/libs/WebAuthnLib.sol"; import {WeightedWebauthnMultisigPlugin} from "../../../../../src/msca/6900/v0.7/plugins/v1_0_0/multisig/WeightedWebauthnMultisigPlugin.sol"; +import {FCL_Elliptic_ZZ} from "@fcl/FCL_elliptic.sol"; import {stdJson} from "forge-std/src/StdJson.sol"; import {VmSafe} from "forge-std/src/Vm.sol"; import {console} from "forge-std/src/console.sol"; @@ -77,10 +78,10 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { using stdJson for string; using MessageHashUtils for bytes32; - event OwnersAdded(address account, bytes30[] owners, OwnerData[] weights); - event OwnersRemoved(address account, bytes30[] owners, uint256 totalWeightRemoved); - event OwnersUpdated(address account, bytes30[] owners, OwnerData[] weights); - event ThresholdUpdated(address account, uint256 oldThresholdWeight, uint256 newThresholdWeight); + event OwnersAdded(address indexed account, bytes30[] owners, OwnerData[] weights); + event OwnersRemoved(address indexed account, bytes30[] owners, uint256 totalWeightRemoved); + event OwnersUpdated(address indexed account, bytes30[] owners, OwnerData[] weights); + event ThresholdUpdated(address indexed account, uint256 oldThresholdWeight, uint256 newThresholdWeight); error AlreadyInitialized(); error EmptyOwnersNotAllowed(); @@ -94,14 +95,16 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { error ThresholdWeightExceedsTotalWeight(uint256 thresholdWeight, uint256 totalWeight); error TooManyOwners(uint256 currentNumOwners, uint256 numOwnersToAdd); error ZeroOwnersInputNotAllowed(); - error ECDSAInvalidSignature(); + error InvalidPublicKey(uint256 x, uint256 y); + error FailToGeneratePublicKey(uint256 x, uint256 y); + error UnsupportedSigType(uint8 sigType); WeightedWebauthnMultisigPlugin private plugin; address private account; address private ownerOne = address(1); address private ownerTwo = address(2); - PublicKey private pubKeyOne = PublicKey({x: 1, y: 1}); - PublicKey private pubKeyTwo = PublicKey({x: 2, y: 2}); + PublicKey private pubKeyOne; + PublicKey private pubKeyTwo; uint256 private weightOne = 100; uint256 private weightTwo = 101; uint256 private pubKeyWeightOne = 100; @@ -246,6 +249,8 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { weightOneList.push(weightOne); ownerTwoList.push(ownerTwo); weightTwoList.push(weightTwo); + pubKeyOne = _generateRandomPublicKey(1); + pubKeyTwo = _generateRandomPublicKey(2); pubKeyOneList.push(pubKeyOne); pubKeyWeightOneList.push(pubKeyWeightOne); pubKeyTwoList.push(pubKeyTwo); @@ -292,7 +297,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { for (uint256 i = 1; i <= _MAX_OWNERS + 1; i++) { _addresses[i - 1] = vm.addr(i); _weights[i - 1] = 1; - _pubKeys[i - 1] = PublicKey(i, i); + _pubKeys[i - 1] = _generateRandomPublicKey(i); } vm.expectRevert(abi.encodeWithSelector(TooManyOwners.selector, 0, _MAX_OWNERS * 2 + 2)); @@ -324,11 +329,23 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { _invalidWeightList[0] = _invalidWeight; vm.expectRevert(abi.encodeWithSelector(InvalidWeight.selector, ownerOne.toBytes30(), account, _invalidWeight)); + vm.prank(account); plugin.onInstall( abi.encode(ownerOneList, _invalidWeightList, pubKeyOneList, pubKeyWeightOneList, thresholdWeightOne) ); } + function testFuzz_onInstallInvalidPublicKey(uint256 x, uint256 y) public { + vm.assume(x >= FCL_Elliptic_ZZ.p); + PublicKey[] memory badPubKeysToAdd = new PublicKey[](1); + badPubKeysToAdd[0] = PublicKey(x, y); + + vm.expectRevert(abi.encodeWithSelector(InvalidPublicKey.selector, badPubKeysToAdd[0].x, badPubKeysToAdd[0].y)); + plugin.onInstall( + abi.encode(ownerOneList, weightOneList, badPubKeysToAdd, pubKeyWeightOneList, thresholdWeightOne) + ); + } + function test_onUninstall() public { _install(); vm.prank(account); @@ -475,9 +492,16 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { vm.assume(input.owner2 != input.owner3); vm.assume(input.owner3 != input.owner1); - vm.assume(!(input.pubKey1.x == 0 && input.pubKey1.y == 0)); - vm.assume(!(input.pubKey2.x == 0 && input.pubKey2.y == 0)); - vm.assume(!(input.pubKey3.x == 0 && input.pubKey3.y == 0)); + vm.assume(input.pubKey1.x != 0); + vm.assume(input.pubKey2.x != 0); + vm.assume(input.pubKey3.x != 0); + vm.assume( + (input.pubKey1.x != input.pubKey2.x) && (input.pubKey2.x != input.pubKey3.x) + && (input.pubKey3.x != input.pubKey1.x) + ); + input.pubKey1 = _generateRandomPublicKey(input.pubKey1.x); + input.pubKey2 = _generateRandomPublicKey(input.pubKey2.x); + input.pubKey3 = _generateRandomPublicKey(input.pubKey3.x); vm.assume(!_isSame(input.pubKey1, input.pubKey2)); vm.assume(!_isSame(input.pubKey2, input.pubKey3)); vm.assume(!_isSame(input.pubKey3, input.pubKey1)); @@ -516,6 +540,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { initialPubKeyWeights[1] = input.pubKeyWeight2; uint256 initialThresholdWeight = input.weight1 + input.weight2 + input.pubKeyWeight1 + input.pubKeyWeight2; + vm.prank(account); plugin.onInstall( abi.encode(initialOwners, initialWeights, initialPubKeys, initialPubKeyWeights, initialThresholdWeight) ); @@ -604,7 +629,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { address[] memory _addresses = new address[](1); _addresses[0] = vm.addr(i); PublicKey[] memory _pubKeys = new PublicKey[](1); - _pubKeys[0] = PublicKey(i + 2, i + 2); + _pubKeys[0] = _generateRandomPublicKey(i + 2); vm.prank(account); plugin.addOwners(_addresses, weightOneList, _pubKeys, pubKeyWeightOneList, 0); } @@ -612,7 +637,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { address[] memory _lastAddress = new address[](1); _lastAddress[0] = vm.addr(_MAX_OWNERS + 1); PublicKey[] memory _lastPubKeys = new PublicKey[](1); - _lastPubKeys[0] = PublicKey(_MAX_OWNERS + 2, _MAX_OWNERS + 2); + _lastPubKeys[0] = _generateRandomPublicKey(_MAX_OWNERS + 2); vm.prank(account); vm.expectRevert(abi.encodeWithSelector(TooManyOwners.selector, _MAX_OWNERS, 2)); plugin.addOwners(_lastAddress, weightOneList, _lastPubKeys, pubKeyWeightOneList, 0); @@ -629,6 +654,17 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { plugin.addOwners(badOwnersToAdd, weightOneList, new PublicKey[](0), new uint256[](0), thresholdWeightOne); } + function testFuzz_addOwnersInvalidPublicKey(uint256 x, uint256 y) public { + vm.assume(x >= FCL_Elliptic_ZZ.p); + PublicKey[] memory badPubKeysToAdd = new PublicKey[](1); + badPubKeysToAdd[0] = PublicKey(x, y); + _install(); + + vm.prank(account); + vm.expectRevert(abi.encodeWithSelector(InvalidPublicKey.selector, badPubKeysToAdd[0].x, badPubKeysToAdd[0].y)); + plugin.addOwners(ownerOneList, weightOneList, badPubKeysToAdd, pubKeyWeightOneList, thresholdWeightOne); + } + function test_addOwners_invalidWeight_zero() public { _install(); @@ -727,9 +763,16 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { vm.assume(input.owner2 != input.owner3); vm.assume(input.owner3 != input.owner1); - vm.assume(!(input.pubKey1.x == 0 && input.pubKey1.y == 0)); - vm.assume(!(input.pubKey2.x == 0 && input.pubKey2.y == 0)); - vm.assume(!(input.pubKey3.x == 0 && input.pubKey3.y == 0)); + vm.assume(input.pubKey1.x != 0); + vm.assume(input.pubKey2.x != 0); + vm.assume(input.pubKey3.x != 0); + vm.assume( + (input.pubKey1.x != input.pubKey2.x) && (input.pubKey2.x != input.pubKey3.x) + && (input.pubKey3.x != input.pubKey1.x) + ); + input.pubKey1 = _generateRandomPublicKey(input.pubKey1.x); + input.pubKey2 = _generateRandomPublicKey(input.pubKey2.x); + input.pubKey3 = _generateRandomPublicKey(input.pubKey3.x); vm.assume(!_isSame(input.pubKey1, input.pubKey2)); vm.assume(!_isSame(input.pubKey2, input.pubKey3)); vm.assume(!_isSame(input.pubKey3, input.pubKey1)); @@ -771,7 +814,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { initialPubKeyWeights[2] = input.pubKeyWeight3; uint256 initialThresholdWeight = input.weight1 + input.weight2 + input.pubKeyWeight1 + input.pubKeyWeight2; - + vm.prank(account); plugin.onInstall( abi.encode(initialOwners, initialWeights, initialPubKeys, initialPubKeyWeights, initialThresholdWeight) ); @@ -1183,9 +1226,16 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { vm.assume(input.owner2 != input.owner3); vm.assume(input.owner3 != input.owner1); - vm.assume(!(input.pubKey1.x == 0 && input.pubKey1.y == 0)); - vm.assume(!(input.pubKey2.x == 0 && input.pubKey2.y == 0)); - vm.assume(!(input.pubKey3.x == 0 && input.pubKey3.y == 0)); + vm.assume(input.pubKey1.x != 0); + vm.assume(input.pubKey2.x != 0); + vm.assume(input.pubKey3.x != 0); + vm.assume( + (input.pubKey1.x != input.pubKey2.x) && (input.pubKey2.x != input.pubKey3.x) + && (input.pubKey3.x != input.pubKey1.x) + ); + input.pubKey1 = _generateRandomPublicKey(input.pubKey1.x); + input.pubKey2 = _generateRandomPublicKey(input.pubKey2.x); + input.pubKey3 = _generateRandomPublicKey(input.pubKey3.x); vm.assume(!_isSame(input.pubKey1, input.pubKey2)); vm.assume(!_isSame(input.pubKey2, input.pubKey3)); vm.assume(!_isSame(input.pubKey3, input.pubKey1)); @@ -1229,6 +1279,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { uint256 initialThresholdWeight = input.weight1 + input.weight2 + input.weight3 + input.weight1 + input.weight2 + input.weight3; + vm.prank(account); plugin.onInstall( abi.encode(initialOwners, initialWeights, initialPubKeys, initialPubKeyWeights, initialThresholdWeight) ); @@ -1462,9 +1513,16 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { function _installPluginForAddPubKeyOnlyOwnersThenK1Owner(AddPubKeyOnlyOwnersThenK1OwnerInput memory input) internal { - vm.assume(!(input.pubKey1.x == 0 && input.pubKey1.y == 0)); - vm.assume(!(input.pubKey2.x == 0 && input.pubKey2.y == 0)); - vm.assume(!(input.pubKey3.x == 0 && input.pubKey3.y == 0)); + vm.assume(input.pubKey1.x != 0); + vm.assume(input.pubKey2.x != 0); + vm.assume(input.pubKey3.x != 0); + vm.assume( + (input.pubKey1.x != input.pubKey2.x) && (input.pubKey2.x != input.pubKey3.x) + && (input.pubKey3.x != input.pubKey1.x) + ); + input.pubKey1 = _generateRandomPublicKey(input.pubKey1.x); + input.pubKey2 = _generateRandomPublicKey(input.pubKey2.x); + input.pubKey3 = _generateRandomPublicKey(input.pubKey3.x); vm.assume(!_isSame(input.pubKey1, input.pubKey2)); vm.assume(!_isSame(input.pubKey2, input.pubKey3)); vm.assume(!_isSame(input.pubKey3, input.pubKey1)); @@ -1494,6 +1552,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { initialPubKeyWeights[1] = input.pubKeyWeight2; uint256 initialThresholdWeight = input.pubKeyWeight1 + input.pubKeyWeight2; + vm.prank(account); plugin.onInstall( abi.encode(initialOwners, initialWeights, initialPubKeys, initialPubKeyWeights, initialThresholdWeight) ); @@ -1538,9 +1597,16 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { } function _installPluginForRemovePubKeyOnlyOwners(RemovePubKeyOnlyOwnersInput memory input) internal { - vm.assume(!(input.pubKey1.x == 0 && input.pubKey1.y == 0)); - vm.assume(!(input.pubKey2.x == 0 && input.pubKey2.y == 0)); - vm.assume(!(input.pubKey3.x == 0 && input.pubKey3.y == 0)); + vm.assume(input.pubKey1.x != 0); + vm.assume(input.pubKey2.x != 0); + vm.assume(input.pubKey3.x != 0); + vm.assume( + (input.pubKey1.x != input.pubKey2.x) && (input.pubKey2.x != input.pubKey3.x) + && (input.pubKey3.x != input.pubKey1.x) + ); + input.pubKey1 = _generateRandomPublicKey(input.pubKey1.x); + input.pubKey2 = _generateRandomPublicKey(input.pubKey2.x); + input.pubKey3 = _generateRandomPublicKey(input.pubKey3.x); vm.assume(!_isSame(input.pubKey1, input.pubKey2)); vm.assume(!_isSame(input.pubKey2, input.pubKey3)); vm.assume(!_isSame(input.pubKey3, input.pubKey1)); @@ -1563,6 +1629,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { initialPubKeyWeights[2] = input.pubKeyWeight3; uint256 initialThresholdWeight = input.pubKeyWeight1 + input.pubKeyWeight2 + input.pubKeyWeight3; + vm.startPrank(account); plugin.onInstall( abi.encode(initialOwners, initialWeights, initialPubKeys, initialPubKeyWeights, initialThresholdWeight) ); @@ -1583,9 +1650,31 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { assertEq(returnedOwners[2], input.pubKey1.toBytes30()); assertEq(returnedOwnersData[2].weight, input.pubKeyWeight1); assertEq(returnedThresholdWeight, initialThresholdWeight); + vm.stopPrank(); } function testFuzz_updateMultisigWeightsPubKeyOnly(UpdateMultisigWeightsPubKeyOnlyInput memory input) public { + vm.assume(input.pubKey1.x != 0); + vm.assume(input.pubKey2.x != 0); + vm.assume(input.pubKey3.x != 0); + vm.assume( + (input.pubKey1.x != input.pubKey2.x) && (input.pubKey2.x != input.pubKey3.x) + && (input.pubKey3.x != input.pubKey1.x) + ); + input.pubKey1 = _generateRandomPublicKey(input.pubKey1.x); + input.pubKey2 = _generateRandomPublicKey(input.pubKey2.x); + input.pubKey3 = _generateRandomPublicKey(input.pubKey3.x); + vm.assume(!_isSame(input.pubKey1, input.pubKey2)); + vm.assume(!_isSame(input.pubKey2, input.pubKey3)); + vm.assume(!_isSame(input.pubKey3, input.pubKey1)); + + input.weight1 = bound(input.weight1, 1, _MAX_WEIGHT); + input.weight2 = bound(input.weight2, 1, _MAX_WEIGHT); + input.weight3 = bound(input.weight3, 1, _MAX_WEIGHT); + input.weight4 = bound(input.weight4, 1, _MAX_WEIGHT); + input.weight5 = bound(input.weight5, 1, _MAX_WEIGHT); + input.weight6 = bound(input.weight6, 1, _MAX_WEIGHT); + address[] memory initialOwners; PublicKey[] memory initialPubKeys = new PublicKey[](3); initialPubKeys[0] = input.pubKey1; @@ -1626,20 +1715,6 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { address[] memory initialOwners, PublicKey[] memory initialPubKeys ) internal { - vm.assume(!(input.pubKey1.x == 0 && input.pubKey1.y == 0)); - vm.assume(!(input.pubKey2.x == 0 && input.pubKey2.y == 0)); - vm.assume(!(input.pubKey3.x == 0 && input.pubKey3.y == 0)); - vm.assume(!_isSame(input.pubKey1, input.pubKey2)); - vm.assume(!_isSame(input.pubKey2, input.pubKey3)); - vm.assume(!_isSame(input.pubKey3, input.pubKey1)); - - input.weight1 = bound(input.weight1, 1, _MAX_WEIGHT); - input.weight2 = bound(input.weight2, 1, _MAX_WEIGHT); - input.weight3 = bound(input.weight3, 1, _MAX_WEIGHT); - input.weight4 = bound(input.weight4, 1, _MAX_WEIGHT); - input.weight5 = bound(input.weight5, 1, _MAX_WEIGHT); - input.weight6 = bound(input.weight6, 1, _MAX_WEIGHT); - uint256[] memory initialWeights; uint256[] memory initialPubKeyWeights = new uint256[](3); initialPubKeyWeights[0] = input.weight1; @@ -1647,6 +1722,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { initialPubKeyWeights[2] = input.weight3; uint256 initialThresholdWeight = input.weight1 + input.weight2 + input.weight3; + vm.prank(account); plugin.onInstall( abi.encode(initialOwners, initialWeights, initialPubKeys, initialPubKeyWeights, initialThresholdWeight) ); @@ -1999,7 +2075,8 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { assertEq(EIP1271_INVALID_SIGNATURE, plugin.isValidSignature(digest, signature)); } - function testIsValidSignature_invalidThresholdWeight(bytes32 digest) public { + function testIsValidSignature_invalidThresholdWeight() public { + bytes32 digest = bytes32(0); bytes memory _sig = bytes("0x0000000000000000000000000000000000000000000000000000000000000000"); // test does not install, so no owner has a threshold vm.expectRevert(abi.encodeWithSelector(InvalidThresholdWeight.selector)); @@ -2026,7 +2103,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { listOfTwoWeights[1] = weightTwo; // 101 uint256 threshold = weightOne + weightTwo; - + vm.prank(account); plugin.onInstall(abi.encode(listOfTwoOwners, listOfTwoWeights, new PublicKey[](0), new uint256[](0), threshold)); // 2. create a valid signature for installed owner @@ -2049,9 +2126,11 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { account: account, signatures: sigWithFooAppended }); - (bool success, uint256 firstFailure) = plugin.checkNSignatures(input); + (bool success, uint256 firstFailure, IWeightedMultisigPlugin.CheckNSignatureError returnError) = + plugin.checkNSignatures(input); assertEq(success, false); assertEq(firstFailure, 1); + assertEq(uint8(returnError), uint8(IWeightedMultisigPlugin.CheckNSignatureError.SIG_PARTS_OVERLAP)); vm.prank(account); assertEq(EIP1271_INVALID_SIGNATURE, plugin.isValidSignature(bytes32(0), sigWithFooAppended)); @@ -2074,7 +2153,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { // sig check should fail (not owner) vm.prank(account); - vm.expectRevert(ECDSAInvalidSignature.selector); + vm.expectRevert(abi.encodeWithSelector(UnsupportedSigType.selector, 57)); plugin.isValidSignature(digest, sig); } @@ -2084,7 +2163,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { // incorrect digest bytes32 messageDigest = bytes32("foo"); vm.prank(account); - vm.expectRevert(ECDSAInvalidSignature.selector); + vm.expectRevert(abi.encodeWithSelector(UnsupportedSigType.selector, 66)); IWeightedMultisigPlugin.CheckNSignatureInput memory input = IWeightedMultisigPlugin.CheckNSignatureInput({ actualDigest: messageDigest, minimalDigest: messageDigest, @@ -2102,7 +2181,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/5212e8eb1830be145cc7b6b2c955c7667a74e14c/test/utils/cryptography/ECDSA.test.js#L195C7-L195C92 bytes32 messageDigest = 0xb94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9; vm.prank(account); - vm.expectRevert(ECDSAInvalidSignature.selector); + vm.expectRevert(abi.encodeWithSelector(UnsupportedSigType.selector, 66)); IWeightedMultisigPlugin.CheckNSignatureInput memory input = IWeightedMultisigPlugin.CheckNSignatureInput({ actualDigest: messageDigest, minimalDigest: messageDigest, @@ -2135,9 +2214,11 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { account: account, signatures: sig }); - (bool success, uint256 firstFailure) = plugin.checkNSignatures(input); + (bool success, uint256 firstFailure, IWeightedMultisigPlugin.CheckNSignatureError returnError) = + plugin.checkNSignatures(input); assertEq(success, false); assertEq(firstFailure, 0); + assertEq(uint8(returnError), uint8(IWeightedMultisigPlugin.CheckNSignatureError.SIG_PARTS_OVERLAP)); } function testFuzz_userOpValidationFunction_eoaOwner(string memory salt, PackedUserOperation memory userOp) public { @@ -2361,7 +2442,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { for (uint256 i = 0; i < input.k; i++) { sigDynamicParts = abi.encodePacked( sigDynamicParts, - _SignIndividualOwnerSignature( + _signIndividualOwnerSignature( offset, userOp, owners[i], @@ -2377,7 +2458,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { userOp.signature = abi.encodePacked(userOp.signature, sigDynamicParts); } - function _SignIndividualOwnerSignature( + function _signIndividualOwnerSignature( uint256[] memory offset, PackedUserOperation memory userOp, Owner memory owner, @@ -2666,7 +2747,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { uint256[] memory weightsToAdd1 = new uint256[](1); weightsToAdd1[0] = weightOne; - + vm.prank(account); plugin.onInstall(abi.encode(ownersToAdd1, weightsToAdd1, new PublicKey[](0), new uint256[](0), weightOne)); // sign minimal user op hash @@ -2867,6 +2948,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { vm.expectEmit(true, true, true, true); emit ThresholdUpdated(account, 0, 2); + vm.prank(account); // thresholdWeight = 2 plugin.onInstall(abi.encode(ownerList, weightList, emptyPubKeyList, emptyPubKeyWeightList, 2)); @@ -2944,6 +3026,54 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { assertEq(ownershipMetadata.numOwners, 1); } + // they are also tested during signature signing + function testFuzz_relaySafeMessageHash(bytes32 hash) public view { + bytes32 replaySafeHash = plugin.getReplaySafeMessageHash(account, hash); + bytes32 expected = MessageHashUtils.toTypedDataHash({ + domainSeparator: keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" + ), + keccak256(abi.encodePacked("Weighted Multisig Webauthn Plugin")), + keccak256(abi.encodePacked("1.0.0")), + block.chainid, + address(plugin), + bytes32(bytes20(account)) + ) + ), + structHash: keccak256(abi.encode(keccak256("CircleWeightedWebauthnMultisigMessage(bytes32 hash)"), hash)) + }); + assertEq(replaySafeHash, expected); + } + + function testIsValidPublicKey_returnsFalse_whenX0Y0() public pure { + assertFalse(FCL_Elliptic_ZZ.ecAff_isOnCurve(0, 0)); + } + + function testIsValidPublicKey_returnsFalse_whenX0() public pure { + assertFalse(FCL_Elliptic_ZZ.ecAff_isOnCurve(0, FCL_Elliptic_ZZ.gy)); + } + + function testIsValidPublicKey_returnsFalse_whenY0() public pure { + assertFalse(FCL_Elliptic_ZZ.ecAff_isOnCurve(FCL_Elliptic_ZZ.gx, 0)); + } + + function testFuzz_isValidPublicKey_returnsFalse_whenXGreaterThanEqualP(uint256 x) public pure { + vm.assume(x >= FCL_Elliptic_ZZ.p); + assertFalse(FCL_Elliptic_ZZ.ecAff_isOnCurve(x, FCL_Elliptic_ZZ.gy)); + } + + function testFuzz_isValidPublicKey_returnsFalse_whenYGreaterThanEqualP(uint256 y) public pure { + vm.assume(y >= FCL_Elliptic_ZZ.p); + assertFalse(FCL_Elliptic_ZZ.ecAff_isOnCurve(FCL_Elliptic_ZZ.gx, y)); + } + + function testFuzz_isValidPublicKey(uint256 random) public view { + (uint256 x, uint256 y) = _generateRandomPoint(random); + assertTrue(FCL_Elliptic_ZZ.ecAff_isOnCurve(x, y)); + } + function _addOwners( address[] memory _owners, uint256[] memory _weights, @@ -2974,6 +3104,7 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { } function _install() internal { + vm.prank(account); vm.expectEmit(true, true, true, true); (bytes30[] memory _tOwners, OwnerData[] memory _tWeights) = _mergeOwnersData(ownerOneList, weightOneList, pubKeyOneList, pubKeyWeightOneList); @@ -3214,4 +3345,25 @@ contract WeightedWebauthnMultisigPluginTest is TestUtils { function toAddress(bytes30 addrInBytes30) internal pure returns (address) { return address(uint160(uint240(addrInBytes30))); } + + function _generateRandomPublicKey(uint256 randomScalar) internal view returns (PublicKey memory) { + (uint256 x, uint256 y) = _generateRandomPoint(randomScalar); + return PublicKey(x, y); + } + + // Generate a random point on secp256r1 + function _generateRandomPoint(uint256 randomScalar) internal view returns (uint256 x, uint256 y) { + if (randomScalar == 0 || randomScalar >= FCL_Elliptic_ZZ.n) { + // as multiplication with 0 always results in the neutral point (not a valid public key) + randomScalar = 1; + } + // Perform scalar multiplication (k * G) + (x, y) = FCL_Elliptic_ZZ.ecZZ_mulmuladd(FCL_Elliptic_ZZ.gx, FCL_Elliptic_ZZ.gy, randomScalar, 0); // Q = (Gx, + // Gy), scalar_u = k + if (FCL_Elliptic_ZZ.ecAff_isOnCurve(x, y)) { + return (x, y); + } else { + revert FailToGeneratePublicKey(x, y); + } + } } diff --git a/test/msca/6900/v0.7/storage/WalletStorageV1Lib.t.sol b/test/msca/6900/v0.7/storage/WalletStorageV1Lib.t.sol index defb46f..ae00d69 100644 --- a/test/msca/6900/v0.7/storage/WalletStorageV1Lib.t.sol +++ b/test/msca/6900/v0.7/storage/WalletStorageV1Lib.t.sol @@ -51,11 +51,12 @@ contract WalletStorageV1LibTest is TestUtils { assertEq(hash, 0xc6a0cc20c824c4eecc4b0fbb7fb297d07492a7bd12c83d4fa4d27b4249f9bfc8); } + // this test is very similar to AddressDLLLibTest, but under the context of plugin and wallet function testAddRemoveGetPlugins() public { (ownerAddr, eoaPrivateKey) = makeAddrAndKey("testAddRemoveGetPlugins"); TestCircleMSCA msca = new TestCircleMSCA(entryPoint, pluginManager); - // sentinel address is initialized - assertTrue(msca.containsPlugin(SENTINEL_ADDRESS)); + // sentinel address is not considered as the value of the list + assertFalse(msca.containsPlugin(SENTINEL_ADDRESS)); // try to remove sentinel stupidly bytes4 selector = bytes4(keccak256("InvalidAddress()")); vm.expectRevert(abi.encodeWithSelector(selector)); @@ -116,9 +117,9 @@ contract WalletStorageV1LibTest is TestUtils { function testAddRemoveGetPreUserOpValidationHooks() public { (ownerAddr, eoaPrivateKey) = makeAddrAndKey("testAddRemoveGetPreUserOpValidationHooks"); TestCircleMSCA msca = new TestCircleMSCA(entryPoint, pluginManager); - // sentinel hook is initialized + // sentinel hook is not part of the list bytes4 selector = bytes4(0xb61d27f6); - assertEq(msca.containsPreUserOpValidationHook(selector, SENTINEL_BYTES21.unpack()), 1); + assertEq(msca.containsPreUserOpValidationHook(selector, SENTINEL_BYTES21.unpack()), 0); // try to remove sentinel stupidly bytes4 errorSelector = bytes4(keccak256("InvalidFunctionReference()")); vm.expectRevert(abi.encodeWithSelector(errorSelector)); diff --git a/test/msca/6900/v0.8/DirectCallsFromModule.t.sol b/test/msca/6900/v0.8/DirectCallsFromModule.t.sol index a29525a..7aea879 100644 --- a/test/msca/6900/v0.8/DirectCallsFromModule.t.sol +++ b/test/msca/6900/v0.8/DirectCallsFromModule.t.sol @@ -98,7 +98,7 @@ contract DirectCallsFromModuleTest is AccountTestUtils { /* -------------------------------------------------------------------------- */ /* Negatives */ /* -------------------------------------------------------------------------- */ - + // TODO: use test_Revert because testFail has been deprecated function testFailDirectCallModuleNotInstalled() public { vm.startPrank(address(directCallModule)); vm.expectRevert( diff --git a/test/msca/6900/v0.8/UpgradableMSCA.t.sol b/test/msca/6900/v0.8/UpgradableMSCA.t.sol index 4f7a12f..71f0fd8 100644 --- a/test/msca/6900/v0.8/UpgradableMSCA.t.sol +++ b/test/msca/6900/v0.8/UpgradableMSCA.t.sol @@ -59,6 +59,7 @@ import {EntryPoint} from "@account-abstraction/contracts/core/EntryPoint.sol"; import {ValidationDataView} from "@erc6900/reference-implementation/interfaces/IModularAccountView.sol"; +import {ExecutionUtils} from "../../../../src/utils/ExecutionUtils.sol"; import {MockModule} from "./helpers/MockModule.sol"; import {IAccountExecute} from "@account-abstraction/contracts/interfaces/IAccountExecute.sol"; import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; @@ -70,8 +71,9 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U contract UpgradableMSCATest is AccountTestUtils { using ModuleEntityLib for bytes21; using ModuleEntityLib for ModuleEntity; - // upgrade + using ExecutionUtils for address; + // upgrade event Upgraded(address indexed newImplementation); // erc721 event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); @@ -94,6 +96,7 @@ contract UpgradableMSCATest is AccountTestUtils { event ReceivedCall(bytes msgData); error RuntimeValidationFailed(address module, uint32 entityId, bytes revertReason); + error NotFoundSelector(); IEntryPoint private entryPoint = new EntryPoint(); uint256 internal eoaPrivateKey; @@ -1249,6 +1252,18 @@ contract UpgradableMSCATest is AccountTestUtils { factory.createAccountWithValidation(addressToBytes32(ownerAddr), salt, initializingData); } + function testShortCalldataIntoFallback() public { + (ownerAddr, eoaPrivateKey) = makeAddrAndKey("testShortCalldataIntoFallback"); + ownerValidation = ModuleEntityLib.pack(address(singleSignerValidationModule), uint32(0)); + ValidationConfig validationConfig = ValidationConfigLib.pack(ownerValidation, true, true, true); + bytes memory initializingData = + abi.encode(validationConfig, new bytes4[](0), abi.encode(uint32(0), ownerAddr), new bytes[](0)); + UpgradableMSCA msca = factory.createAccountWithValidation(addressToBytes32(ownerAddr), salt, initializingData); + // fail early even before InvalidValidationFunctionId is reverted + vm.expectRevert(NotFoundSelector.selector); + address(msca).callWithReturnDataOrRevert(0, bytes("aaa")); + } + function _installMultipleOwnerValidations() internal returns (UpgradableMSCA msca) { ownerValidation = ModuleEntityLib.pack(address(singleSignerValidationModule), uint32(0)); ValidationConfig validationConfig = ValidationConfigLib.pack(ownerValidation, true, true, true); diff --git a/test/msca/6900/v0.8/UpgradableMSCAFactory.t.sol b/test/msca/6900/v0.8/UpgradableMSCAFactory.t.sol index 4e1cef4..9a0c443 100644 --- a/test/msca/6900/v0.8/UpgradableMSCAFactory.t.sol +++ b/test/msca/6900/v0.8/UpgradableMSCAFactory.t.sol @@ -83,6 +83,18 @@ contract UpgradableMSCAFactoryTest is AccountTestUtils { ownerValidation = ModuleEntityLib.pack(address(singleSignerValidationModule), uint32(0)); } + function testSetModuleAddressZero() public { + address[] memory _modules = new address[](1); + _modules[0] = address(0); + bool[] memory _permissions = new bool[](1); + _permissions[0] = true; + vm.startPrank(factoryOwner); + bytes4 errorSelector = bytes4(keccak256("ModuleIsNotAllowed(address)")); + vm.expectRevert(abi.encodeWithSelector(errorSelector, address(0))); + factory.setModules(_modules, _permissions); + vm.stopPrank(); + } + function testInstallDisabledModule() public { SingleSignerValidationModule maliciousModule = new SingleSignerValidationModule(); ownerValidation = ModuleEntityLib.pack(address(maliciousModule), uint32(0)); diff --git a/test/msca/6900/v0.8/modules/SingleSignerValidationModule.t.sol b/test/msca/6900/v0.8/modules/SingleSignerValidationModule.t.sol index e50d2c8..fd43888 100644 --- a/test/msca/6900/v0.8/modules/SingleSignerValidationModule.t.sol +++ b/test/msca/6900/v0.8/modules/SingleSignerValidationModule.t.sol @@ -26,7 +26,11 @@ import { } from "@erc6900/reference-implementation/interfaces/IModularAccountView.sol"; import {IModularAccount} from "@erc6900/reference-implementation/interfaces/IModularAccount.sol"; -import {ModuleEntity, ValidationConfig} from "@erc6900/reference-implementation/interfaces/IModularAccount.sol"; +import { + ModuleEntity, + ValidationConfig, + ValidationFlags +} from "@erc6900/reference-implementation/interfaces/IModularAccount.sol"; import {ModuleEntityLib} from "@erc6900/reference-implementation/libraries/ModuleEntityLib.sol"; import {ValidationConfigLib} from "@erc6900/reference-implementation/libraries/ValidationConfigLib.sol"; @@ -44,11 +48,14 @@ import {UpgradableMSCAFactory} from "../../../../../src/msca/6900/v0.8/factories import {EIP1271_INVALID_SIGNATURE, EIP1271_VALID_SIGNATURE} from "../../../../../src/common/Constants.sol"; import {PackedUserOperation} from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {console} from "forge-std/src/console.sol"; contract SingleSignerValidationModuleTest is AccountTestUtils { using ModuleEntityLib for bytes21; using ModuleEntityLib for ModuleEntity; + using ValidationConfigLib for ValidationFlags; // upgrade event Upgraded(address indexed newImplementation); @@ -100,20 +107,18 @@ contract SingleSignerValidationModuleTest is AccountTestUtils { factory.setModules(_modules, _permissions); vm.stopPrank(); - signerValidation = ModuleEntityLib.pack(address(singleSignerValidationModule), uint8(0)); + signerValidation = ModuleEntityLib.pack(address(singleSignerValidationModule), uint32(0)); (signerAddr1, eoaPrivateKey1) = makeAddrAndKey("Circle_Single_Signer_Validation_Module_V1_Test1"); (signerAddr2, eoaPrivateKey2) = makeAddrAndKey("Circle_Single_Signer_Validation_Module_V1_Test2"); bytes32 salt = 0x0000000000000000000000000000000000000000000000000000000000000000; - signerValidation = ModuleEntityLib.pack(address(singleSignerValidationModule), uint8(0)); ValidationConfig validationConfig = ValidationConfigLib.pack(signerValidation, true, true, true); bytes memory initializingData = - abi.encode(validationConfig, new bytes4[](0), abi.encode(uint8(0), signerAddr1), bytes(""), bytes("")); + abi.encode(validationConfig, new bytes4[](0), abi.encode(uint32(0), signerAddr1), bytes("")); vm.startPrank(signerAddr1); msca1 = factory.createAccountWithValidation(addressToBytes32(signerAddr1), salt, initializingData); vm.stopPrank(); vm.startPrank(signerAddr2); - initializingData = - abi.encode(validationConfig, new bytes4[](0), abi.encode(uint8(0), signerAddr2), bytes(""), bytes("")); + initializingData = abi.encode(validationConfig, new bytes4[](0), abi.encode(uint32(0), signerAddr2), bytes("")); msca2 = factory.createAccountWithValidation(addressToBytes32(signerAddr2), salt, initializingData); vm.stopPrank(); console.logString("Circle_Single_Signer_Validation_Module_V1 address:"); @@ -134,8 +139,8 @@ contract SingleSignerValidationModuleTest is AccountTestUtils { assertEq(validationData.selectors.length, 0); assertEq(validationData.validationHooks.length, 0); assertEq(validationData.executionHooks.length, 0); - assertEq(validationData.isGlobal, true); - assertEq(validationData.isSignatureValidation, true); + assertEq(validationData.validationFlags.isGlobal(), true); + assertEq(validationData.validationFlags.isSignatureValidation(), true); // verify executionDetail ExecutionDataView memory executionData = msca1.getExecutionData(singleSignerValidationModule.transferSigner.selector); @@ -179,14 +184,14 @@ contract SingleSignerValidationModuleTest is AccountTestUtils { assertEq(executionData.allowGlobalValidation, true); assertEq(executionData.executionHooks.length, 0); - assertEq(singleSignerValidationModule.moduleId(), "circle.single-signer-validation-module.2.0.0"); + assertEq(singleSignerValidationModule.moduleId(), "circle.single-signer-validation-module.1.0.0"); } /// fail because transferSigner was not installed in validation module function testTransferSignerWhenFunctionUninstalled() public { address sender = address(msca1); // it should start with the deployed signerAddr - assertEq(singleSignerValidationModule.signers(uint8(0), mscaAddr1), signerAddr1); + assertEq(singleSignerValidationModule.signers(uint32(0), mscaAddr1), signerAddr1); // could be any address, I'm using UpgradableMSCA for simplicity UpgradableMSCA newSigner = new UpgradableMSCA(entryPoint); // deployment was done in setUp @@ -196,7 +201,7 @@ contract SingleSignerValidationModuleTest is AccountTestUtils { // start with balance vm.deal(sender, 10 ether); bytes memory transferSignerCallData = - abi.encodeCall(singleSignerValidationModule.transferSigner, (uint8(0), address(newSigner))); + abi.encodeCall(singleSignerValidationModule.transferSigner, (uint32(0), address(newSigner))); bytes memory initCode = ""; PackedUserOperation memory userOp = buildPartialUserOp( sender, @@ -231,7 +236,7 @@ contract SingleSignerValidationModuleTest is AccountTestUtils { ); entryPoint.handleOps(ops, beneficiary); // won't change - assertEq(singleSignerValidationModule.signers(uint8(0), mscaAddr1), address(signerAddr1)); + assertEq(singleSignerValidationModule.signers(uint32(0), mscaAddr1), address(signerAddr1)); vm.stopPrank(); } @@ -239,7 +244,7 @@ contract SingleSignerValidationModuleTest is AccountTestUtils { function testTransferSignerViaExecuteFunction() public { address sender = address(msca2); // it should start with the deployed signerAddr - assertEq(singleSignerValidationModule.signers(uint8(0), mscaAddr2), signerAddr2); + assertEq(singleSignerValidationModule.signers(uint32(0), mscaAddr2), signerAddr2); // could be any address, I'm using UpgradableMSCA for simplicity UpgradableMSCA newSigner = new UpgradableMSCA(entryPoint); // deployment was done in setUp @@ -249,7 +254,7 @@ contract SingleSignerValidationModuleTest is AccountTestUtils { // start with balance vm.deal(sender, 10 ether); bytes memory transferSignerCallData = - abi.encodeCall(singleSignerValidationModule.transferSigner, (uint8(0), address(newSigner))); + abi.encodeCall(singleSignerValidationModule.transferSigner, (uint32(0), address(newSigner))); bytes memory executeCallData = abi.encodeCall(IModularAccount.execute, (address(singleSignerValidationModule), 0, transferSignerCallData)); bytes memory initCode = ""; @@ -280,13 +285,13 @@ contract SingleSignerValidationModuleTest is AccountTestUtils { vm.startPrank(address(entryPoint)); entryPoint.handleOps(ops, beneficiary); // now it's the new signer - assertEq(singleSignerValidationModule.signers(uint8(0), mscaAddr2), address(newSigner)); + assertEq(singleSignerValidationModule.signers(uint32(0), mscaAddr2), address(newSigner)); vm.stopPrank(); } function testTransferSignerViaRuntime() public { // it should start with the deployed signerAddr - assertEq(singleSignerValidationModule.signers(uint8(0), mscaAddr2), signerAddr2); + assertEq(singleSignerValidationModule.signers(uint32(0), mscaAddr2), signerAddr2); // could be any address, I'm using UpgradableMSCA for simplicity UpgradableMSCA newSigner = new UpgradableMSCA(entryPoint); // deployment was done in setUp @@ -294,7 +299,7 @@ contract SingleSignerValidationModuleTest is AccountTestUtils { // start with balance vm.deal(mscaAddr2, 10 ether); bytes memory transferSignerCallData = - abi.encodeCall(singleSignerValidationModule.transferSigner, (uint8(0), address(newSigner))); + abi.encodeCall(singleSignerValidationModule.transferSigner, (uint32(0), address(newSigner))); bytes memory executeCallData = abi.encodeCall(IModularAccount.execute, (address(singleSignerValidationModule), 0, transferSignerCallData)); @@ -305,14 +310,14 @@ contract SingleSignerValidationModuleTest is AccountTestUtils { executeCallData, encodeSignature(new PreValidationHookData[](0), signerValidation, bytes(""), true) ); // now it's the new signer - assertEq(singleSignerValidationModule.signers(uint8(0), mscaAddr2), address(newSigner)); + assertEq(singleSignerValidationModule.signers(uint32(0), mscaAddr2), address(newSigner)); vm.stopPrank(); } /// you can find more negative test cases in UpgradableMSCATest function testValidateSignature() public view { // it should start with the deployed signerAddr - assertEq(singleSignerValidationModule.signers(uint8(0), mscaAddr2), signerAddr2); + assertEq(singleSignerValidationModule.signers(uint32(0), mscaAddr2), signerAddr2); // deployment was done in setUp assertTrue(mscaAddr2.code.length != 0); // raw message hash @@ -329,4 +334,26 @@ contract SingleSignerValidationModuleTest is AccountTestUtils { encode1271Signature(new PreValidationHookData[](0), signerValidation, abi.encodePacked(r, s, uint32(0))); assertEq(msca2.isValidSignature(messageHash, signature), bytes4(EIP1271_INVALID_SIGNATURE)); } + + // they are also tested during signature signing + function testFuzz_relaySafeMessageHash(bytes32 hash) public view { + address account = address(msca1); + bytes32 replaySafeHash = singleSignerValidationModule.getReplaySafeMessageHash(account, hash); + bytes32 expected = MessageHashUtils.toTypedDataHash({ + domainSeparator: keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" + ), + keccak256(abi.encodePacked("circle.single-signer-validation-module.1.0.0")), + keccak256(abi.encodePacked("1.0.0")), + block.chainid, + address(singleSignerValidationModule), + bytes32(bytes20(account)) + ) + ), + structHash: keccak256(abi.encode(keccak256("SingleSignerValidationMessage(bytes message)"), hash)) + }); + assertEq(replaySafeHash, expected); + } } diff --git a/test/msca/6900/v0.8/modules/WeightedMultisigValidationModule.t.sol b/test/msca/6900/v0.8/modules/WeightedMultisigValidationModule.t.sol new file mode 100644 index 0000000..e2b8fc9 --- /dev/null +++ b/test/msca/6900/v0.8/modules/WeightedMultisigValidationModule.t.sol @@ -0,0 +1,2970 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + + * SPDX-License-Identifier: GPL-3.0-or-later + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +pragma solidity 0.8.24; + +/* solhint-disable max-states-count */ + +import {PublicKey, WebAuthnData, WebAuthnSigDynamicPart} from "../../../../../src/common/CommonStructs.sol"; +import {AddressBytesLib} from "../../../../../src/libs/AddressBytesLib.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +import {EntryPoint} from "@account-abstraction/contracts/core/EntryPoint.sol"; +import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; + +import { + Call, + IModularAccount, + ModuleEntity, + ValidationConfig +} from "@erc6900/reference-implementation/interfaces/IModularAccount.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +import {PublicKeyLib} from "../../../../../src/libs/PublicKeyLib.sol"; +import {ModuleEntityLib} from "@erc6900/reference-implementation/libraries/ModuleEntityLib.sol"; + +import { + AccountMetadata, + CheckNSignaturesRequest, + SignerMetadata, + SignerMetadataWithId +} from "../../../../../src/msca/6900/v0.8/modules/multisig/MultisigStructs.sol"; +import {SingleSignerValidationModule} from + "../../../../../src/msca/6900/v0.8/modules/validation/SingleSignerValidationModule.sol"; + +import {UpgradableMSCA} from "../../../../../src/msca/6900/v0.8/account/UpgradableMSCA.sol"; +import {UpgradableMSCAFactory} from "../../../../../src/msca/6900/v0.8/factories/UpgradableMSCAFactory.sol"; + +import {IWeightedMultisigValidationModule} from + "../../../../../src/msca/6900/v0.8/modules/multisig/IWeightedMultisigValidationModule.sol"; +import {WeightedMultisigValidationModule} from + "../../../../../src/msca/6900/v0.8/modules/multisig/WeightedMultisigValidationModule.sol"; + +import {AccountTestUtils} from "../utils/AccountTestUtils.sol"; + +import { + EIP1271_INVALID_SIGNATURE, + EIP1271_VALID_SIGNATURE, + SIG_VALIDATION_FAILED, + SIG_VALIDATION_SUCCEEDED, + ZERO, + ZERO_BYTES32 +} from "../../../../../src/common/Constants.sol"; +import {WebAuthnLib} from "../../../../../src/libs/WebAuthnLib.sol"; +import {MAX_SIGNERS} from "../../../../../src/msca/6900/v0.8/modules/multisig/MultisigConstants.sol"; +import {MockContractOwner} from "../../../../util/MockContractOwner.sol"; +import {PackedUserOperation} from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; +import {IModule} from "@erc6900/reference-implementation/interfaces/IModule.sol"; +import {IValidationModule} from "@erc6900/reference-implementation/interfaces/IValidationModule.sol"; +import {ValidationConfigLib} from "@erc6900/reference-implementation/libraries/ValidationConfigLib.sol"; +import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol"; + +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {stdJson} from "forge-std/src/StdJson.sol"; +import {VmSafe} from "forge-std/src/Vm.sol"; +import {console} from "forge-std/src/console.sol"; + +contract WeightedMultisigValidationModuleTest is AccountTestUtils { + using ECDSA for bytes32; + using PublicKeyLib for PublicKey[]; + using PublicKeyLib for PublicKey; + using AddressBytesLib for address; + using stdJson for string; + using MessageHashUtils for bytes32; + using Strings for uint256; + + event ValidationInstalled(address indexed module, uint32 indexed entityId); + event WalletStorageInitialized(); + event UpgradableMSCAInitialized(address indexed account, address indexed entryPointAddress); + event UserOperationEvent( + bytes32 indexed userOpHash, + address indexed sender, + address indexed paymaster, + uint256 nonce, + bool success, + uint256 actualGasCost, + uint256 actualGasUsed + ); + event AccountMetadataUpdated( + address indexed account, uint32 indexed entityId, AccountMetadata oldMetadata, AccountMetadata newMetadata + ); + event SignersAdded(address indexed account, uint32 indexed entityId, SignerMetadataWithId[] addedSigners); + event SignersRemoved(address indexed account, uint32 indexed entityId, SignerMetadataWithId[] removedSigners); + event SignersUpdated(address indexed account, uint32 indexed entityId, SignerMetadataWithId[] updatedSigners); + + error InvalidSignerWeight(uint32 entityId, address account, bytes30 signerId, uint256 weight); + error ZeroThresholdWeight(uint32 entityId, address account); + error SignerWeightsLengthMismatch(uint32 entityId, address account); + error ThresholdWeightExceedsTotalWeight(uint256 thresholdWeight, uint256 totalWeight); + error TooManySigners(uint256 numSigners); + error TooFewSigners(uint256 numSigners); + error InvalidSignerMetadata(uint32 entityId, address account, SignerMetadata signerMetadata); + error SignerIdAlreadyExists(uint32 entityId, address account, bytes30 signerId); + error SignerIdDoesNotExist(uint32 entityId, address account, bytes30 signerId); + error SignerMetadataDoesNotExist(uint32 entityId, address account, bytes30 signerId); + error SignerMetadataAlreadyExists(uint32 entityId, address account, SignerMetadata signerMetaData); + error AlreadyInitialized(uint32 entityId, address account); + error Uninitialized(uint32 entityId, address account); + error EmptyThresholdWeightAndSigners(uint32 entityId, address account); + error InvalidSigLength(uint32 entityId, address account, uint256 length); + error InvalidAddress(uint32 entityId, address account, address addr); + error InvalidSigOffset(uint32 entityId, address account, uint256 offset); + error InvalidNumSigsOnActualDigest(uint32 entityId, address account, uint256 numSigs); + error Unsupported(); + error InvalidUserOpDigest(uint32 entityId, address account); + + SingleSignerValidationModule private singleSignerValidationModule; + WeightedMultisigValidationModule private module; + UpgradableMSCAFactory private factory; + address payable private beneficiary; + UpgradableMSCA private msca; + // for WeightedMultisigValidationModule + ModuleEntity private multisigValidation; + uint32 private multisigEntityId = uint32(0); + address private factorySigner; + // SSVM = Single Signer Validation Module + uint32 private ecdsaSignerOneEntityIdForSSVM = uint32(1); + ModuleEntity private ecdsaSignerOneValidationForSSVM; + // public key signers + PublicKey private pubKeySignerOne = PublicKey({x: 1, y: 1}); + PublicKey private pubKeySignerTwo = PublicKey({x: 2, y: 2}); + IEntryPoint private entryPoint = new EntryPoint(); + bytes32 private salt = 0x0000000000000000000000000000000000000000000000000000000000000000; + string internal constant P256_10_KEYS_FIXTURE = "/test/fixtures/p256key_11_fixture.json"; + + struct Signer { + bytes30 signerId; + /// A wallet with a public and private key. + // struct Wallet { + // // The wallet's address. + // address addr; + // // The wallet's public key `X`. + // uint256 publicKeyX; + // // The wallet's public key `Y`. + // uint256 publicKeyY; + // // The wallet's private key. + // uint256 privateKey; + // } + VmSafe.Wallet signerWallet; + uint8 sigType; // e.g. 0: contract 2: r1, see Smart_Contract_Signatures_Encoding.md + address contractAddr; // sigType == 0 + } + + struct TestR1Key { + uint256 publicKeyX; + uint256 publicKeyY; + uint256 privateKey; + } + + Signer private eoaSignerOne; + Signer private eoaSignerTwo; + address private eoaSignerOneAddr; + address private eoaSignerTwoAddr; + uint256 private eoaSignerOnePrivateKey; + uint256 private eoaSignerTwoPrivateKey; + bytes30 private eoaSignerOneId; + bytes30 private eoaSignerTwoId; + + Signer private contractSignerOne; + Signer private contractSignerTwo; + + Signer private passKeySignerOne; + Signer private passKeySignerTwo; + PublicKey private passKeySignerOnePublicKey; + PublicKey private passKeySignerTwoPublicKey; + + struct AddAndRemoveSignersFuzzInput { + uint256 numOfSigner; + uint32 entityId; + uint256 signersToDelete; + } + + /// @dev Each signer must have weight 1 for this setup. + struct MultisigInput { + uint256 actualSigners; // number of signers that actually sign + uint256 totalSigners; // number of total signers + uint256 sigDynamicPartOffset; + } + + function setUp() public { + beneficiary = payable(address(makeAddr("bundler"))); + factorySigner = makeAddr("factorySigner"); + factory = new UpgradableMSCAFactory(factorySigner, address(entryPoint)); + singleSignerValidationModule = new SingleSignerValidationModule(); + module = new WeightedMultisigValidationModule(address(entryPoint)); + + address[] memory _modules = new address[](2); + _modules[0] = address(module); + // we enable singleSignerValidationModule for some test cases + _modules[1] = address(singleSignerValidationModule); + bool[] memory _permissions = new bool[](2); + _permissions[0] = true; + _permissions[1] = true; + vm.startPrank(factorySigner); + factory.setModules(_modules, _permissions); + vm.stopPrank(); + + ecdsaSignerOneValidationForSSVM = + ModuleEntityLib.pack(address(singleSignerValidationModule), ecdsaSignerOneEntityIdForSSVM); + // multisig + multisigValidation = ModuleEntityLib.pack(address(module), multisigEntityId); + // set up signers + eoaSignerOne = _createEOASigner("eoaSigner1"); + eoaSignerTwo = _createEOASigner("eoaSigner2"); + eoaSignerOneAddr = eoaSignerOne.signerWallet.addr; + eoaSignerTwoAddr = eoaSignerTwo.signerWallet.addr; + eoaSignerOneId = eoaSignerOne.signerId; + eoaSignerTwoId = eoaSignerTwo.signerId; + eoaSignerOnePrivateKey = eoaSignerOne.signerWallet.privateKey; + eoaSignerTwoPrivateKey = eoaSignerTwo.signerWallet.privateKey; + + contractSignerOne = _createContractSigner("contractSigner1"); + contractSignerTwo = _createContractSigner("contractSigner2"); + + passKeySignerOne = _createPasskeySigner(0); + passKeySignerTwo = _createPasskeySigner(1); + passKeySignerOnePublicKey = + PublicKey({x: passKeySignerOne.signerWallet.publicKeyX, y: passKeySignerOne.signerWallet.publicKeyY}); + passKeySignerTwoPublicKey = + PublicKey({x: passKeySignerTwo.signerWallet.publicKeyX, y: passKeySignerTwo.signerWallet.publicKeyY}); + } + + function testEntryPoint() public view { + assertEq(module.ENTRYPOINT(), address(entryPoint)); + } + + function testModuleId() public view { + assertEq(module.moduleId(), "circle.weighted-multisig-module.1.0.0"); + } + + // they are also tested during signature signing + function testFuzz_relaySafeMessageHash(bytes32 hash) public view { + address account = address(msca); + bytes32 replaySafeHash = module.getReplaySafeMessageHash(account, hash); + bytes32 expected = MessageHashUtils.toTypedDataHash({ + domainSeparator: keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" + ), + keccak256(abi.encodePacked("circle.weighted-multisig-module.1.0.0")), + keccak256(abi.encodePacked("1.0.0")), + block.chainid, + address(module), + bytes32(bytes20(account)) + ) + ), + structHash: keccak256(abi.encode(keccak256("CircleWeightedMultisigMessage(bytes message)"), hash)) + }); + assertEq(replaySafeHash, expected); + } + + function testSupportsInterfaces() public view { + assertTrue(module.supportsInterface(type(IWeightedMultisigValidationModule).interfaceId)); + assertTrue(module.supportsInterface(type(IValidationModule).interfaceId)); + assertTrue(module.supportsInterface(type(IERC165).interfaceId)); + assertTrue(module.supportsInterface(type(IModule).interfaceId)); + } + + // install WeightedMultisigValidationModule as part of deployment + function testCreateAccountWithMultisigVM() public { + ValidationConfig validationConfig = ValidationConfigLib.pack(multisigValidation, true, true, true); + SignerMetadata[] memory signersMetadataToAdd = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = eoaSignerTwoAddr; + signersMetadataToAdd[0] = signerMetaDataOne; + signersMetadataToAdd[1] = signerMetaDataTwo; + bytes memory installData = abi.encode(multisigEntityId, signersMetadataToAdd, 2); + bytes memory initializingData = abi.encode(validationConfig, new bytes4[](0), installData, new bytes[](0)); + (address counterfactualAddr,) = + factory.getAddressWithValidation(addressToBytes32(address(this)), salt, initializingData); + + // not needed for installation, only needed to verify the data + SignerMetadataWithId[] memory addedSigners = new SignerMetadataWithId[](2); + addedSigners[0].signerId = eoaSignerOneId; + addedSigners[0].signerMetadata = signersMetadataToAdd[0]; + addedSigners[1].signerId = eoaSignerTwoId; + addedSigners[1].signerMetadata = signersMetadataToAdd[1]; + vm.expectEmit(true, true, true, true); + emit SignersAdded(counterfactualAddr, multisigEntityId, addedSigners); + + vm.expectEmit(true, true, true, true); + emit AccountMetadataUpdated( + counterfactualAddr, multisigEntityId, AccountMetadata(0, 0, 0), AccountMetadata(2, 2, 2) + ); + + vm.expectEmit(true, true, true, true); + emit ValidationInstalled(address(module), multisigEntityId); + + vm.expectEmit(true, true, true, true); + emit UpgradableMSCAInitialized(counterfactualAddr, address(entryPoint)); + + vm.expectEmit(true, true, true, true); + emit WalletStorageInitialized(); + + msca = factory.createAccountWithValidation(addressToBytes32(address(this)), salt, initializingData); + assertEq(address(msca), counterfactualAddr); + + // verify module + SignerMetadataWithId[] memory signersMetadataRet = + module.signersMetadataOf(multisigEntityId, counterfactualAddr); + assertEq(signersMetadataRet[0].signerMetadata.addr, eoaSignerOneAddr); + assertEq(signersMetadataRet[0].signerMetadata.weight, 1); + assertFalse(signersMetadataRet[0].signerMetadata.publicKey.isValidPublicKey()); + assertEq(signersMetadataRet[0].signerId, eoaSignerOneId); + + assertEq(signersMetadataRet[1].signerMetadata.addr, eoaSignerTwoAddr); + assertEq(signersMetadataRet[1].signerMetadata.weight, 1); + assertFalse(signersMetadataRet[1].signerMetadata.publicKey.isValidPublicKey()); + assertEq(signersMetadataRet[1].signerId, eoaSignerTwoId); + + AccountMetadata memory accountMetadata = module.accountMetadataOf(multisigEntityId, counterfactualAddr); + assertEq(accountMetadata.numSigners, 2); + assertEq(accountMetadata.thresholdWeight, 2); + assertEq(accountMetadata.totalWeight, 2); + } + + // 1. create an account with SingleSignerValidationModule + // 2. install WeightedMultisigValidationModule (WMVM) with entityId 0 + // 3. uninstall WeightedMultisigValidationModule with entityId 0 + function testInstallAndUninstallWMVMAfterAccountCreation() public { + (address signer,) = makeAddrAndKey("testInstallAndUninstallWMVMAfterAccountCreation_signer"); + ModuleEntity signerValidation = ModuleEntityLib.pack(address(singleSignerValidationModule), uint32(3)); + // entityId is 3 + ValidationConfig singleSignerValidationConfig = ValidationConfigLib.pack(signerValidation, true, true, true); + bytes memory installData = abi.encode(uint32(3), signer); + bytes memory initializingData = + abi.encode(singleSignerValidationConfig, new bytes4[](0), installData, new bytes[](0)); + msca = factory.createAccountWithValidation(addressToBytes32(address(this)), salt, initializingData); + + ValidationConfig multisigValidationConfig = ValidationConfigLib.pack(multisigValidation, true, true, true); + SignerMetadata[] memory signersMetadataToAdd = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 3; + signerMetaDataTwo.addr = eoaSignerTwoAddr; + signersMetadataToAdd[0] = signerMetaDataOne; + signersMetadataToAdd[1] = signerMetaDataTwo; + installData = abi.encode(multisigEntityId, signersMetadataToAdd, 2); + + Call[] memory calls = new Call[](1); + calls[0] = Call( + address(msca), + 0, + abi.encodeCall( + IModularAccount.installValidation, + (multisigValidationConfig, new bytes4[](0), installData, new bytes[](0)) + ) + ); + + SignerMetadataWithId[] memory addedSigners = new SignerMetadataWithId[](2); + addedSigners[0].signerId = eoaSignerOneId; + addedSigners[0].signerMetadata = signersMetadataToAdd[0]; + addedSigners[1].signerId = eoaSignerTwoId; + addedSigners[1].signerMetadata = signersMetadataToAdd[1]; + vm.prank(address(msca)); + vm.expectEmit(true, true, true, true); + emit SignersAdded(address(msca), multisigEntityId, addedSigners); + + vm.expectEmit(true, true, true, true); + emit AccountMetadataUpdated(address(msca), multisigEntityId, AccountMetadata(0, 0, 0), AccountMetadata(2, 2, 4)); + + vm.expectEmit(true, true, true, true); + emit ValidationInstalled(address(module), multisigEntityId); + msca.executeWithRuntimeValidation( + abi.encodeCall(IModularAccount.executeBatch, (calls)), + encodeSignature(new PreValidationHookData[](0), signerValidation, bytes(""), true) + ); + + // verify module + SignerMetadataWithId[] memory signersMetadataRet = module.signersMetadataOf(multisigEntityId, address(msca)); + assertEq(signersMetadataRet[0].signerMetadata.addr, eoaSignerOneAddr); + assertEq(signersMetadataRet[0].signerMetadata.weight, 1); + assertFalse(signersMetadataRet[0].signerMetadata.publicKey.isValidPublicKey()); + assertEq(signersMetadataRet[0].signerId, eoaSignerOneId); + + assertEq(signersMetadataRet[1].signerMetadata.addr, eoaSignerTwoAddr); + assertEq(signersMetadataRet[1].signerMetadata.weight, 3); + assertFalse(signersMetadataRet[1].signerMetadata.publicKey.isValidPublicKey()); + assertEq(signersMetadataRet[1].signerId, eoaSignerTwoId); + + AccountMetadata memory accountMetadata = module.accountMetadataOf(multisigEntityId, address(msca)); + assertEq(accountMetadata.numSigners, 2); + assertEq(accountMetadata.thresholdWeight, 2); + assertEq(accountMetadata.totalWeight, 4); + _uninstallForTestInstallAndUninstallWMVMAfterAccountCreation(signerValidation, signersMetadataRet); + } + + function _uninstallForTestInstallAndUninstallWMVMAfterAccountCreation( + ModuleEntity signerValidation, + SignerMetadataWithId[] memory signersMetadataBeforeUninstall + ) internal { + bytes memory uninstallData = abi.encode(multisigEntityId); + bytes memory uninstallValidationData = + abi.encodeCall(IModularAccount.uninstallValidation, (multisigValidation, uninstallData, new bytes[](0))); + SignerMetadataWithId[] memory deletedSignersMetadata = + new SignerMetadataWithId[](signersMetadataBeforeUninstall.length); + for (uint256 i = 0; i < deletedSignersMetadata.length; ++i) { + // we delete the oldest signer first + // but signersMetadataOf returns the most recent signer first + deletedSignersMetadata[i] = signersMetadataBeforeUninstall[signersMetadataBeforeUninstall.length - i - 1]; + } + // still need signerValidation because WeightedMultisigValidationModule doesn't support runtime call yet + vm.prank(address(msca)); + vm.expectEmit(true, true, true, true); + emit SignersRemoved(address(msca), multisigEntityId, deletedSignersMetadata); + msca.executeWithRuntimeValidation( + uninstallValidationData, encodeSignature(new PreValidationHookData[](0), signerValidation, bytes(""), true) + ); + + // verify module doesn't have any data after uninstall + SignerMetadataWithId[] memory signersMetadataAfterUninstall = + module.signersMetadataOf(multisigEntityId, address(msca)); + assertEq(signersMetadataAfterUninstall.length, 0); + + AccountMetadata memory accountMetadata = module.accountMetadataOf(multisigEntityId, address(msca)); + assertEq(accountMetadata.numSigners, 0); + assertEq(accountMetadata.thresholdWeight, 0); + assertEq(accountMetadata.totalWeight, 0); + } + + function testInstallSameSignerWithDifferentWeightsOnDifferentEntityIds() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 2; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 2)); + // install another entity id with same signer but different weights + uint32 multisigEntityId2 = uint32(1); + signerMetaDataOne.weight = 4; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall( + // different threshold weight + abi.encode(multisigEntityId2, signersMetadata, 4) + ); + // verify the data for entityId(1) + SignerMetadataWithId[] memory addedSigners = module.signersMetadataOf(multisigEntityId2, address(msca)); + assertEq(addedSigners.length, 1); + assertEq(addedSigners[0].signerMetadata.addr, eoaSignerOneAddr); + assertEq(addedSigners[0].signerMetadata.weight, 4); + AccountMetadata memory accountMetadata = module.accountMetadataOf(multisigEntityId2, address(msca)); + assertEq(accountMetadata.numSigners, 1); + assertEq(accountMetadata.thresholdWeight, 4); + assertEq(accountMetadata.totalWeight, 4); + + // verify the data for entityId(0) + addedSigners = module.signersMetadataOf(multisigEntityId, address(msca)); + assertEq(addedSigners.length, 1); + assertEq(addedSigners[0].signerMetadata.addr, eoaSignerOneAddr); + assertEq(addedSigners[0].signerMetadata.weight, 2); + accountMetadata = module.accountMetadataOf(multisigEntityId, address(msca)); + assertEq(accountMetadata.numSigners, 1); + assertEq(accountMetadata.thresholdWeight, 2); + assertEq(accountMetadata.totalWeight, 2); + } + + function testInstallInvalidThresholdWeight() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.expectRevert(abi.encodeWithSelector(ZeroThresholdWeight.selector, multisigEntityId, address(msca))); + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 0)); + } + + function testInstallNoSignersMetadata() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](0); + vm.expectRevert(abi.encodeWithSelector(TooFewSigners.selector, 0)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 2)); + } + + function testInstallTooManySigners() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](MAX_SIGNERS + 1); + for (uint256 i = 0; i < signersMetadata.length; i++) { + SignerMetadata memory signerMetaData; + signerMetaData.weight = 1; + signerMetaData.addr = vm.addr(i + 1); + signersMetadata[i] = signerMetaData; + } + vm.expectRevert(abi.encodeWithSelector(TooManySigners.selector, signersMetadata.length)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 2)); + } + + function testInstallWithBothAddrAndPubKeyPresent() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + signerMetaDataOne.publicKey = pubKeySignerOne; + signersMetadata[0] = signerMetaDataOne; + vm.expectRevert( + abi.encodeWithSelector(InvalidSignerMetadata.selector, multisigEntityId, address(msca), signerMetaDataOne) + ); + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 2)); + } + + function testInstallWithBothAddrAndPubKeyAbsent() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + // no address or public key + signersMetadata[0] = signerMetaDataOne; + vm.expectRevert( + abi.encodeWithSelector(InvalidSignerMetadata.selector, multisigEntityId, address(msca), signerMetaDataOne) + ); + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 2)); + } + + function testInstallDuplicatedAddr() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataOne; + vm.expectRevert( + abi.encodeWithSelector( + SignerMetadataAlreadyExists.selector, multisigEntityId, address(msca), signerMetaDataOne + ) + ); + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 2)); + } + + function testInstallWithInvalidWeights() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 0; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.expectRevert( + abi.encodeWithSelector( + InvalidSignerWeight.selector, multisigEntityId, address(msca), eoaSignerOneId, signerMetaDataOne.weight + ) + ); + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 2)); + + signerMetaDataOne.weight = 1000001; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.expectRevert( + abi.encodeWithSelector( + InvalidSignerWeight.selector, multisigEntityId, address(msca), eoaSignerOneId, signerMetaDataOne.weight + ) + ); + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 2)); + } + + // totalWeight < thresholdWeight + function testInstallWithHighThresholdWeight() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.expectRevert(abi.encodeWithSelector(ThresholdWeightExceedsTotalWeight.selector, 2, 1)); + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 2)); + } + + function testInstallTwice() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 1)); + vm.expectRevert(abi.encodeWithSelector(AlreadyInitialized.selector, multisigEntityId, address(msca))); + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 1)); + } + + function testUninstallOnlyProvidedEntityId() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 2; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 2)); + // install another entity id + module.onInstall(abi.encode(uint32(1), signersMetadata, 2)); + // uninstall uint32(1) + module.onUninstall(abi.encode(uint32(1))); + + // verify the data for entityId(1) doesn't exist + SignerMetadataWithId[] memory signersMetadataRet = module.signersMetadataOf(uint32(1), address(msca)); + assertEq(signersMetadataRet.length, 0); + AccountMetadata memory accountMetadata = module.accountMetadataOf(uint32(1), address(msca)); + assertEq(accountMetadata.numSigners, 0); + assertEq(accountMetadata.thresholdWeight, 0); + assertEq(accountMetadata.totalWeight, 0); + + // verify the data for entityId(0) still exists + signersMetadataRet = module.signersMetadataOf(multisigEntityId, address(msca)); + assertEq(signersMetadataRet.length, 1); + assertEq(signersMetadataRet[0].signerMetadata.addr, signerMetaDataOne.addr); + assertEq(signersMetadataRet[0].signerMetadata.weight, signerMetaDataOne.weight); + accountMetadata = module.accountMetadataOf(multisigEntityId, address(msca)); + assertEq(accountMetadata.numSigners, 1); + assertEq(accountMetadata.thresholdWeight, 2); + assertEq(accountMetadata.totalWeight, 2); + } + + function testUninstallTwice() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 1)); + vm.prank(address(msca)); + module.onUninstall(abi.encode(multisigEntityId)); + vm.expectRevert(abi.encodeWithSelector(Uninitialized.selector, multisigEntityId, address(msca))); + vm.prank(address(msca)); + module.onUninstall(abi.encode(multisigEntityId)); + } + + // 1. deploy the MSCA with signer 1 with SingleSignerValidationModule that provides signature validation + // 2. we install WeightedMultisigValidationModule + // 3. uninstall WeightedMultisigValidationModule + function testInstallAndUninstallViaUserOp() public { + // deploy the account along with executeBatch call that installs the remaining validation functions + bytes memory installData = abi.encode(ecdsaSignerOneEntityIdForSSVM, eoaSignerTwoAddr); + bytes memory initializingData = abi.encode( + ValidationConfigLib.pack(ecdsaSignerOneValidationForSSVM, true, true, true), + new bytes4[](0), + installData, + new bytes[](0) + ); + (address sender,) = factory.getAddressWithValidation(addressToBytes32(address(this)), salt, initializingData); + vm.deal(sender, 1 ether); + bytes memory createAccountCall = abi.encodeCall( + UpgradableMSCAFactory.createAccountWithValidation, (addressToBytes32(address(this)), salt, initializingData) + ); + bytes memory initCode = abi.encodePacked(address(factory), createAccountCall); + // executeBatchCallData + Call[] memory calls = new Call[](1); + ValidationConfig multisigValidationConfig = ValidationConfigLib.pack(multisigValidation, true, true, true); + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 3; + signerMetaDataTwo.addr = eoaSignerTwoAddr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + calls[0] = Call( + sender, + 0, + abi.encodeCall( + IModularAccount.installValidation, + ( + multisigValidationConfig, + new bytes4[](0), + abi.encode(multisigEntityId, signersMetadata, 2), + new bytes[](0) + ) + ) + ); + bytes memory executeCallData = abi.encodeCall(IModularAccount.executeBatch, (calls)); + PackedUserOperation memory userOp = buildPartialUserOp( + sender, 0, vm.toString(initCode), vm.toString(executeCallData), 1000000, 1000000, 0, 1, 1, "0x" + ); + bytes32 userOpHash = entryPoint.getUserOpHash(userOp); + // signed by the signer two that deploys the account + bytes memory signature = signUserOpHash(entryPoint, vm, eoaSignerTwoPrivateKey, userOp); + userOp.signature = + encodeSignature(new PreValidationHookData[](0), ecdsaSignerOneValidationForSSVM, signature, true); + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + ops[0] = userOp; + vm.prank(address(entryPoint)); + vm.expectEmit(true, true, true, false); + emit UserOperationEvent(userOpHash, sender, address(0), 0, true, 685419, 685419); + entryPoint.handleOps(ops, beneficiary); + _verifyResultForTestInstallAndUninstallViaUserOp(sender); + // uninstall + _uninstallForTestInstallAndUninstallViaUserOp(sender, eoaSignerTwoPrivateKey, ecdsaSignerOneValidationForSSVM); + } + + function _verifyResultForTestInstallAndUninstallViaUserOp(address sender) internal view { + // verify the account has been deployed + assertTrue(sender.code.length > 0); + // verify signer 2 has been installed on SingleSignerValidationModule + assertEq(singleSignerValidationModule.signers(ecdsaSignerOneEntityIdForSSVM, sender), eoaSignerTwoAddr); + // verify WeightedMultisigValidationModule has been installed + SignerMetadataWithId[] memory signersMetadataRet = module.signersMetadataOf(multisigEntityId, sender); + assertEq(signersMetadataRet[0].signerMetadata.addr, eoaSignerOneAddr); + assertEq(signersMetadataRet[0].signerMetadata.weight, 1); + assertFalse(signersMetadataRet[0].signerMetadata.publicKey.isValidPublicKey()); + assertEq(signersMetadataRet[0].signerId, eoaSignerOneId); + + assertEq(signersMetadataRet[1].signerMetadata.addr, eoaSignerTwoAddr); + assertEq(signersMetadataRet[1].signerMetadata.weight, 3); + assertFalse(signersMetadataRet[1].signerMetadata.publicKey.isValidPublicKey()); + assertEq(signersMetadataRet[1].signerId, eoaSignerTwoId); + + AccountMetadata memory accountMetadata = module.accountMetadataOf(multisigEntityId, sender); + assertEq(accountMetadata.numSigners, 2); + assertEq(accountMetadata.thresholdWeight, 2); + assertEq(accountMetadata.totalWeight, 4); + } + + function _uninstallForTestInstallAndUninstallViaUserOp(address sender, uint256 key, ModuleEntity signerValidation) + internal + { + bytes memory uninstallData = abi.encode(multisigEntityId); + bytes memory uninstallValidationData = + abi.encodeCall(IModularAccount.uninstallValidation, (multisigValidation, uninstallData, new bytes[](0))); + PackedUserOperation memory userOp = buildPartialUserOp( + sender, + entryPoint.getNonce(sender, 0), + "0x", + vm.toString(uninstallValidationData), + 1000000, + 1000000, + 0, + 1, + 1, + "0x" + ); + // signed by the singer two that deploys the account + bytes memory signature = signUserOpHash(entryPoint, vm, key, userOp); + userOp.signature = encodeSignature(new PreValidationHookData[](0), signerValidation, signature, true); + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + ops[0] = userOp; + vm.prank(address(entryPoint)); + entryPoint.handleOps(ops, beneficiary); + + // verify module doesn't have any data after uninstall + SignerMetadataWithId[] memory signersMetadataRet = module.signersMetadataOf(multisigEntityId, address(msca)); + assertEq(signersMetadataRet.length, 0); + AccountMetadata memory accountMetadata = module.accountMetadataOf(multisigEntityId, address(msca)); + assertEq(accountMetadata.numSigners, 0); + assertEq(accountMetadata.thresholdWeight, 0); + assertEq(accountMetadata.totalWeight, 0); + } + + // fuzz test on numOfSigner, entityId and signersToDelete + // we first install an initial signer, + // then add new signers from fuzz input and verify the added signers and account metadata, + // then remove some of the added signers and verify the removed & remaining signers + function testFuzz_addAndRemoveSigners(AddAndRemoveSignersFuzzInput memory input) public { + // using MAX_SIGNERS (1000) would need significantly more time to run + input.numOfSigner = bound(input.numOfSigner, 1, 100); + input.signersToDelete = bound(input.signersToDelete, 1, input.numOfSigner); + // init with one signer + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(input.entityId, signersMetadata, 1)); + // add new signers + signersMetadata = new SignerMetadata[](input.numOfSigner); + uint256 newThresholdWeight = 0; + // skip deleting the initial signer + bytes30[] memory signerIdsDeleted = new bytes30[](input.signersToDelete); + bytes30[] memory signerIdsRemained = new bytes30[](input.numOfSigner - input.signersToDelete); + SignerMetadata[] memory signersMetadataRemained = + new SignerMetadata[](input.numOfSigner - input.signersToDelete); + console.log("num of signers: ", input.numOfSigner); + console.log("deleted signers: ", input.signersToDelete); + console.log("remaining signers: ", input.numOfSigner - input.signersToDelete); + // counter for deleted signers + uint256 c1; + // counter for remained signers + uint256 c2; + // exclude initial signer + for (uint256 i = 0; i < signersMetadata.length; ++i) { + if (i % 2 == 0) { + signersMetadata[i].weight = i + 1; + signersMetadata[i].addr = vm.addr(i + 1); + } else { + signersMetadata[i].weight = i + 1; + signersMetadata[i].publicKey = PublicKey({x: i + 1, y: i + 1}); + } + newThresholdWeight += signersMetadata[i].weight; + if (c1 < input.signersToDelete) { + signerIdsDeleted[c1++] = _getSignerId(signersMetadata[i]); + } else { + signerIdsRemained[c2] = _getSignerId(signersMetadata[i]); + signersMetadataRemained[c2] = signersMetadata[i]; + c2++; + } + } + vm.prank(address(msca)); + module.addSigners(input.entityId, signersMetadata, newThresholdWeight); + // verify the added signers and account metadata + _verifyAddSignersResultForTestFuzzAddAndRemoveSigners(input, newThresholdWeight); + vm.prank(address(msca)); + // remove the added signers + module.removeSigners(input.entityId, signerIdsDeleted, 1); // use initial signer's weight + _verifyRemoveSignersResultForTestFuzzAddAndRemoveSigners( + input, signerIdsDeleted, signerIdsRemained, signersMetadataRemained + ); + } + + function _getSignerId(SignerMetadata memory signerMetadata) internal view returns (bytes30) { + if (signerMetadata.addr != address(0)) { + return module.getSignerId(signerMetadata.addr); + } else { + return module.getSignerId(signerMetadata.publicKey); + } + } + + function _verifyAddSignersResultForTestFuzzAddAndRemoveSigners( + AddAndRemoveSignersFuzzInput memory input, + uint256 newThresholdWeight + ) internal view { + SignerMetadataWithId[] memory signersMetadataRet = module.signersMetadataOf(input.entityId, address(msca)); + // initial signer + assertEq(signersMetadataRet[0].signerMetadata.addr, eoaSignerOneAddr); + assertEq(signersMetadataRet[0].signerMetadata.weight, 1); + // exclude initial signer + for (uint256 i = 1; i < signersMetadataRet.length; ++i) { + if (signersMetadataRet[i].signerMetadata.addr != address(0)) { + assertEq(signersMetadataRet[i].signerMetadata.addr, vm.addr(i)); // need to offset by 1 compared to + // signersMetadata + assertEq(signersMetadataRet[i].signerMetadata.weight, i); + assertEq(signersMetadataRet[i].signerId, module.getSignerId(signersMetadataRet[i].signerMetadata.addr)); + } else { + assertEq(signersMetadataRet[i].signerMetadata.publicKey.x, i); + assertEq(signersMetadataRet[i].signerMetadata.publicKey.y, i); + assertEq(signersMetadataRet[i].signerMetadata.weight, i); + assertEq( + signersMetadataRet[i].signerId, module.getSignerId(signersMetadataRet[i].signerMetadata.publicKey) + ); + } + } + AccountMetadata memory accountMetadata = module.accountMetadataOf(input.entityId, address(msca)); + assertEq(accountMetadata.numSigners, input.numOfSigner + 1); // +1 for the initial signer + assertEq(accountMetadata.thresholdWeight, newThresholdWeight); + assertEq(accountMetadata.totalWeight, newThresholdWeight + 1); // +1 for the initial signer weight + } + + function _verifyRemoveSignersResultForTestFuzzAddAndRemoveSigners( + AddAndRemoveSignersFuzzInput memory input, + bytes30[] memory signerIdsDeleted, + bytes30[] memory signerIdsRemained, + SignerMetadata[] memory signersMetadataRemained + ) internal view { + SignerMetadataWithId[] memory signersMetadataRet = module.signersMetadataOf(input.entityId, address(msca)); + assertEq(signersMetadataRet.length, 1 + signerIdsRemained.length); // add the initial signer + uint256 totalRemainingWeight = 0; + { + // verify initial signer + (uint256 weight, address addr,) = + module.signersMetadataPerEntity(input.entityId, eoaSignerOneId, address(msca)); + assertEq(addr, eoaSignerOneAddr); + assertEq(weight, 1); + totalRemainingWeight += weight; + } + // verify the deleted signers are gone + for (uint256 i = 0; i < signerIdsDeleted.length; ++i) { + (uint256 weight, address addr, PublicKey memory publicKey) = + module.signersMetadataPerEntity(input.entityId, signerIdsDeleted[i], address(msca)); + assertEq(weight, 0); + assertEq(addr, address(0)); + assertEq(publicKey.x, 0); + assertEq(publicKey.y, 0); + } + // verify the remaining signers after deletion + for (uint256 i = 0; i < signerIdsRemained.length; ++i) { + (uint256 weight, address addr, PublicKey memory publicKey) = + module.signersMetadataPerEntity(input.entityId, signerIdsRemained[i], address(msca)); + assertEq(weight, signersMetadataRemained[i].weight); + totalRemainingWeight += weight; + if (addr != address(0)) { + assertEq(addr, signersMetadataRemained[i].addr); + } else { + assertEq(publicKey.x, signersMetadataRemained[i].publicKey.x); + assertEq(publicKey.y, signersMetadataRemained[i].publicKey.y); + } + } + AccountMetadata memory accountMetadata = module.accountMetadataOf(input.entityId, address(msca)); + assertEq(accountMetadata.numSigners, 1 + signersMetadataRemained.length); + assertEq(accountMetadata.thresholdWeight, 1); // from the initial signer + assertEq(accountMetadata.totalWeight, totalRemainingWeight); + } + + function testAddSignersToUninitializedAccount() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + vm.expectRevert(abi.encodeWithSelector(Uninitialized.selector, multisigEntityId, address(msca))); + module.addSigners(multisigEntityId, signersMetadata, 1); + } + + function testAddZeroSigners() public { + // init with one signer + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 1)); + + signersMetadata = new SignerMetadata[](0); + vm.prank(address(msca)); + vm.expectRevert(abi.encodeWithSelector(TooFewSigners.selector, 0)); + module.addSigners(multisigEntityId, signersMetadata, 1); + } + + function testAddTooManySigners() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 1)); + + signersMetadata = new SignerMetadata[](1001); + for (uint256 i = 0; i < 1001; i++) { + SignerMetadata memory signerMetaData; + signerMetaData.weight = 1; + signerMetaData.addr = vm.addr(i + 1); + signersMetadata[i] = signerMetaData; + } + vm.expectRevert(abi.encodeWithSelector(TooManySigners.selector, 1001)); + vm.prank(address(msca)); + module.addSigners(multisigEntityId, signersMetadata, 2); + } + + function testAddSignerWithBothAddrAndPubKeyPresent() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 1)); + + signersMetadata = new SignerMetadata[](1); + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + signerMetaDataOne.publicKey = pubKeySignerOne; + signersMetadata[0] = signerMetaDataOne; + vm.expectRevert( + abi.encodeWithSelector(InvalidSignerMetadata.selector, multisigEntityId, address(msca), signerMetaDataOne) + ); + vm.prank(address(msca)); + module.addSigners(multisigEntityId, signersMetadata, 2); + } + + function testAddSignersWithBothAddrAndPubKeyAbsent() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 1)); + + signersMetadata = new SignerMetadata[](1); + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = address(0); + signerMetaDataOne.publicKey = PublicKey({x: 0, y: 0}); + // no address or public key + signersMetadata[0] = signerMetaDataOne; + vm.expectRevert( + abi.encodeWithSelector(InvalidSignerMetadata.selector, multisigEntityId, address(msca), signerMetaDataOne) + ); + vm.prank(address(msca)); + module.addSigners(multisigEntityId, signersMetadata, 2); + } + + function testAddSignersWithDuplicatedAddr() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 1)); + + signersMetadata = new SignerMetadata[](2); + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataOne; + SignerMetadataWithId[] memory signersMetadataRet = new SignerMetadataWithId[](2); + signersMetadataRet[0].signerId = eoaSignerOneId; + signersMetadataRet[0].signerMetadata = signerMetaDataOne; + signersMetadataRet[1].signerId = eoaSignerOneId; + signersMetadataRet[1].signerMetadata = signerMetaDataOne; + vm.expectRevert( + abi.encodeWithSelector( + SignerMetadataAlreadyExists.selector, multisigEntityId, address(msca), signerMetaDataOne + ) + ); + vm.prank(address(msca)); + module.addSigners(multisigEntityId, signersMetadata, 2); + } + + function testAddSignersWithInvalidWeights() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 1)); + + signerMetaDataOne.weight = 0; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.expectRevert( + abi.encodeWithSelector( + InvalidSignerWeight.selector, multisigEntityId, address(msca), eoaSignerOneId, signerMetaDataOne.weight + ) + ); + vm.prank(address(msca)); + module.addSigners(multisigEntityId, signersMetadata, 2); + + signerMetaDataOne.weight = 1000001; + signerMetaDataOne.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaDataOne; + vm.expectRevert( + abi.encodeWithSelector( + InvalidSignerWeight.selector, multisigEntityId, address(msca), eoaSignerOneId, signerMetaDataOne.weight + ) + ); + vm.prank(address(msca)); + module.addSigners(multisigEntityId, signersMetadata, 2); + } + + // totalWeight < thresholdWeight + function testAddSignersWithHighThresholdWeight() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaData; + signerMetaData.weight = 1; + signerMetaData.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaData; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 1)); + + signersMetadata = new SignerMetadata[](1); + signerMetaData.weight = 1; + signerMetaData.addr = eoaSignerTwoAddr; + signersMetadata[0] = signerMetaData; + vm.expectRevert(abi.encodeWithSelector(ThresholdWeightExceedsTotalWeight.selector, 3, 2)); + vm.prank(address(msca)); + module.addSigners(multisigEntityId, signersMetadata, 3); + } + + function testAddSignersWithoutUpdatingThresholdWeight() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaData; + signerMetaData.weight = 1; + signerMetaData.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaData; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 1)); + + signersMetadata = new SignerMetadata[](1); + // new signer with weight 2 + signerMetaData.weight = 2; + signerMetaData.addr = eoaSignerTwoAddr; + signersMetadata[0] = signerMetaData; + SignerMetadataWithId[] memory signersMetadataRet = new SignerMetadataWithId[](1); + signersMetadataRet[0].signerId = eoaSignerTwoId; + signersMetadataRet[0].signerMetadata = signerMetaData; + vm.prank(address(msca)); + vm.expectEmit(true, true, true, true); + emit SignersAdded(address(msca), multisigEntityId, signersMetadataRet); + // do not update the threshold weight + module.addSigners(multisigEntityId, signersMetadata, 0); + AccountMetadata memory accountMetadata = module.accountMetadataOf(multisigEntityId, address(msca)); + assertEq(accountMetadata.thresholdWeight, 1); + } + + function testRemoveSignersFromUninitializedAccount() public { + bytes30[] memory signersToDelete = new bytes30[](1); + signersToDelete[0] = eoaSignerOneId; + vm.prank(address(msca)); + vm.expectRevert(abi.encodeWithSelector(Uninitialized.selector, multisigEntityId, address(msca))); + module.removeSigners(multisigEntityId, signersToDelete, 0); + } + + function testRemoveSignersWithRandomSignerId() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaData; + signerMetaData.weight = 1; + signerMetaData.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaData; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 1)); + + bytes30[] memory signerIdsToDelete = new bytes30[](1); + // random entityId 123 + signerIdsToDelete[0] = module.getSignerId(address(123)); + vm.prank(address(msca)); + vm.expectRevert( + abi.encodeWithSelector(SignerIdDoesNotExist.selector, multisigEntityId, address(msca), signerIdsToDelete[0]) + ); + module.removeSigners(multisigEntityId, signerIdsToDelete, 0); + } + + function testRemoveLastSigner() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaData; + signerMetaData.weight = 1; + signerMetaData.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaData; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 1)); + + bytes30[] memory signerIdsToDelete = new bytes30[](1); + signerIdsToDelete[0] = eoaSignerOneId; + vm.prank(address(msca)); + vm.expectRevert(abi.encodeWithSelector(TooFewSigners.selector, 0)); + module.removeSigners(multisigEntityId, signerIdsToDelete, 0); + } + + // totalWeight < thresholdWeight + function testRemoveSignersWithHighThresholdWeight() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 2; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = eoaSignerTwoAddr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 2)); + + bytes30[] memory signerIdsToDelete = new bytes30[](1); + signerIdsToDelete[0] = eoaSignerOneId; + vm.prank(address(msca)); + // removing 2, now totalWeight is 1 < thresholdWeight 2 + vm.expectRevert(abi.encodeWithSelector(ThresholdWeightExceedsTotalWeight.selector, 2, 1)); + module.removeSigners(multisigEntityId, signerIdsToDelete, 0); + } + + function testRemoveSignersWithoutUpdatingThresholdWeight() public { + SignerMetadata[] memory signersToRemove = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = eoaSignerTwoAddr; + signersToRemove[0] = signerMetaDataOne; + signersToRemove[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersToRemove, 1)); + + bytes30[] memory signerIdsToDelete = new bytes30[](1); + signerIdsToDelete[0] = eoaSignerOneId; + SignerMetadataWithId[] memory removedSigners = new SignerMetadataWithId[](1); + removedSigners[0].signerId = eoaSignerOneId; + removedSigners[0].signerMetadata = signerMetaDataOne; + vm.prank(address(msca)); + // removing 1, now totalWeight is 1 == thresholdWeight 1 + // no update to threshold weight + vm.expectEmit(true, true, true, true); + emit SignersRemoved(address(msca), multisigEntityId, removedSigners); + module.removeSigners(multisigEntityId, signerIdsToDelete, 0); + AccountMetadata memory accountMetadata = module.accountMetadataOf(multisigEntityId, address(msca)); + assertEq(accountMetadata.numSigners, 1); + assertEq(accountMetadata.thresholdWeight, 1); // still 1 + assertEq(accountMetadata.totalWeight, 1); + } + + function testRemoveSignersWithSignerIds() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaData; + signerMetaData.weight = 1; + signerMetaData.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaData; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 1)); + vm.prank(address(msca)); + vm.expectRevert(abi.encodeWithSelector(TooFewSigners.selector, 0)); + // zero signers + module.removeSigners(multisigEntityId, new bytes30[](0), 0); + } + + function testUpdateWeightsWithUninitializedAccount() public { + SignerMetadataWithId[] memory signersToUpdate = new SignerMetadataWithId[](1); + SignerMetadata memory signerMetaData; + signerMetaData.weight = 1; + signersToUpdate[0].signerMetadata = signerMetaData; + SignerMetadataWithId[] memory updatedSigners = new SignerMetadataWithId[](1); + updatedSigners[0].signerId = eoaSignerOneId; + updatedSigners[0].signerMetadata = signerMetaData; + vm.prank(address(msca)); + vm.expectRevert(abi.encodeWithSelector(Uninitialized.selector, multisigEntityId, address(msca))); + module.updateWeights(multisigEntityId, signersToUpdate, 0); + } + + function testUpdateWeightsWithNothingToUpdate() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaData; + signerMetaData.weight = 1; + signerMetaData.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaData; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 1)); + + SignerMetadataWithId[] memory signersToUpdate = new SignerMetadataWithId[](0); + vm.prank(address(msca)); + vm.expectRevert( + abi.encodeWithSelector(EmptyThresholdWeightAndSigners.selector, multisigEntityId, address(msca)) + ); + module.updateWeights(multisigEntityId, signersToUpdate, 0); + } + + function testUpdateWeightsWithInvalidWeights() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaData; + signerMetaData.weight = 1; + signerMetaData.addr = eoaSignerOneAddr; + signersMetadata[0] = signerMetaData; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 1)); + + SignerMetadataWithId[] memory signersToUpdate = new SignerMetadataWithId[](1); + signersToUpdate[0].signerId = eoaSignerOneId; + signersToUpdate[0].signerMetadata.weight = 0; + // only need id & weight, setting addr just to verify logs + signersToUpdate[0].signerMetadata.addr = signersMetadata[0].addr; + vm.prank(address(msca)); + vm.expectRevert( + abi.encodeWithSelector( + InvalidSignerWeight.selector, + multisigEntityId, + address(msca), + signersToUpdate[0].signerId, + signersToUpdate[0].signerMetadata.weight + ) + ); + module.updateWeights(multisigEntityId, signersToUpdate, 0); + + signersToUpdate[0].signerMetadata.weight = 1000001; + vm.prank(address(msca)); + vm.expectRevert( + abi.encodeWithSelector( + InvalidSignerWeight.selector, + multisigEntityId, + address(msca), + signersToUpdate[0].signerId, + signersToUpdate[0].signerMetadata.weight + ) + ); + module.updateWeights(multisigEntityId, signersToUpdate, 0); + } + + function testUpdateWeightsWithOnlySignerWeights() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 2; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 2; + signerMetaDataTwo.addr = eoaSignerTwoAddr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 2)); + + SignerMetadataWithId[] memory signersToUpdate = new SignerMetadataWithId[](2); + signersToUpdate[0].signerMetadata = signerMetaDataOne; + signersToUpdate[1].signerMetadata = signerMetaDataTwo; + signersToUpdate[0].signerId = eoaSignerOneId; + signersToUpdate[1].signerId = eoaSignerTwoId; + // one decrease, one increase, delta is 1 + // 2 -> 1 + signersToUpdate[0].signerMetadata.weight = 1; + // 2 -> 4 + signersToUpdate[1].signerMetadata.weight = 4; + vm.expectEmit(true, true, true, true); + emit SignersUpdated(address(msca), multisigEntityId, signersToUpdate); + vm.expectEmit(true, true, true, true); + emit AccountMetadataUpdated(address(msca), multisigEntityId, AccountMetadata(2, 2, 4), AccountMetadata(2, 2, 5)); + // no modification to threshold weight + vm.prank(address(msca)); + module.updateWeights(multisigEntityId, signersToUpdate, 0); + // verify account metadata + AccountMetadata memory accountMetadata = module.accountMetadataOf(multisigEntityId, address(msca)); + assertEq(accountMetadata.numSigners, 2); + assertEq(accountMetadata.thresholdWeight, 2); + assertEq(accountMetadata.totalWeight, 5); + + // one increase, one decrease, delta is 0 + // 1 -> 2 + signersToUpdate[0].signerMetadata.weight = 2; + // 4 -> 3 + signersToUpdate[1].signerMetadata.weight = 3; + vm.expectEmit(true, true, true, true); + emit SignersUpdated(address(msca), multisigEntityId, signersToUpdate); + vm.expectEmit(true, true, true, true); + emit AccountMetadataUpdated(address(msca), multisigEntityId, AccountMetadata(2, 2, 5), AccountMetadata(2, 2, 5)); + // no modification to threshold weight + vm.prank(address(msca)); + module.updateWeights(multisigEntityId, signersToUpdate, 0); + // verify account metadata + accountMetadata = module.accountMetadataOf(multisigEntityId, address(msca)); + assertEq(accountMetadata.numSigners, 2); + assertEq(accountMetadata.thresholdWeight, 2); + assertEq(accountMetadata.totalWeight, 5); + + // both decrease, delta is -3 + // 2 -> 1 + signersToUpdate[0].signerMetadata.weight = 1; + // 3 -> 1 + signersToUpdate[1].signerMetadata.weight = 1; + vm.expectEmit(true, true, true, true); + emit SignersUpdated(address(msca), multisigEntityId, signersToUpdate); + vm.expectEmit(true, true, true, true); + emit AccountMetadataUpdated(address(msca), multisigEntityId, AccountMetadata(2, 2, 5), AccountMetadata(2, 2, 2)); + // no modification to threshold weight + vm.prank(address(msca)); + module.updateWeights(multisigEntityId, signersToUpdate, 0); + // verify account metadata + accountMetadata = module.accountMetadataOf(multisigEntityId, address(msca)); + assertEq(accountMetadata.numSigners, 2); + assertEq(accountMetadata.thresholdWeight, 2); + assertEq(accountMetadata.totalWeight, 2); + + // both increase, delta is 2 + // 1 -> 2 + signersToUpdate[0].signerMetadata.weight = 2; + // 1 -> 2 + signersToUpdate[1].signerMetadata.weight = 2; + vm.expectEmit(true, true, true, true); + emit SignersUpdated(address(msca), multisigEntityId, signersToUpdate); + vm.expectEmit(true, true, true, true); + emit AccountMetadataUpdated(address(msca), multisigEntityId, AccountMetadata(2, 2, 2), AccountMetadata(2, 2, 4)); + // no modification to threshold weight + vm.prank(address(msca)); + module.updateWeights(multisigEntityId, signersToUpdate, 0); + // verify account metadata + accountMetadata = module.accountMetadataOf(multisigEntityId, address(msca)); + assertEq(accountMetadata.numSigners, 2); + assertEq(accountMetadata.thresholdWeight, 2); + assertEq(accountMetadata.totalWeight, 4); + } + + function testUpdateWeightsWithOnlyThresholdWeight() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 2; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 2; + signerMetaDataTwo.addr = eoaSignerTwoAddr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 2)); + + SignerMetadataWithId[] memory signersToUpdate = new SignerMetadataWithId[](0); + vm.expectEmit(true, true, true, true); + emit AccountMetadataUpdated(address(msca), multisigEntityId, AccountMetadata(2, 2, 4), AccountMetadata(2, 3, 4)); + vm.prank(address(msca)); + module.updateWeights(multisigEntityId, signersToUpdate, 3); + // verify account metadata + AccountMetadata memory accountMetadata = module.accountMetadataOf(multisigEntityId, address(msca)); + assertEq(accountMetadata.numSigners, 2); + assertEq(accountMetadata.thresholdWeight, 3); + assertEq(accountMetadata.totalWeight, 4); + } + + function testUpdateWeightsWithBothSignerAndThresholdWeights() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 2; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 2; + signerMetaDataTwo.addr = eoaSignerTwoAddr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 2)); + + SignerMetadataWithId[] memory signersToUpdate = new SignerMetadataWithId[](2); + signersToUpdate[0].signerMetadata = signerMetaDataOne; + signersToUpdate[1].signerMetadata = signerMetaDataTwo; + signersToUpdate[0].signerId = eoaSignerOneId; + signersToUpdate[1].signerId = eoaSignerTwoId; + // decrease from 2 + signersToUpdate[0].signerMetadata.weight = 1; + // increase from 2 + signersToUpdate[1].signerMetadata.weight = 3; + vm.expectEmit(true, true, true, true); + emit SignersUpdated(address(msca), multisigEntityId, signersToUpdate); + vm.expectEmit(true, true, true, true); + emit AccountMetadataUpdated(address(msca), multisigEntityId, AccountMetadata(2, 2, 4), AccountMetadata(2, 3, 4)); + // no modification to threshold weight + vm.prank(address(msca)); + module.updateWeights(multisigEntityId, signersToUpdate, 3); + // verify account metadata + AccountMetadata memory accountMetadata = module.accountMetadataOf(multisigEntityId, address(msca)); + assertEq(accountMetadata.numSigners, 2); + assertEq(accountMetadata.thresholdWeight, 3); + assertEq(accountMetadata.totalWeight, 4); + } + + function testUpdateWeightsWithTooHighThresholdWeight() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 2; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 2; + signerMetaDataTwo.addr = eoaSignerTwoAddr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, 2)); + + SignerMetadataWithId[] memory signersToUpdate = new SignerMetadataWithId[](0); + vm.prank(address(msca)); + vm.expectRevert(abi.encodeWithSelector(ThresholdWeightExceedsTotalWeight.selector, 10, 4)); + module.updateWeights(multisigEntityId, signersToUpdate, 10); + // verify account metadata is not changed + AccountMetadata memory accountMetadata = module.accountMetadataOf(multisigEntityId, address(msca)); + assertEq(accountMetadata.numSigners, 2); + assertEq(accountMetadata.thresholdWeight, 2); + assertEq(accountMetadata.totalWeight, 4); + } + + function testValidateSignatureLengthTooShort() public { + bytes32 digest = bytes32(0); + bytes memory sig = bytes("foo"); + vm.expectRevert(abi.encodeWithSelector(InvalidSigLength.selector, multisigEntityId, address(msca), sig.length)); + vm.prank(address(msca)); + module.validateSignature(address(msca), multisigEntityId, address(this), digest, sig); + } + + function testValidateSignatureUninitializedAccount() public { + bytes32 digest = bytes32(0); + bytes memory sig = bytes("0x0000000000000000000000000000000000000000000000000000000000000000"); + vm.expectRevert(abi.encodeWithSelector(Uninitialized.selector, multisigEntityId, address(msca))); + vm.prank(address(msca)); + module.validateSignature(address(msca), multisigEntityId, address(this), digest, sig); + } + + // 2nd signer has too short signature + function testCheckNSignaturesSigConstantPartTooShort() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = eoaSignerTwoAddr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + // create a valid signature for 1st installed signer + bytes32 digest = module.getReplaySafeMessageHash(address(msca), bytes32(0)); + bytes memory sig = signMessage(vm, eoaSignerOnePrivateKey, digest); + assertEq(sig.length, 65); + + // append <65 bytes of data for 2nd installed signer + bytes memory fooBytes = bytes("foo"); + bytes memory sigWithFooAppended = abi.encodePacked(sig, fooBytes); + assertEq(sigWithFooAppended.length, 68); + + vm.prank(address(msca)); + CheckNSignaturesRequest memory request = CheckNSignaturesRequest({ + entityId: multisigEntityId, + actualDigest: digest, + minimalDigest: digest, + requiredNumSigsOnActualDigest: 0, + account: address(msca), + signatures: sigWithFooAppended + }); + (bool success, uint256 firstFailure) = module.checkNSignatures(request); + assertEq(success, false); + assertEq(firstFailure, 1); + } + + function testCheckNSignaturesWithExactlyOneSignerOnActualDigest() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = eoaSignerTwoAddr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + // sign actual actualDigest + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), bytes32(uint256(1))); + bytes32 minimalDigest = module.getReplaySafeMessageHash(address(msca), bytes32(0)); + bytes memory sig = _signEOASig(eoaSignerOne, actualDigest, minimalDigest, eoaSignerOneId); + assertEq(sig.length, 65); + + bytes memory sig2 = _signEOASig(eoaSignerTwo, actualDigest, minimalDigest, eoaSignerOneId); + assertEq(sig2.length, 65); + // signer 1: 0xad3a4ceb930ec5721dd69ceedf111fd7af523ad67c5e1dbd0f6d12cfb611 + // signer 2: 0x0e634ce59dea96d6c8a2d23a25368a67f9e790b49fcc9b838bfefb4c2b30 + sig = abi.encodePacked(sig2, sig); + assertEq(sig.length, 130); + + vm.prank(address(msca)); + CheckNSignaturesRequest memory request = CheckNSignaturesRequest({ + entityId: multisigEntityId, + actualDigest: actualDigest, + minimalDigest: minimalDigest, + requiredNumSigsOnActualDigest: 1, + account: address(msca), + signatures: sig + }); + (bool success, uint256 firstFailure) = module.checkNSignatures(request); + assertEq(success, true); + assertEq(firstFailure, 0); + } + + function testCheckNSignaturesSignersOutOfOrder() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = eoaSignerTwoAddr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + // sign actual actualDigest + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), bytes32(uint256(1))); + bytes32 minimumDigest = module.getReplaySafeMessageHash(address(msca), bytes32(0)); + bytes memory sig = _signEOASig(eoaSignerOne, actualDigest, minimumDigest, eoaSignerOneId); + assertEq(sig.length, 65); + + bytes memory sig2 = _signEOASig(eoaSignerTwo, actualDigest, minimumDigest, eoaSignerOneId); + assertEq(sig2.length, 65); + // signer 1: 0xad3a4ceb930ec5721dd69ceedf111fd7af523ad67c5e1dbd0f6d12cfb611 + // signer 2: 0x0e634ce59dea96d6c8a2d23a25368a67f9e790b49fcc9b838bfefb4c2b30 + // out of order + sig = abi.encodePacked(sig, sig2); + assertEq(sig.length, 130); + + vm.prank(address(msca)); + CheckNSignaturesRequest memory request = CheckNSignaturesRequest({ + entityId: multisigEntityId, + actualDigest: actualDigest, + minimalDigest: minimumDigest, + requiredNumSigsOnActualDigest: 1, + account: address(msca), + signatures: sig + }); + (bool success, uint256 firstFailure) = module.checkNSignatures(request); + assertEq(success, false); + assertEq(firstFailure, 1); + } + + // we only require 1 signature on actualDigest, but we have 2 + function testCheckNSignaturesMoreThanOneSignersOnActualDigest() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = eoaSignerTwoAddr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + // sign actual actualDigest + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), bytes32(uint256(1))); + bytes memory sig = _signEOASig(eoaSignerOne, actualDigest, actualDigest, eoaSignerOne.signerId); + assertEq(sig.length, 65); + + bytes memory sig2 = _signEOASig(eoaSignerTwo, actualDigest, actualDigest, eoaSignerTwo.signerId); + assertEq(sig2.length, 65); + sig = abi.encodePacked(sig, sig2); + assertEq(sig.length, 130); + + vm.prank(address(msca)); + vm.expectRevert( + abi.encodeWithSelector(InvalidNumSigsOnActualDigest.selector, multisigEntityId, address(msca), 1 - 2) + ); + CheckNSignaturesRequest memory request = CheckNSignaturesRequest({ + entityId: multisigEntityId, + actualDigest: actualDigest, + minimalDigest: actualDigest, + requiredNumSigsOnActualDigest: 1, + account: address(msca), + signatures: sig + }); + module.checkNSignatures(request); + } + + // positive + function testFuzz_validateSignatureEOASigner(bytes32 hash) public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = eoaSignerTwoAddr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + // sign actual actualDigest + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), hash); + Signer[] memory signers = new Signer[](2); + signers[0] = eoaSignerOne; + signers[1] = eoaSignerTwo; + _sortSignersById(signers); + bytes memory sig = _signSigs( + MultisigInput({actualSigners: 2, totalSigners: 2, sigDynamicPartOffset: 0}), + signers, + actualDigest, + actualDigest + ); + + vm.prank(address(msca)); + assertEq( + EIP1271_VALID_SIGNATURE, module.validateSignature(address(msca), multisigEntityId, address(this), hash, sig) + ); + } + + // negative + function testFuzz_validateSignatureWrongEOASigner(bytes32 hash) public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = eoaSignerOneAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = eoaSignerTwoAddr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + // sign actual actualDigest + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), hash); + Signer[] memory signers = new Signer[](2); + signers[0] = _createEOASigner("randomSigner"); + signers[1] = eoaSignerTwo; + _sortSignersById(signers); + bytes memory sig = _signSigs( + MultisigInput({actualSigners: 2, totalSigners: 2, sigDynamicPartOffset: 0}), + signers, + actualDigest, + actualDigest + ); + + vm.prank(address(msca)); + assertEq( + EIP1271_INVALID_SIGNATURE, + module.validateSignature(address(msca), multisigEntityId, address(this), hash, sig) + ); + } + + function testCheckNSignaturesContractSigUpperBitsNotClean() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = contractSignerOne.contractAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight)); + + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), bytes32(uint256(1))); + (uint8 v, bytes memory sigDynamicParts) = + _signContractSig(contractSignerOne, actualDigest, actualDigest, contractSignerOne.signerId); + bytes32 dirtyUpperBits = + bytes32(uint256(uint160(contractSignerOne.contractAddr))) | bytes32(uint256(0xFF << 160)); // dirty upper bits + bytes memory sig = abi.encodePacked(dirtyUpperBits, uint256(65), v, sigDynamicParts); + assertEq(sig.length, 162); // 32 + 32 + 1 + 32 + 65 + + vm.prank(address(msca)); + CheckNSignaturesRequest memory request = CheckNSignaturesRequest({ + entityId: multisigEntityId, + actualDigest: actualDigest, + minimalDigest: actualDigest, + requiredNumSigsOnActualDigest: 1, + account: address(msca), + signatures: sig + }); + vm.expectRevert( + abi.encodeWithSelector( + InvalidAddress.selector, multisigEntityId, address(msca), contractSignerOne.contractAddr + ) + ); + module.checkNSignatures(request); + } + + function testCheckNSignaturesWrongContractAddress() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = contractSignerOne.contractAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight)); + + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), bytes32(uint256(1))); + (uint8 v, bytes memory sigDynamicParts) = + _signContractSig(contractSignerOne, actualDigest, actualDigest, contractSignerOne.signerId); + bytes32 wrongContract = bytes32(uint256(1)); + bytes memory sig = abi.encodePacked(wrongContract, uint256(65), v, sigDynamicParts); + assertEq(sig.length, 162); // 32 + 32 + 1 + 32 + 65 + + vm.prank(address(msca)); + CheckNSignaturesRequest memory request = CheckNSignaturesRequest({ + entityId: multisigEntityId, + actualDigest: actualDigest, + minimalDigest: actualDigest, + requiredNumSigsOnActualDigest: 1, + account: address(msca), + signatures: sig + }); + (bool success, uint256 firstFailure) = module.checkNSignatures(request); + assertEq(success, false); + assertEq(firstFailure, 0); + } + + function testCheckNSignaturesInvalidSigOffset() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = contractSignerOne.contractAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight)); + + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), bytes32(uint256(1))); + (uint8 v, bytes memory sigDynamicParts) = + _signContractSig(contractSignerOne, actualDigest, actualDigest, contractSignerOne.signerId); + uint256 wrongOffset = 1000; // offset > length + bytes memory sig = abi.encodePacked(abi.encode(contractSignerOne.contractAddr), wrongOffset, v, sigDynamicParts); + assertEq(sig.length, 162); // 32 + 32 + 1 + 32 + 65 + + vm.prank(address(msca)); + CheckNSignaturesRequest memory request = CheckNSignaturesRequest({ + entityId: multisigEntityId, + actualDigest: actualDigest, + minimalDigest: actualDigest, + requiredNumSigsOnActualDigest: 1, + account: address(msca), + signatures: sig + }); + vm.expectRevert(abi.encodeWithSelector(InvalidSigOffset.selector, multisigEntityId, address(msca), wrongOffset)); + module.checkNSignatures(request); + + wrongOffset = 0; // offset > length + sig = abi.encodePacked(abi.encode(contractSignerOne.contractAddr), wrongOffset, v, sigDynamicParts); + + vm.prank(address(msca)); + request = CheckNSignaturesRequest({ + entityId: multisigEntityId, + actualDigest: actualDigest, + minimalDigest: actualDigest, + requiredNumSigsOnActualDigest: 1, + account: address(msca), + signatures: sig + }); + vm.expectRevert(abi.encodeWithSelector(InvalidSigOffset.selector, multisigEntityId, address(msca), wrongOffset)); + module.checkNSignatures(request); + } + + function testCheckNSignaturesInvalidSigLength() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = contractSignerOne.contractAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight)); + + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), bytes32(uint256(1))); + uint256 wrongSigLength = 160; // sigDynamicParts offset + length > total length + + bytes32 r; + bytes32 s; + uint8 v; + (v, r, s) = vm.sign(contractSignerOne.signerWallet.privateKey, actualDigest); + bytes memory sigDynamicParts = abi.encodePacked(wrongSigLength, r, s, v); + v = 32; // 0 + 32 + + bytes memory sig = abi.encodePacked(abi.encode(contractSignerOne.contractAddr), uint256(65), v, sigDynamicParts); + assertEq(sig.length, 162); // 32 + 32 + 1 + 32 + 65 + + vm.prank(address(msca)); + CheckNSignaturesRequest memory request = CheckNSignaturesRequest({ + entityId: multisigEntityId, + actualDigest: actualDigest, + minimalDigest: actualDigest, + requiredNumSigsOnActualDigest: 1, + account: address(msca), + signatures: sig + }); + // sigDynamicPartOffset: 65 + // sigDynamicPartTotalLen: 160 + 32 + vm.expectRevert( + abi.encodeWithSelector(InvalidSigLength.selector, multisigEntityId, address(msca), 65 + wrongSigLength + 32) + ); + module.checkNSignatures(request); + } + + function testCheckNSignaturesExactlyOneSignerOnActualDigest() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = contractSignerOne.contractAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight)); + + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), bytes32(uint256(1))); + (uint8 v, bytes memory sigDynamicParts) = + _signContractSig(contractSignerOne, actualDigest, actualDigest, contractSignerOne.signerId); + bytes memory sig = abi.encodePacked(abi.encode(contractSignerOne.contractAddr), uint256(65), v, sigDynamicParts); + assertEq(sig.length, 162); // 32 + 32 + 1 + 32 + 65 + + vm.prank(address(msca)); + CheckNSignaturesRequest memory request = CheckNSignaturesRequest({ + entityId: multisigEntityId, + actualDigest: actualDigest, + minimalDigest: actualDigest, + requiredNumSigsOnActualDigest: 1, + account: address(msca), + signatures: sig + }); + (bool success, uint256 firstFailure) = module.checkNSignatures(request); + assertEq(success, true); + assertEq(firstFailure, 0); + } + + function testCheckNSignaturesTwoContractSigners() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = contractSignerOne.contractAddr; + signersMetadata[0] = signerMetaDataOne; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = contractSignerTwo.contractAddr; + signersMetadata[1] = signerMetaDataTwo; + + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), bytes32(uint256(1))); + bytes32 minimalDigest = module.getReplaySafeMessageHash(address(msca), bytes32(0)); + Signer[] memory signers = new Signer[](2); + signers[0] = contractSignerOne; + signers[1] = contractSignerTwo; + _sortSignersById(signers); + bytes memory sig = _signSigs( + MultisigInput({actualSigners: 2, totalSigners: 2, sigDynamicPartOffset: 0}), + signers, + actualDigest, + minimalDigest + ); + assertEq(sig.length, 324); // (32 + 32 + 1 + 32 + 65) * 2 + + vm.prank(address(msca)); + CheckNSignaturesRequest memory request = CheckNSignaturesRequest({ + entityId: multisigEntityId, + actualDigest: actualDigest, + minimalDigest: minimalDigest, + requiredNumSigsOnActualDigest: 1, + account: address(msca), + signatures: sig + }); + (bool success, uint256 firstFailure) = module.checkNSignatures(request); + assertEq(success, true); + assertEq(firstFailure, 0); + } + + // signer 1 puts its signature dynamic part after signer 2 + // recommended encoding would be constant part 1, constant part 2, dynamic part 1, dynamic part 2 + // but constant part 1, constant part 2, dynamic part 2, dynamic part 1 would also work because + // signature dynamic part is essentially indexed by the offset + function testCheckNSignaturesSwappedSigDynamicPartsIndexedByOffset() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = contractSignerOne.contractAddr; + signersMetadata[0] = signerMetaDataOne; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = contractSignerTwo.contractAddr; + signersMetadata[1] = signerMetaDataTwo; + + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), bytes32(uint256(1))); + bytes32 minimalDigest = module.getReplaySafeMessageHash(address(msca), bytes32(0)); + Signer[] memory signers = new Signer[](2); + signers[0] = contractSignerOne; + signers[1] = contractSignerTwo; + _sortSignersById(signers); + + MultisigInput memory input = MultisigInput({actualSigners: 2, totalSigners: 2, sigDynamicPartOffset: 0}); + input.sigDynamicPartOffset = 130; // two constant parts + // must be this order due to input.sigDynamicPartOffset += 97 + (bytes memory individualSigConstantPart2, bytes memory individualSigDynamicPart2) = + _signIndividualSig(input, signers[1], actualDigest, minimalDigest, contractSignerOne.signerId); + (bytes memory individualSigConstantPart1, bytes memory individualSigDynamicPart1) = + _signIndividualSig(input, signers[0], actualDigest, minimalDigest, contractSignerOne.signerId); + + // constant part 1, constant part 2, dynamic part 2, dynamic part 1 + bytes memory sig = abi.encodePacked( + individualSigConstantPart1, individualSigConstantPart2, individualSigDynamicPart2, individualSigDynamicPart1 + ); + assertEq(sig.length, 324); // (32 + 32 + 1 + 32 + 65) * 2 + + vm.prank(address(msca)); + CheckNSignaturesRequest memory request = CheckNSignaturesRequest({ + entityId: multisigEntityId, + actualDigest: actualDigest, + minimalDigest: minimalDigest, + requiredNumSigsOnActualDigest: 1, + account: address(msca), + signatures: sig + }); + (bool success, uint256 firstFailure) = module.checkNSignatures(request); + assertEq(success, true); + assertEq(firstFailure, 0); + } + + function testCheckNSignaturesWrongContractSig() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = contractSignerOne.contractAddr; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight)); + + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), bytes32(uint256(1))); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(contractSignerOne.signerWallet.privateKey, actualDigest); + // swap in an invalid s + s = bytes32(0); + bytes memory sigDynamicParts = abi.encodePacked(uint256(65), r, s, v); + v = 32; // 0 + 32 + bytes memory sig = abi.encodePacked(abi.encode(contractSignerOne.contractAddr), uint256(65), v, sigDynamicParts); + + vm.prank(address(msca)); + CheckNSignaturesRequest memory request = CheckNSignaturesRequest({ + entityId: multisigEntityId, + actualDigest: actualDigest, + minimalDigest: actualDigest, + requiredNumSigsOnActualDigest: 1, + account: address(msca), + signatures: sig + }); + (bool success, uint256 firstFailure) = module.checkNSignatures(request); + assertEq(success, false); + assertEq(firstFailure, 0); + } + + // positive + function testFuzz_validateSignatureContractSigner(bytes32 hash) public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = contractSignerOne.contractAddr; + signersMetadata[0] = signerMetaDataOne; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = contractSignerTwo.contractAddr; + signersMetadata[1] = signerMetaDataTwo; + + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), hash); + Signer[] memory signers = new Signer[](2); + signers[0] = contractSignerOne; + signers[1] = contractSignerTwo; + _sortSignersById(signers); + bytes memory sig = _signSigs( + MultisigInput({actualSigners: 2, totalSigners: 2, sigDynamicPartOffset: 0}), + signers, + actualDigest, + actualDigest + ); + + vm.prank(address(msca)); + assertEq( + EIP1271_VALID_SIGNATURE, module.validateSignature(address(msca), multisigEntityId, address(this), hash, sig) + ); + } + + // negative + function testFuzz_validateSignatureWrongContractSigner(bytes32 hash) public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = contractSignerOne.contractAddr; + signersMetadata[0] = signerMetaDataOne; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = contractSignerTwo.contractAddr; + signersMetadata[1] = signerMetaDataTwo; + + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), hash); + Signer[] memory signers = new Signer[](2); + signers[0] = _createContractSigner("randomSigner"); + signers[1] = contractSignerTwo; + _sortSignersById(signers); + bytes memory sig = _signSigs( + MultisigInput({actualSigners: 2, totalSigners: 2, sigDynamicPartOffset: 0}), + signers, + actualDigest, + actualDigest + ); + + vm.prank(address(msca)); + assertEq( + EIP1271_INVALID_SIGNATURE, + module.validateSignature(address(msca), multisigEntityId, address(this), hash, sig) + ); + } + + function testCheckNSignaturesWrongPasskeySig() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.publicKey = passKeySignerOnePublicKey; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight)); + + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), bytes32(uint256(1))); + WebAuthnSigDynamicPart memory webAuthnSigDynamicPartForFullDigest; + webAuthnSigDynamicPartForFullDigest.webAuthnData = _getWebAuthnData(actualDigest); + bytes32 webauthnFullDigest = _getWebAuthnMessageHash(webAuthnSigDynamicPartForFullDigest.webAuthnData); + + (webAuthnSigDynamicPartForFullDigest.r, webAuthnSigDynamicPartForFullDigest.s) = + signP256Message(vm, passKeySignerOne.signerWallet.privateKey, webauthnFullDigest); + uint8 v = 34; // 2 + 32 + // swap in an invalid s + webAuthnSigDynamicPartForFullDigest.s = 0; + bytes memory sigBytes = abi.encode(webAuthnSigDynamicPartForFullDigest); + bytes memory sigDynamicParts = abi.encodePacked(uint256(sigBytes.length), sigBytes); + + bytes32 pubKeyId = bytes32(bytes.concat(bytes2(0), passKeySignerOne.signerId)); + bytes memory sigConstantPart = abi.encodePacked(pubKeyId, uint256(65), v); + bytes memory sig = abi.encodePacked(sigConstantPart, sigDynamicParts); + + vm.prank(address(msca)); + CheckNSignaturesRequest memory request = CheckNSignaturesRequest({ + entityId: multisigEntityId, + actualDigest: actualDigest, + minimalDigest: actualDigest, + requiredNumSigsOnActualDigest: 1, + account: address(msca), + signatures: sig + }); + (bool success, uint256 firstFailure) = module.checkNSignatures(request); + assertEq(success, false); + assertEq(firstFailure, 0); + } + + function testCheckNSignaturesPasskeySigner() public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](1); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.publicKey = passKeySignerOnePublicKey; + signersMetadata[0] = signerMetaDataOne; + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight)); + + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), bytes32(uint256(1))); + bytes32 minimalDigest = module.getReplaySafeMessageHash(address(msca), bytes32(0)); + Signer[] memory signers = new Signer[](1); + signers[0] = passKeySignerOne; + _sortSignersById(signers); + bytes memory sig = _signSigs( + MultisigInput({actualSigners: 1, totalSigners: 1, sigDynamicPartOffset: 0}), + signers, + actualDigest, + minimalDigest + ); + + vm.prank(address(msca)); + CheckNSignaturesRequest memory request = CheckNSignaturesRequest({ + entityId: multisigEntityId, + actualDigest: actualDigest, + minimalDigest: minimalDigest, + requiredNumSigsOnActualDigest: 1, + account: address(msca), + signatures: sig + }); + (bool success, uint256 firstFailure) = module.checkNSignatures(request); + assertEq(success, true); + assertEq(firstFailure, 0); + } + + // positive + function testFuzz_validateSignaturePasskeySigner(bytes32 hash) public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.publicKey = passKeySignerOnePublicKey; + signersMetadata[0] = signerMetaDataOne; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.publicKey = passKeySignerTwoPublicKey; + signersMetadata[1] = signerMetaDataTwo; + + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), hash); + Signer[] memory signers = new Signer[](2); + signers[0] = passKeySignerOne; + signers[1] = passKeySignerTwo; + _sortSignersById(signers); + bytes memory sig = _signSigs( + MultisigInput({actualSigners: 2, totalSigners: 2, sigDynamicPartOffset: 0}), + signers, + actualDigest, + actualDigest + ); + + vm.prank(address(msca)); + assertEq( + EIP1271_VALID_SIGNATURE, module.validateSignature(address(msca), multisigEntityId, address(this), hash, sig) + ); + } + + // negative + function testFuzz_validateSignatureWrongPasskeySigner(bytes32 hash) public { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.publicKey = passKeySignerOnePublicKey; + signersMetadata[0] = signerMetaDataOne; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.publicKey = passKeySignerTwoPublicKey; + signersMetadata[1] = signerMetaDataTwo; + + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + bytes32 actualDigest = module.getReplaySafeMessageHash(address(msca), hash); + Signer[] memory signers = new Signer[](2); + signers[0] = passKeySignerOne; + signers[1] = _createPasskeySigner(2); + _sortSignersById(signers); + bytes memory sig = _signSigs( + MultisigInput({actualSigners: 2, totalSigners: 2, sigDynamicPartOffset: 0}), + signers, + actualDigest, + actualDigest + ); + + vm.prank(address(msca)); + assertEq( + EIP1271_INVALID_SIGNATURE, + module.validateSignature(address(msca), multisigEntityId, address(this), hash, sig) + ); + } + + function testFuzz_validateSignatureMixedSigTypes(MultisigInput memory input, bytes32 hash) public { + // Ensure 1 < totalSigners <= 10 + input.totalSigners %= 11; + vm.assume(input.totalSigners > 0); + // Ensure 1 < actualSigners < totalSigners + input.actualSigners %= 11; + input.actualSigners %= input.totalSigners; + vm.assume(input.actualSigners > 0); + input.sigDynamicPartOffset = 0; + console.log("totalSigners: ", input.totalSigners); + console.log("actualSigners: ", input.actualSigners); + bytes32 wrappedDigest = module.getReplaySafeMessageHash(address(msca), hash); + Signer[] memory signers = _installSignersOfMixedTypes(input); + _sortSignersById(signers); + bytes memory sig = _signSigs(input, signers, wrappedDigest, wrappedDigest); + vm.prank(address(msca)); + assertEq( + EIP1271_VALID_SIGNATURE, module.validateSignature(address(msca), multisigEntityId, address(this), hash, sig) + ); + } + + function testFuzz_validateUserOpNoActualDigestProvided(PackedUserOperation memory userOp) public { + userOp.accountGasLimits = ZERO_BYTES32; + userOp.preVerificationGas = ZERO; + userOp.gasFees = ZERO_BYTES32; + userOp.paymasterAndData = ""; + userOp.sender = address(msca); + bytes32 userOpHash = entryPoint.getUserOpHash(userOp); + vm.expectRevert(abi.encodeWithSelector(InvalidUserOpDigest.selector, multisigEntityId, address(msca))); + vm.prank(address(msca)); + module.validateUserOp(multisigEntityId, userOp, userOpHash); + } + + function testFuzz_validateUserOpLengthTooShort(PackedUserOperation memory userOp) public { + userOp.sender = address(msca); + userOp.signature = bytes("foo"); + bytes32 userOpHash = entryPoint.getUserOpHash(userOp); + vm.expectRevert( + abi.encodeWithSelector(InvalidSigLength.selector, multisigEntityId, address(msca), userOp.signature.length) + ); + vm.prank(address(msca)); + module.validateUserOp(multisigEntityId, userOp, userOpHash); + } + + function testFuzz_validateUserOpUninitializedAccount(PackedUserOperation memory userOp) public { + userOp.sender = address(msca); + userOp.signature = bytes.concat(bytes32(0), bytes32(0), bytes1(0)); + bytes32 userOpHash = entryPoint.getUserOpHash(userOp); + vm.expectRevert(abi.encodeWithSelector(Uninitialized.selector, multisigEntityId, address(msca))); + vm.prank(address(msca)); + module.validateUserOp(multisigEntityId, userOp, userOpHash); + } + + function testFuzz_validateUserOpSameSignerSignsRepeatedly( + string memory salt1, + string memory salt2, + string memory salt3, + PackedUserOperation memory userOp + ) public { + // make sure we have actual digest set in userOp when it's submitted for estimation (simulation) or validation + if (userOp.accountGasLimits == ZERO_BYTES32) { + userOp.accountGasLimits = bytes32(uint256(1)); + } + vm.assume(keccak256(abi.encodePacked(salt1)) != keccak256(abi.encodePacked(salt2))); + vm.assume(keccak256(abi.encodePacked(salt1)) != keccak256(abi.encodePacked(salt3))); + vm.assume(keccak256(abi.encodePacked(salt2)) != keccak256(abi.encodePacked(salt3))); + userOp.sender = address(msca); + Signer memory signer1 = _createEOASigner(salt1); + Signer memory signer2 = _createEOASigner(salt2); + Signer memory signer3 = _createEOASigner(salt3); + SignerMetadata[] memory signersMetadata = new SignerMetadata[](3); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = signer1.signerWallet.addr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = signer2.signerWallet.addr; + SignerMetadata memory signerMetaDataThree; + signerMetaDataThree.weight = 1; + signerMetaDataThree.addr = signer3.signerWallet.addr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + signersMetadata[2] = signerMetaDataThree; + vm.prank(address(msca)); + module.onInstall( + abi.encode( + multisigEntityId, + signersMetadata, + signerMetaDataOne.weight + signerMetaDataTwo.weight + signerMetaDataThree.weight + ) + ); + + // sign actual actualDigest + bytes32 fullUserOpHash = entryPoint.getUserOpHash(userOp); + Signer[] memory signers = new Signer[](3); + signers[0] = signer1; + signers[1] = signer2; + // repeated signer with the same weight + signers[2] = signer2; + _sortSignersById(signers); + + // create minimal userOpHash + userOp.preVerificationGas = 0; + userOp.accountGasLimits = ZERO_BYTES32; + userOp.gasFees = ZERO_BYTES32; + userOp.paymasterAndData = ""; + bytes32 minimalUserOpHash = entryPoint.getUserOpHash(userOp); + userOp.signature = _signSigs( + MultisigInput({actualSigners: 3, totalSigners: 3, sigDynamicPartOffset: 0}), + signers, + fullUserOpHash.toEthSignedMessageHash(), + minimalUserOpHash.toEthSignedMessageHash() + ); + + vm.prank(address(msca)); + assertEq(SIG_VALIDATION_FAILED, module.validateUserOp(multisigEntityId, userOp, fullUserOpHash)); + } + + function testFuzz_validateUserOpEOASigner( + string memory salt1, + string memory salt2, + PackedUserOperation memory userOp + ) public { + // make sure we have actual digest set in userOp when it's submitted for estimation (simulation) or validation + if (userOp.accountGasLimits == ZERO_BYTES32) { + userOp.accountGasLimits = bytes32(uint256(1)); + } + vm.assume(keccak256(abi.encodePacked(salt1)) != keccak256(abi.encodePacked(salt2))); + userOp.sender = address(msca); + Signer memory signer1 = _createEOASigner(salt1); + Signer memory signer2 = _createEOASigner(salt2); + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = signer1.signerWallet.addr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = signer2.signerWallet.addr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + // sign actual actualDigest + bytes32 fullUserOpHash = entryPoint.getUserOpHash(userOp); + Signer[] memory signers = new Signer[](2); + signers[0] = signer1; + signers[1] = signer2; + _sortSignersById(signers); + + // create minimal userOpHash + userOp.preVerificationGas = 0; + userOp.accountGasLimits = ZERO_BYTES32; + userOp.gasFees = ZERO_BYTES32; + userOp.paymasterAndData = ""; + bytes32 minimalUserOpHash = entryPoint.getUserOpHash(userOp); + userOp.signature = _signSigs( + MultisigInput({actualSigners: 2, totalSigners: 2, sigDynamicPartOffset: 0}), + signers, + fullUserOpHash.toEthSignedMessageHash(), + minimalUserOpHash.toEthSignedMessageHash() + ); + + vm.prank(address(msca)); + assertEq(SIG_VALIDATION_SUCCEEDED, module.validateUserOp(multisigEntityId, userOp, fullUserOpHash)); + } + + function testFuzz_validateUserOpWrongEOASigner( + string memory salt1, + string memory salt2, + PackedUserOperation memory userOp + ) public { + // make sure we have actual digest set in userOp when it's submitted for estimation (simulation) or validation + if (userOp.accountGasLimits == ZERO_BYTES32) { + userOp.accountGasLimits = bytes32(uint256(1)); + } + vm.assume(keccak256(abi.encodePacked(salt1)) != keccak256(abi.encodePacked(salt2))); + userOp.sender = address(msca); + Signer memory signer1 = _createEOASigner(salt1); + Signer memory signer2 = _createEOASigner(salt2); + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = signer1.signerWallet.addr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = signer2.signerWallet.addr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + // sign actual actualDigest + bytes32 fullUserOpHash = entryPoint.getUserOpHash(userOp); + Signer[] memory signers = new Signer[](2); + signers[0] = _createEOASigner("randomSigner"); + signers[1] = signer2; + _sortSignersById(signers); + + // create minimal userOpHash + userOp.preVerificationGas = 0; + userOp.accountGasLimits = ZERO_BYTES32; + userOp.gasFees = ZERO_BYTES32; + userOp.paymasterAndData = ""; + bytes32 minimalUserOpHash = entryPoint.getUserOpHash(userOp); + userOp.signature = _signSigs( + MultisigInput({actualSigners: 2, totalSigners: 2, sigDynamicPartOffset: 0}), + signers, + fullUserOpHash.toEthSignedMessageHash(), + minimalUserOpHash.toEthSignedMessageHash() + ); + + vm.prank(address(msca)); + assertEq(SIG_VALIDATION_FAILED, module.validateUserOp(multisigEntityId, userOp, fullUserOpHash)); + } + + function testFuzz_validateUserOpContractSigner( + string memory salt1, + string memory salt2, + PackedUserOperation memory userOp + ) public { + // make sure we have actual digest set in userOp when it's submitted for estimation (simulation) or validation + if (userOp.accountGasLimits == ZERO_BYTES32) { + userOp.accountGasLimits = bytes32(uint256(1)); + } + vm.assume(keccak256(abi.encodePacked(salt1)) != keccak256(abi.encodePacked(salt2))); + userOp.sender = address(msca); + Signer memory signer1 = _createContractSigner(salt1); + Signer memory signer2 = _createContractSigner(salt2); + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = signer1.contractAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = signer2.contractAddr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + // sign actual actualDigest + bytes32 fullUserOpHash = entryPoint.getUserOpHash(userOp); + Signer[] memory signers = new Signer[](2); + signers[0] = signer1; + signers[1] = signer2; + _sortSignersById(signers); + + // create minimal userOpHash + userOp.preVerificationGas = 0; + userOp.accountGasLimits = ZERO_BYTES32; + userOp.gasFees = ZERO_BYTES32; + userOp.paymasterAndData = ""; + bytes32 minimalUserOpHash = entryPoint.getUserOpHash(userOp); + userOp.signature = _signSigs( + MultisigInput({actualSigners: 2, totalSigners: 2, sigDynamicPartOffset: 0}), + signers, + fullUserOpHash.toEthSignedMessageHash(), + minimalUserOpHash.toEthSignedMessageHash() + ); + + vm.prank(address(msca)); + assertEq(SIG_VALIDATION_SUCCEEDED, module.validateUserOp(multisigEntityId, userOp, fullUserOpHash)); + } + + function testFuzz_validateUserOpWrongContractSigner( + string memory salt1, + string memory salt2, + PackedUserOperation memory userOp + ) public { + // make sure we have actual digest set in userOp when it's submitted for estimation (simulation) or validation + if (userOp.accountGasLimits == ZERO_BYTES32) { + userOp.accountGasLimits = bytes32(uint256(1)); + } + vm.assume(keccak256(abi.encodePacked(salt1)) != keccak256(abi.encodePacked(salt2))); + userOp.sender = address(msca); + Signer memory signer1 = _createContractSigner(salt1); + Signer memory signer2 = _createContractSigner(salt2); + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.addr = signer1.contractAddr; + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.addr = signer2.contractAddr; + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + // sign actual actualDigest + bytes32 fullUserOpHash = entryPoint.getUserOpHash(userOp); + Signer[] memory signers = new Signer[](2); + signers[0] = _createContractSigner("randomSigner"); + signers[1] = signer2; + _sortSignersById(signers); + + // create minimal userOpHash + userOp.preVerificationGas = 0; + userOp.accountGasLimits = ZERO_BYTES32; + userOp.gasFees = ZERO_BYTES32; + userOp.paymasterAndData = ""; + bytes32 minimalUserOpHash = entryPoint.getUserOpHash(userOp); + userOp.signature = _signSigs( + MultisigInput({actualSigners: 2, totalSigners: 2, sigDynamicPartOffset: 0}), + signers, + fullUserOpHash.toEthSignedMessageHash(), + minimalUserOpHash.toEthSignedMessageHash() + ); + + vm.prank(address(msca)); + assertEq(SIG_VALIDATION_FAILED, module.validateUserOp(multisigEntityId, userOp, fullUserOpHash)); + } + + function testFuzz_validateUserOpPasskeySigner(uint8 salt1, PackedUserOperation memory userOp) public { + // make sure we have actual digest set in userOp when it's submitted for estimation (simulation) or validation + if (userOp.accountGasLimits == ZERO_BYTES32) { + userOp.accountGasLimits = bytes32(uint256(1)); + } + vm.assume(salt1 <= uint8(9)); + uint8 salt2 = salt1 + 1; + userOp.sender = address(msca); + Signer memory signer1 = _createPasskeySigner(uint256(salt1)); + Signer memory signer2 = _createPasskeySigner(uint256(salt2)); + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.publicKey = + PublicKey({x: signer1.signerWallet.publicKeyX, y: signer1.signerWallet.publicKeyY}); + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.publicKey = + PublicKey({x: signer2.signerWallet.publicKeyX, y: signer2.signerWallet.publicKeyY}); + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + // sign actual actualDigest + bytes32 fullUserOpHash = entryPoint.getUserOpHash(userOp); + Signer[] memory signers = new Signer[](2); + signers[0] = signer1; + signers[1] = signer2; + _sortSignersById(signers); + + // create minimal userOpHash + userOp.preVerificationGas = 0; + userOp.accountGasLimits = ZERO_BYTES32; + userOp.gasFees = ZERO_BYTES32; + userOp.paymasterAndData = ""; + bytes32 minimalUserOpHash = entryPoint.getUserOpHash(userOp); + userOp.signature = _signSigs( + MultisigInput({actualSigners: 2, totalSigners: 2, sigDynamicPartOffset: 0}), + signers, + fullUserOpHash.toEthSignedMessageHash(), + minimalUserOpHash.toEthSignedMessageHash() + ); + + vm.prank(address(msca)); + assertEq(SIG_VALIDATION_SUCCEEDED, module.validateUserOp(multisigEntityId, userOp, fullUserOpHash)); + } + + function testFuzz_validateUserOpWrongPasskeySigner(uint8 salt1, PackedUserOperation memory userOp) public { + // make sure we have actual digest set in userOp when it's submitted for estimation (simulation) or validation + if (userOp.accountGasLimits == ZERO_BYTES32) { + userOp.accountGasLimits = bytes32(uint256(1)); + } + vm.assume(salt1 <= uint8(8)); + uint8 salt2 = salt1 + 1; + uint8 randomSalt = salt1 + 2; + vm.assume(randomSalt < uint8(11)); + userOp.sender = address(msca); + Signer memory signer1 = _createPasskeySigner(uint256(salt1)); + Signer memory signer2 = _createPasskeySigner(uint256(salt2)); + SignerMetadata[] memory signersMetadata = new SignerMetadata[](2); + SignerMetadata memory signerMetaDataOne; + signerMetaDataOne.weight = 1; + signerMetaDataOne.publicKey = + PublicKey({x: signer1.signerWallet.publicKeyX, y: signer1.signerWallet.publicKeyY}); + SignerMetadata memory signerMetaDataTwo; + signerMetaDataTwo.weight = 1; + signerMetaDataTwo.publicKey = + PublicKey({x: signer2.signerWallet.publicKeyX, y: signer2.signerWallet.publicKeyY}); + signersMetadata[0] = signerMetaDataOne; + signersMetadata[1] = signerMetaDataTwo; + vm.prank(address(msca)); + module.onInstall( + abi.encode(multisigEntityId, signersMetadata, signerMetaDataOne.weight + signerMetaDataTwo.weight) + ); + + // sign actual actualDigest + bytes32 fullUserOpHash = entryPoint.getUserOpHash(userOp); + Signer[] memory signers = new Signer[](2); + signers[0] = signer1; + signers[1] = _createPasskeySigner(uint256(randomSalt)); + _sortSignersById(signers); + + // create minimal userOpHash + userOp.preVerificationGas = 0; + userOp.accountGasLimits = ZERO_BYTES32; + userOp.gasFees = ZERO_BYTES32; + userOp.paymasterAndData = ""; + bytes32 minimalUserOpHash = entryPoint.getUserOpHash(userOp); + userOp.signature = _signSigs( + MultisigInput({actualSigners: 2, totalSigners: 2, sigDynamicPartOffset: 0}), + signers, + fullUserOpHash.toEthSignedMessageHash(), + minimalUserOpHash.toEthSignedMessageHash() + ); + + vm.prank(address(msca)); + assertEq(SIG_VALIDATION_FAILED, module.validateUserOp(multisigEntityId, userOp, fullUserOpHash)); + } + + function testFuzz_validateUserOpMixedSigTypes(MultisigInput memory input, PackedUserOperation memory userOp) + public + { + // Ensure 1 < totalSigners <= 10 + input.totalSigners %= 11; + vm.assume(input.totalSigners > 0); + // Ensure 1 < actualSigners < totalSigners + input.actualSigners %= 11; + input.actualSigners %= input.totalSigners; + input.sigDynamicPartOffset = 0; + userOp.sender = address(msca); + vm.assume(input.actualSigners > 0); + input.sigDynamicPartOffset = 0; + bytes32 fullUserOpHash = entryPoint.getUserOpHash(userOp); + // create minimal userOpHash + userOp.preVerificationGas = 0; + userOp.accountGasLimits = ZERO_BYTES32; + userOp.gasFees = ZERO_BYTES32; + userOp.paymasterAndData = ""; + bytes32 minimalUserOpHash = entryPoint.getUserOpHash(userOp); + Signer[] memory signers = _installSignersOfMixedTypes(input); + _sortSignersById(signers); + userOp.signature = _signSigs( + input, signers, fullUserOpHash.toEthSignedMessageHash(), minimalUserOpHash.toEthSignedMessageHash() + ); + vm.prank(address(msca)); + assertEq(SIG_VALIDATION_SUCCEEDED, module.validateUserOp(multisigEntityId, userOp, fullUserOpHash)); + } + + function _signSigs(MultisigInput memory input, Signer[] memory signers, bytes32 fullDigest, bytes32 minimalDigest) + internal + pure + returns (bytes memory signature) + { + bytes30 signerIdMustSignFullDigest; + if (fullDigest != minimalDigest) { + signerIdMustSignFullDigest = signers[input.totalSigners % input.actualSigners].signerId; + } + bytes memory sigConstantParts = bytes(""); + bytes memory sigDynamicParts = bytes(""); + input.sigDynamicPartOffset = input.actualSigners * 65; // start after constant part + for (uint256 i = 0; i < input.actualSigners; i++) { + // append constant and dynamic parts from individual signer + (bytes memory individualSigConstantPart, bytes memory individualSigDynamicPart) = + _signIndividualSig(input, signers[i], fullDigest, minimalDigest, signerIdMustSignFullDigest); + sigConstantParts = abi.encodePacked(sigConstantParts, individualSigConstantPart); + sigDynamicParts = abi.encodePacked(sigDynamicParts, individualSigDynamicPart); + } + signature = abi.encodePacked(sigConstantParts, sigDynamicParts); + return signature; + } + + function _signIndividualSig( + MultisigInput memory input, + Signer memory signer, + bytes32 fullDigest, + bytes32 minimalDigest, + bytes30 signerIdMustSignFullDigest + ) internal pure returns (bytes memory sigConstantPart, bytes memory sigDynamicPart) { + if (signer.sigType == 27) { + console.logString("eoa signer signs.."); + // only produce constant parts + sigConstantPart = _signEOASig(signer, fullDigest, minimalDigest, signerIdMustSignFullDigest); + } else if (signer.sigType == 0) { + console.logString("contract signer signs.."); + uint8 v; + (v, sigDynamicPart) = _signContractSig(signer, fullDigest, minimalDigest, signerIdMustSignFullDigest); + sigConstantPart = abi.encodePacked(abi.encode(signer.contractAddr), uint256(input.sigDynamicPartOffset), v); + input.sigDynamicPartOffset += sigDynamicPart.length; // ecdsa is 97 = 65 (k1 sig length) + 32 (length of + // sig) + } else { + console.logString("r1 signer signs.."); + uint8 v; + (v, sigDynamicPart) = _signR1Sig(signer, fullDigest, minimalDigest, signerIdMustSignFullDigest); + bytes32 pubKeyId = bytes32(bytes.concat(bytes2(0), signer.signerId)); + sigConstantPart = abi.encodePacked(pubKeyId, uint256(input.sigDynamicPartOffset), v); + input.sigDynamicPartOffset += (sigDynamicPart.length); + } + return (sigConstantPart, sigDynamicPart); + } + + function _signEOASig( + Signer memory signer, + bytes32 fullDigest, + bytes32 minimalDigest, + bytes30 signerIdMustSignFullDigest + ) internal pure returns (bytes memory signed) { + bytes32 r; + bytes32 s; + uint8 v; + if (signer.signerId == signerIdMustSignFullDigest) { + (v, r, s) = vm.sign(signer.signerWallet.privateKey, fullDigest); + v += 32; + } else { + (v, r, s) = vm.sign(signer.signerWallet.privateKey, minimalDigest); + } + signer.sigType = v; + return abi.encodePacked(r, s, v); + } + + function _signContractSig( + Signer memory signer, + bytes32 fullDigest, + bytes32 minimalDigest, + bytes30 signerIdMustSignFullDigest + ) internal pure returns (uint8 v, bytes memory sigDynamicParts) { + bytes32 r; + bytes32 s; + if (signer.signerId == signerIdMustSignFullDigest) { + (v, r, s) = vm.sign(signer.signerWallet.privateKey, fullDigest); + sigDynamicParts = abi.encodePacked(uint256(65), r, s, v); + v = 32; // 0 + 32 + } else { + (v, r, s) = vm.sign(signer.signerWallet.privateKey, minimalDigest); + sigDynamicParts = abi.encodePacked(uint256(65), r, s, v); + v = 0; + } + return (v, sigDynamicParts); + } + + function _signR1Sig( + Signer memory signer, + bytes32 fullDigest, + bytes32 minimalDigest, + bytes30 signerIdMustSignFullDigest + ) internal pure returns (uint8 v, bytes memory sigDynamicParts) { + bytes memory sigBytes; + if (signer.signerId == signerIdMustSignFullDigest) { + WebAuthnSigDynamicPart memory webAuthnSigDynamicPartForFullDigest; + webAuthnSigDynamicPartForFullDigest.webAuthnData = _getWebAuthnData(fullDigest); + bytes32 webauthnFullDigest = _getWebAuthnMessageHash(webAuthnSigDynamicPartForFullDigest.webAuthnData); + (webAuthnSigDynamicPartForFullDigest.r, webAuthnSigDynamicPartForFullDigest.s) = + signP256Message(vm, signer.signerWallet.privateKey, webauthnFullDigest); + v = 34; // 2 + 32 + sigBytes = abi.encode(webAuthnSigDynamicPartForFullDigest); + } else { + WebAuthnSigDynamicPart memory webAuthnSigDynamicPartForMinimalDigest; + webAuthnSigDynamicPartForMinimalDigest.webAuthnData = _getWebAuthnData(minimalDigest); + bytes32 webauthnMinimalDigest = _getWebAuthnMessageHash(webAuthnSigDynamicPartForMinimalDigest.webAuthnData); + (webAuthnSigDynamicPartForMinimalDigest.r, webAuthnSigDynamicPartForMinimalDigest.s) = + signP256Message(vm, signer.signerWallet.privateKey, webauthnMinimalDigest); + v = 2; + sigBytes = abi.encode(webAuthnSigDynamicPartForMinimalDigest); + } + // length of bytes || sig data bytes + sigDynamicParts = abi.encodePacked(uint256(sigBytes.length), sigBytes); + return (v, sigDynamicParts); + } + + function _createEOASigner(string memory signerKeySeed) internal returns (Signer memory o) { + VmSafe.Wallet memory signerWallet; + (signerWallet.addr, signerWallet.privateKey) = makeAddrAndKey(signerKeySeed); + o.signerId = module.getSignerId(signerWallet.addr); + o.signerWallet = signerWallet; + o.sigType = 27; // will be overridden for full digest later + return o; + } + + function _createContractSigner(string memory signerKeySeed) internal returns (Signer memory o) { + VmSafe.Wallet memory signerWallet; + (signerWallet.addr, signerWallet.privateKey) = makeAddrAndKey(signerKeySeed); + MockContractOwner m = new MockContractOwner(signerWallet.addr); + o.signerId = module.getSignerId(address(m)); + o.signerWallet = signerWallet; + o.contractAddr = address(m); + o.sigType = 0; // will be overridden for full digest later + return o; + } + + // TODO: generate dynamic keys when library has better support + function _createPasskeySigner(uint256 label) internal view returns (Signer memory o) { + TestR1Key[] memory testR1Keys = _loadR1Keys(); + VmSafe.Wallet memory signerWallet; + if (label < 11) { + // p256key_11_fixture.json only supports 11 keys + signerWallet.privateKey = testR1Keys[label].privateKey; + signerWallet.publicKeyX = testR1Keys[label].publicKeyX; + signerWallet.publicKeyY = testR1Keys[label].publicKeyY; + } else { + revert Unsupported(); + } + o.signerId = module.getSignerId(PublicKey({x: signerWallet.publicKeyX, y: signerWallet.publicKeyY})); + o.signerWallet = signerWallet; + o.sigType = 2; // will be overridden for full digest later + return o; + } + + function _sortSignersById(Signer[] memory signers) internal pure { + uint256 n = signers.length; + uint256 minIdx; + for (uint256 i = 0; i < n; i++) { + minIdx = i; + for (uint256 j = i; j < n; j++) { + if (signers[j].signerId < signers[minIdx].signerId) { + minIdx = j; + } + } + (signers[i], signers[minIdx]) = (signers[minIdx], signers[i]); + } + } + + function _getWebAuthnData(bytes32 challenge) internal pure returns (WebAuthnData memory webAuthnData) { + webAuthnData.clientDataJSON = string( + abi.encodePacked( + // solhint-disable-next-line quotes + '{"type":"webauthn.get","challenge":"', + WebAuthnLib.encodeURL(abi.encode(challenge)), + // solhint-disable-next-line quotes + '","origin":"https://developers.circle.com/","crossOrigin":false}' + ) + ); + // TODO: write a python script to generate authenticator data randomly + webAuthnData.authenticatorData = hex"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630100000001"; + webAuthnData.challengeIndex = 23; + webAuthnData.typeIndex = 1; + return webAuthnData; + } + + function _getWebAuthnMessageHash(WebAuthnData memory webAuthnData) internal pure returns (bytes32 messageHash) { + bytes32 clientDataJSONHash = sha256(bytes(webAuthnData.clientDataJSON)); + messageHash = sha256(abi.encodePacked(webAuthnData.authenticatorData, clientDataJSONHash)); + return messageHash; + } + + /// @dev load 10 pre generated r1 keys for testing purpose. + function _loadR1Keys() internal view returns (TestR1Key[] memory testKeys) { + string memory rootPath = vm.projectRoot(); + string memory path = string.concat(rootPath, P256_10_KEYS_FIXTURE); + string memory json = vm.readFile(path); + uint256 count = abi.decode(json.parseRaw(".numOfKeys"), (uint256)); + testKeys = new TestR1Key[](count); + + for (uint256 i; i < count; ++i) { + (, uint256 privateKey, uint256 x, uint256 y) = _parseJson({json: json, resultIndex: i}); + testKeys[i] = TestR1Key({privateKey: privateKey, publicKeyX: x, publicKeyY: y}); + } + } + + function _parseJson(string memory json, uint256 resultIndex) + internal + pure + returns (string memory jsonResultSelector, uint256 privateKey, uint256 x, uint256 y) + { + jsonResultSelector = string.concat(".results.[", string.concat(vm.toString(resultIndex), "]")); + privateKey = abi.decode(json.parseRaw(string.concat(jsonResultSelector, ".private_key")), (uint256)); + x = abi.decode(json.parseRaw(string.concat(jsonResultSelector, ".x")), (uint256)); + y = abi.decode(json.parseRaw(string.concat(jsonResultSelector, ".y")), (uint256)); + } + + function _installSignersOfMixedTypes(MultisigInput memory input) internal returns (Signer[] memory signers) { + SignerMetadata[] memory signersMetadata = new SignerMetadata[](input.totalSigners); + signers = new Signer[](input.totalSigners); + uint256 numOfK1Signers; + uint256 numOfR1Signers; + for (uint256 i = 0; i < input.totalSigners; i++) { + if ((input.actualSigners + input.totalSigners + i) % 3 == 0) { + // contract k1 signer + console.logString("we have a contract signer.."); + signers[i] = _createContractSigner((input.actualSigners + input.totalSigners + i).toString()); + signersMetadata[i].weight = 1; + signersMetadata[i].addr = signers[i].contractAddr; + numOfK1Signers++; + } else if ((input.actualSigners + input.totalSigners + i) % 3 == 1) { + // eoa k1 signer + console.logString("we have an eoa signer.."); + signers[i] = _createEOASigner((input.actualSigners + input.totalSigners + i).toString()); + signersMetadata[i].weight = 1; + signersMetadata[i].addr = signers[i].signerWallet.addr; + numOfK1Signers++; + } else { + // r1 signer + console.logString("we have a r1 signer.."); + signers[i] = _createPasskeySigner(numOfR1Signers); + signersMetadata[i].weight = 1; + signersMetadata[i].publicKey = + PublicKey({x: signers[i].signerWallet.publicKeyX, y: signers[i].signerWallet.publicKeyY}); + numOfR1Signers++; + } + } + + // initial thresholdWeight is set to k because every signer has weight 1 + vm.prank(address(msca)); + module.onInstall(abi.encode(multisigEntityId, signersMetadata, input.actualSigners)); + return signers; + } +} diff --git a/yarn.lock b/yarn.lock index b69a1eb..b05e520 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,9 +25,9 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@erc6900/reference-implementation@github:erc6900/reference-implementation#v0.8.0-rc.6": - version "0.8.0-rc.5" - resolved "https://codeload.github.com/erc6900/reference-implementation/tar.gz/6cdcfa653eb019d27d23586a86ff8171201a4066" +"@erc6900/reference-implementation@github:erc6900/reference-implementation#v0.8.0": + version "0.8.0" + resolved "https://codeload.github.com/erc6900/reference-implementation/tar.gz/c9b256cfd963a655179fa3cd9ea3f92c73cbfcdd" "@modular-account-libs@github:erc6900/modular-account-libs#v0.8.0-rc.0": version "0.8.0-rc.0" @@ -69,10 +69,10 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-5.6.0.tgz#41dd6093d34652cddb5d5bdeee04eafc33826668" integrity sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g== -"@solidity-parser/parser@^0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.18.0.tgz#8e77a02a09ecce957255a2f48c9a7178ec191908" - integrity sha512-yfORGUIPgLck41qyN7nbwJRAx17/jAIXCTanHOJZhB6PJ1iAk/84b/xlsVKFSyNyLXIj0dhppoE0+CRws7wlzA== +"@solidity-parser/parser@^0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.19.0.tgz#37a8983b2725af9b14ff8c4a475fa0e98d773c3f" + integrity sha512-RV16k/qIxW/wWc+mLzV3ARyKUaMUTBy9tOLMzFhtNSKYeTAanQ3a5MudJKf/8arIFnA2L27SNjarQKmFg0w/jA== "@szmarczak/http-timer@^5.0.1": version "5.0.1" @@ -302,9 +302,9 @@ fast-uri@^3.0.1: version "0.0.0" resolved "https://codeload.github.com/rdubois-crypto/FreshCryptoLib/tar.gz/8179e08cac72072bd260796633fec41fdfd5b441" -"forge-std@github:foundry-rs/forge-std#v1.9.2": - version "1.9.2" - resolved "https://codeload.github.com/foundry-rs/forge-std/tar.gz/1714bee72e286e73f76e320d110e0eaf5c4e649d" +"forge-std@github:foundry-rs/forge-std#v1.9.5": + version "1.9.5" + resolved "https://codeload.github.com/foundry-rs/forge-std/tar.gz/b93cf4bc34ff214c099dc970b153f85ade8c9f66" form-data-encoder@^2.1.2: version "2.1.4" @@ -650,12 +650,12 @@ solady@0.0.243: resolved "https://registry.yarnpkg.com/solady/-/solady-0.0.243.tgz#f1d3063206f44111c45a987d66f68a0a72c9f60e" integrity sha512-KwYZ79Sx+KUQ04d4dyj5P+ny1yi1LYDYv2ixZtgr43Eyn3PTvc+21do+eHG63vl4ZSREh3B3W9BAIgar68qF1A== -solhint@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/solhint/-/solhint-5.0.3.tgz#b57f6d2534fe09a60f9db1b92e834363edd1cbde" - integrity sha512-OLCH6qm/mZTCpplTXzXTJGId1zrtNuDYP5c2e6snIv/hdRVxPfBBz/bAlL91bY/Accavkayp2Zp2BaDSrLVXTQ== +solhint@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/solhint/-/solhint-5.0.5.tgz#43d9c730c2b22e11aa45582580749e0801a049d4" + integrity sha512-WrnG6T+/UduuzSWsSOAbfq1ywLUDwNea3Gd5hg6PS+pLUm8lz2ECNr0beX609clBxmDeZ3676AiA9nPDljmbJQ== dependencies: - "@solidity-parser/parser" "^0.18.0" + "@solidity-parser/parser" "^0.19.0" ajv "^6.12.6" antlr4 "^4.13.1-patch-1" ast-parents "^0.0.1"