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"