multisig.adoc 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. = Multisig Account
  2. 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.
  3. 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.
  4. == Beyond Standard Signature Verification
  5. 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:
  6. 1. It assumes the signer has an EVM address
  7. 2. It treats the signer as a single identity
  8. This becomes problematic when implementing multisig accounts where:
  9. * You may want to use signers that don't have EVM addresses (like keys from hardware devices)
  10. * Each signer needs to be individually verified rather than treated as a collective identity
  11. * You need a threshold system to determine when enough valid signatures are present
  12. 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.
  13. == ERC-7913 Signers
  14. 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:
  15. === SignerERC7913
  16. 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`.
  17. [source,solidity]
  18. ----
  19. // contracts/MyAccountERC7913.sol
  20. // SPDX-License-Identifier: MIT
  21. pragma solidity ^0.8.24;
  22. import {Account} from "@openzeppelin/community-contracts/account/Account.sol";
  23. import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
  24. import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
  25. import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
  26. import {ERC7739} from "@openzeppelin/community-contracts/utils/cryptography/signers/ERC7739.sol";
  27. import {ERC7821} from "@openzeppelin/community-contracts/account/extensions/ERC7821.sol";
  28. import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
  29. import {SignerERC7913} from "@openzeppelin/community-contracts/utils/cryptography/signers/SignerERC7913.sol";
  30. contract MyAccountERC7913 is Account, SignerERC7913, ERC7739, ERC7821, ERC721Holder, ERC1155Holder, Initializable {
  31. constructor() EIP712("MyAccount7913", "1") {}
  32. function initialize(bytes memory signer) public initializer {
  33. _setSigner(signer);
  34. }
  35. function setSigner(bytes memory signer) public onlyEntryPointOrSelf {
  36. _setSigner(signer);
  37. }
  38. /// @dev Allows the entry point as an authorized executor.
  39. function _erc7821AuthorizedExecutor(
  40. address caller,
  41. bytes32 mode,
  42. bytes calldata executionData
  43. ) internal view virtual override returns (bool) {
  44. return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
  45. }
  46. }
  47. ----
  48. WARNING: Leaving an account uninitialized may leave it unusable since no public key was associated with it.
  49. === MultiSignerERC7913
  50. The xref:api:utils/cryptography.adoc#MultiSignerERC7913[`MultiSignerERC7913`] contract extends this concept to support multiple signers with a threshold-based signature verification system.
  51. [source,solidity]
  52. ----
  53. // contracts/MyAccountMultiSigner.sol
  54. // SPDX-License-Identifier: MIT
  55. pragma solidity ^0.8.27;
  56. import {Account} from "@openzeppelin/community-contracts/account/Account.sol";
  57. import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
  58. import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
  59. import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
  60. import {ERC7739} from "@openzeppelin/community-contracts/utils/cryptography/signers/ERC7739.sol";
  61. import {ERC7821} from "@openzeppelin/community-contracts/account/extensions/ERC7821.sol";
  62. import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
  63. import {MultiSignerERC7913} from "@openzeppelin/community-contracts/utils/cryptography/signers/MultiSignerERC7913.sol";
  64. contract MyAccountMultiSigner is
  65. Account,
  66. MultiSignerERC7913,
  67. ERC7739,
  68. ERC7821,
  69. ERC721Holder,
  70. ERC1155Holder,
  71. Initializable
  72. {
  73. constructor() EIP712("MyAccountMultiSigner", "1") {}
  74. function initialize(bytes[] memory signers, uint256 threshold) public initializer {
  75. _addSigners(signers);
  76. _setThreshold(threshold);
  77. }
  78. function addSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
  79. _addSigners(signers);
  80. }
  81. function removeSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
  82. _removeSigners(signers);
  83. }
  84. function setThreshold(uint256 threshold) public onlyEntryPointOrSelf {
  85. _setThreshold(threshold);
  86. }
  87. /// @dev Allows the entry point as an authorized executor.
  88. function _erc7821AuthorizedExecutor(
  89. address caller,
  90. bytes32 mode,
  91. bytes calldata executionData
  92. ) internal view virtual override returns (bool) {
  93. return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
  94. }
  95. }
  96. ----
  97. This implementation is ideal for standard multisig setups where each signer has equal authority, and a fixed number of approvals is required.
  98. 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.
  99. 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.
  100. 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.
  101. === MultiSignerERC7913Weighted
  102. For more sophisticated governance structures, the xref:api:utils/cryptography.adoc#MultiSignerERC7913Weighted[`MultiSignerERC7913Weighted`] contract extends `MultiSignerERC7913` by assigning different weights to each signer.
  103. [source,solidity]
  104. ----
  105. // contracts/MyAccountMultiSignerWeighted.sol
  106. // SPDX-License-Identifier: MIT
  107. pragma solidity ^0.8.27;
  108. import {Account} from "@openzeppelin/community-contracts/account/Account.sol";
  109. import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
  110. import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
  111. import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
  112. import {ERC7739} from "@openzeppelin/community-contracts/utils/cryptography/signers/ERC7739.sol";
  113. import {ERC7821} from "@openzeppelin/community-contracts/account/extensions/ERC7821.sol";
  114. import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
  115. import {MultiSignerERC7913Weighted} from "@openzeppelin/community-contracts/utils/cryptography/signers/MultiSignerERC7913Weighted.sol";
  116. contract MyAccountMultiSignerWeighted is
  117. Account,
  118. MultiSignerERC7913Weighted,
  119. ERC7739,
  120. ERC7821,
  121. ERC721Holder,
  122. ERC1155Holder,
  123. Initializable
  124. {
  125. constructor() EIP712("MyAccountMultiSignerWeighted", "1") {}
  126. function initialize(bytes[] memory signers, uint256[] memory weights, uint256 threshold) public initializer {
  127. _addSigners(signers);
  128. _setSignerWeights(signers, weights);
  129. _setThreshold(threshold);
  130. }
  131. function addSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
  132. _addSigners(signers);
  133. }
  134. function removeSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
  135. _removeSigners(signers);
  136. }
  137. function setThreshold(uint256 threshold) public onlyEntryPointOrSelf {
  138. _setThreshold(threshold);
  139. }
  140. function setSignerWeights(bytes[] memory signers, uint256[] memory weights) public onlyEntryPointOrSelf {
  141. _setSignerWeights(signers, weights);
  142. }
  143. /// @dev Allows the entry point as an authorized executor.
  144. function _erc7821AuthorizedExecutor(
  145. address caller,
  146. bytes32 mode,
  147. bytes calldata executionData
  148. ) internal view virtual override returns (bool) {
  149. return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
  150. }
  151. }
  152. ----
  153. This implementation is perfect for scenarios where different signers should have varying levels of authority, such as:
  154. * Board members with different voting powers
  155. * Organizational structures with hierarchical decision-making
  156. * Hybrid governance systems combining core team and community members
  157. * Execution setups like "social recovery" where you trust particular guardians more than others
  158. 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.
  159. 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).
  160. == Setting Up a Multisig Account
  161. To create a multisig account, you need to:
  162. 1. Define your signers
  163. 2. Determine your threshold
  164. 3. Initialize your account with these parameters
  165. The example below demonstrates setting up a 2-of-3 multisig account with different types of signers:
  166. [source,solidity]
  167. ----
  168. // Example setup code
  169. function setupMultisigAccount() external {
  170. // Create signers using different types of keys
  171. bytes memory ecdsaSigner = alice; // EOA address (20 bytes)
  172. // P256 signer with format: verifier || pubKey
  173. bytes memory p256Signer = abi.encodePacked(
  174. p256Verifier,
  175. bobP256PublicKeyX,
  176. bobP256PublicKeyY
  177. );
  178. // RSA signer with format: verifier || pubKey
  179. bytes memory rsaSigner = abi.encodePacked(
  180. rsaVerifier,
  181. abi.encode(charlieRSAPublicKeyE, charlieRSAPublicKeyN)
  182. );
  183. // Create array of signers
  184. bytes[] memory signers = new bytes[](3);
  185. signers[0] = ecdsaSigner;
  186. signers[1] = p256Signer;
  187. signers[2] = rsaSigner;
  188. // Set threshold to 2 (2-of-3 multisig)
  189. uint256 threshold = 2;
  190. // Initialize the account
  191. myMultisigAccount.initialize(signers, threshold);
  192. }
  193. ----
  194. For a weighted multisig, you would also specify weights:
  195. [source,solidity]
  196. ----
  197. // Example setup for weighted multisig
  198. function setupWeightedMultisigAccount() external {
  199. // Create array of signers (same as above)
  200. bytes[] memory signers = new bytes[](3);
  201. signers[0] = ecdsaSigner;
  202. signers[1] = p256Signer;
  203. signers[2] = rsaSigner;
  204. // Assign weights to signers (Alice:1, Bob:2, Charlie:3)
  205. uint256[] memory weights = new uint256[](3);
  206. weights[0] = 1;
  207. weights[1] = 2;
  208. weights[2] = 3;
  209. // Set threshold to 4 (requires at least Bob+Charlie or all three)
  210. uint256 threshold = 4;
  211. // Initialize the weighted account
  212. myWeightedMultisigAccount.initialize(signers, weights, threshold);
  213. }
  214. ----
  215. 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.
  216. 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.
  217. === Signature Format
  218. The multisig signature is encoded as:
  219. [source,solidity]
  220. ----
  221. abi.encode(
  222. bytes[] signers, // Array of signers sorted by `keccak256`
  223. bytes[] signatures // Array of signatures corresponding to each signer
  224. )
  225. ----
  226. Where:
  227. * `signers` is an array of the signers participating in this particular signature
  228. * `signatures` is an array of the individual signatures corresponding to each signer
  229. [NOTE]
  230. ====
  231. 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.
  232. ====