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 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 accounts section, the standard approach for smart contracts to verify signatures is ERC-1271, which defines an isValidSignature(hash, signature)
. However, it is limited in two important ways:
-
It assumes the signer has an EVM address
-
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 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
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 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
.
// 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/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/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);
}
/// @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);
}
}
Leaving an account uninitialized may leave it unusable since no public key was associated with it. |
MultiSignerERC7913
The MultiSignerERC7913
contract extends this concept to support multiple signers with a threshold-based signature verification system.
// contracts/MyAccountERC7913.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/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/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.
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 the public isSigner(bytes memory signer)
function to check if a given signer is authorized, which is useful when validating signatures or implementing customized access control logic.
MultiSignerERC7913Weighted
For more sophisticated governance structures, the MultiSignerERC7913Weighted
contract extends MultiSignerERC7913
by assigning different weights to each signer.
// contracts/MyAccountERC7913.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/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/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.
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:
-
Define your signers
-
Determine your threshold
-
Initialize your account with these parameters
The example below demonstrates setting up a 2-of-3 multisig account with different types of signers:
// 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:
// 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);
}
The _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:
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
To avoid duplicate signers, the contract uses |