123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- = Multisig Account
- A multi-signature (multisig) account is a smart account that requires multiple authorized signers to approve operations before execution. Unlike traditional accounts controlled by a single private key, multisigs distribute control among multiple parties, eliminating single points of failure. For example, a 2-of-3 multisig requires signatures from at least 2 out of 3 possible signers.
- Popular implementations like https://safe.global/[Safe] (formerly Gnosis Safe) have become the standard for securing valuable assets. Multisigs provide enhanced security through collective authorization, customizable controls for ownership and thresholds, and the ability to rotate signers without changing the account address.
- == Beyond Standard Signature Verification
- As discussed in the xref:accounts.adoc#signature_validation[accounts section], the standard approach for smart contracts to verify signatures is https://eips.ethereum.org/EIPS/eip-1271[ERC-1271], which defines an `isValidSignature(hash, signature)`. However, it is limited in two important ways:
- 1. It assumes the signer has an EVM address
- 2. It treats the signer as a single identity
- This becomes problematic when implementing multisig accounts where:
- * You may want to use signers that don't have EVM addresses (like keys from hardware devices)
- * Each signer needs to be individually verified rather than treated as a collective identity
- * You need a threshold system to determine when enough valid signatures are present
- The https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/SignatureChecker.sol[SignatureChecker] library is useful for verifying EOA and ERC-1271 signatures, but it's not designed for more complex arrangements like threshold-based multisigs.
- == ERC-7913 Signers
- https://eips.ethereum.org/EIPS/eip-7913[ERC-7913] extends the concept of signer representation to include keys that don't have EVM addresses, addressing this limitation. OpenZeppelin implements this standard through three contracts:
- === SignerERC7913
- The xref:api:utils.adoc#SignerERC7913[`SignerERC7913`] contract allows a single ERC-7913 formatted signer to control an account. The signer is represented as a `bytes` object that concatenates a verifier address and a key: `verifier || key`.
- [source,solidity]
- ----
- // contracts/MyAccountERC7913.sol
- // SPDX-License-Identifier: MIT
- pragma solidity ^0.8.24;
- import {Account} from "@openzeppelin/community-contracts/account/Account.sol";
- import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
- import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
- import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
- import {ERC7739} from "@openzeppelin/community-contracts/utils/cryptography/signers/ERC7739.sol";
- import {ERC7821} from "@openzeppelin/community-contracts/account/extensions/ERC7821.sol";
- import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
- import {SignerERC7913} from "@openzeppelin/community-contracts/utils/cryptography/signers/SignerERC7913.sol";
- contract MyAccountERC7913 is Account, SignerERC7913, ERC7739, ERC7821, ERC721Holder, ERC1155Holder, Initializable {
- constructor() EIP712("MyAccount7913", "1") {}
- function initialize(bytes memory signer) public initializer {
- _setSigner(signer);
- }
- function setSigner(bytes memory signer) public onlyEntryPointOrSelf {
- _setSigner(signer);
- }
- /// @dev Allows the entry point as an authorized executor.
- function _erc7821AuthorizedExecutor(
- address caller,
- bytes32 mode,
- bytes calldata executionData
- ) internal view virtual override returns (bool) {
- return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
- }
- }
- ----
- WARNING: Leaving an account uninitialized may leave it unusable since no public key was associated with it.
- === MultiSignerERC7913
- The xref:api:utils/cryptography.adoc#MultiSignerERC7913[`MultiSignerERC7913`] contract extends this concept to support multiple signers with a threshold-based signature verification system.
- [source,solidity]
- ----
- // contracts/MyAccountMultiSigner.sol
- // SPDX-License-Identifier: MIT
- pragma solidity ^0.8.27;
- import {Account} from "@openzeppelin/community-contracts/account/Account.sol";
- import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
- import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
- import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
- import {ERC7739} from "@openzeppelin/community-contracts/utils/cryptography/signers/ERC7739.sol";
- import {ERC7821} from "@openzeppelin/community-contracts/account/extensions/ERC7821.sol";
- import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
- import {MultiSignerERC7913} from "@openzeppelin/community-contracts/utils/cryptography/signers/MultiSignerERC7913.sol";
- contract MyAccountMultiSigner is
- Account,
- MultiSignerERC7913,
- ERC7739,
- ERC7821,
- ERC721Holder,
- ERC1155Holder,
- Initializable
- {
- constructor() EIP712("MyAccountMultiSigner", "1") {}
- function initialize(bytes[] memory signers, uint256 threshold) public initializer {
- _addSigners(signers);
- _setThreshold(threshold);
- }
- function addSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
- _addSigners(signers);
- }
- function removeSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
- _removeSigners(signers);
- }
- function setThreshold(uint256 threshold) public onlyEntryPointOrSelf {
- _setThreshold(threshold);
- }
- /// @dev Allows the entry point as an authorized executor.
- function _erc7821AuthorizedExecutor(
- address caller,
- bytes32 mode,
- bytes calldata executionData
- ) internal view virtual override returns (bool) {
- return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
- }
- }
- ----
- This implementation is ideal for standard multisig setups where each signer has equal authority, and a fixed number of approvals is required.
- The `MultiSignerERC7913` contract provides several key features for managing multi-signature accounts. It maintains a set of authorized signers and implements a threshold-based system that requires a minimum number of signatures to approve operations. The contract includes an internal interface for managing signers, allowing for the addition and removal of authorized parties.
- NOTE: `MultiSignerERC7913` safeguards to ensure that the threshold remains achievable based on the current number of active signers, preventing situations where operations could become impossible to execute.
- The contract also provides public functions for querying signer information: xref:api:utils/cryptography.adoc#MultiSignerERC7913-isSigner-bytes-[`isSigner(bytes memory signer)`] to check if a given signer is authorized, xref:api:utils/cryptography.adoc#MultiSignerERC7913-getSigners-uint64-uint64-[`getSigners(uint64 start, uint64 end)`] to retrieve a paginated list of authorized signers, and xref:api:utils/cryptography.adoc#MultiSignerERC7913-getSignerCount[`getSignerCount()`] to get the total number of signers. These functions are useful when validating signatures, implementing customized access control logic, or building user interfaces that need to display signer information.
- === MultiSignerERC7913Weighted
- For more sophisticated governance structures, the xref:api:utils/cryptography.adoc#MultiSignerERC7913Weighted[`MultiSignerERC7913Weighted`] contract extends `MultiSignerERC7913` by assigning different weights to each signer.
- [source,solidity]
- ----
- // contracts/MyAccountMultiSignerWeighted.sol
- // SPDX-License-Identifier: MIT
- pragma solidity ^0.8.27;
- import {Account} from "@openzeppelin/community-contracts/account/Account.sol";
- import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
- import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
- import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
- import {ERC7739} from "@openzeppelin/community-contracts/utils/cryptography/signers/ERC7739.sol";
- import {ERC7821} from "@openzeppelin/community-contracts/account/extensions/ERC7821.sol";
- import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
- import {MultiSignerERC7913Weighted} from "@openzeppelin/community-contracts/utils/cryptography/signers/MultiSignerERC7913Weighted.sol";
- contract MyAccountMultiSignerWeighted is
- Account,
- MultiSignerERC7913Weighted,
- ERC7739,
- ERC7821,
- ERC721Holder,
- ERC1155Holder,
- Initializable
- {
- constructor() EIP712("MyAccountMultiSignerWeighted", "1") {}
- function initialize(bytes[] memory signers, uint256[] memory weights, uint256 threshold) public initializer {
- _addSigners(signers);
- _setSignerWeights(signers, weights);
- _setThreshold(threshold);
- }
- function addSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
- _addSigners(signers);
- }
- function removeSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
- _removeSigners(signers);
- }
- function setThreshold(uint256 threshold) public onlyEntryPointOrSelf {
- _setThreshold(threshold);
- }
- function setSignerWeights(bytes[] memory signers, uint256[] memory weights) public onlyEntryPointOrSelf {
- _setSignerWeights(signers, weights);
- }
- /// @dev Allows the entry point as an authorized executor.
- function _erc7821AuthorizedExecutor(
- address caller,
- bytes32 mode,
- bytes calldata executionData
- ) internal view virtual override returns (bool) {
- return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
- }
- }
- ----
- This implementation is perfect for scenarios where different signers should have varying levels of authority, such as:
- * Board members with different voting powers
- * Organizational structures with hierarchical decision-making
- * Hybrid governance systems combining core team and community members
- * Execution setups like "social recovery" where you trust particular guardians more than others
- The `MultiSignerERC7913Weighted` contract extends `MultiSignerERC7913` with a weighting system. Each signer can have a custom weight, and operations require the total weight of signing participants to meet or exceed the threshold. Signers without explicit weights default to a weight of 1.
- NOTE: When setting up a weighted multisig, ensure the threshold value matches the scale used for signer weights. For example, if signers have weights like 1, 2, or 3, then a threshold of 4 would require at least two signers (e.g., one with weight 1 and one with weight 3).
- == Setting Up a Multisig Account
- To create a multisig account, you need to:
- 1. Define your signers
- 2. Determine your threshold
- 3. Initialize your account with these parameters
- The example below demonstrates setting up a 2-of-3 multisig account with different types of signers:
- [source,solidity]
- ----
- // Example setup code
- function setupMultisigAccount() external {
- // Create signers using different types of keys
- bytes memory ecdsaSigner = alice; // EOA address (20 bytes)
-
- // P256 signer with format: verifier || pubKey
- bytes memory p256Signer = abi.encodePacked(
- p256Verifier,
- bobP256PublicKeyX,
- bobP256PublicKeyY
- );
-
- // RSA signer with format: verifier || pubKey
- bytes memory rsaSigner = abi.encodePacked(
- rsaVerifier,
- abi.encode(charlieRSAPublicKeyE, charlieRSAPublicKeyN)
- );
-
- // Create array of signers
- bytes[] memory signers = new bytes[](3);
- signers[0] = ecdsaSigner;
- signers[1] = p256Signer;
- signers[2] = rsaSigner;
-
- // Set threshold to 2 (2-of-3 multisig)
- uint256 threshold = 2;
-
- // Initialize the account
- myMultisigAccount.initialize(signers, threshold);
- }
- ----
- For a weighted multisig, you would also specify weights:
- [source,solidity]
- ----
- // Example setup for weighted multisig
- function setupWeightedMultisigAccount() external {
- // Create array of signers (same as above)
- bytes[] memory signers = new bytes[](3);
- signers[0] = ecdsaSigner;
- signers[1] = p256Signer;
- signers[2] = rsaSigner;
-
- // Assign weights to signers (Alice:1, Bob:2, Charlie:3)
- uint256[] memory weights = new uint256[](3);
- weights[0] = 1;
- weights[1] = 2;
- weights[2] = 3;
-
- // Set threshold to 4 (requires at least Bob+Charlie or all three)
- uint256 threshold = 4;
-
- // Initialize the weighted account
- myWeightedMultisigAccount.initialize(signers, weights, threshold);
- }
- ----
- IMPORTANT: The xref:api:utils/cryptography.adoc#MultiSignerERC7913-_validateReachableThreshold--[`_validateReachableThreshold`] function ensures that the sum of weights for all active signers meets or exceeds the threshold. Any customization built on top of the multisigner contracts must ensure the threshold is always reachable.
- For multisig accounts, the signature is a complex structure that contains both the signers and their individual signatures. The format follows ERC-7913's specification and must be properly encoded.
- === Signature Format
- The multisig signature is encoded as:
- [source,solidity]
- ----
- abi.encode(
- bytes[] signers, // Array of signers sorted by `keccak256`
- bytes[] signatures // Array of signatures corresponding to each signer
- )
- ----
- Where:
- * `signers` is an array of the signers participating in this particular signature
- * `signatures` is an array of the individual signatures corresponding to each signer
- [NOTE]
- ====
- To avoid duplicate signers, the contract uses `keccak256` to generate a unique id for each signer. When providing a multisignature, the `signers` array should be sorted in ascending order by `keccak256`, and the `signatures` array must match the order of their corresponding signers.
- ====
|