Skip to content

Commit 7be5dde

Browse files
authored
Add MultiSignerERC7913Weighted (#5718)
1 parent 5c79432 commit 7be5dde

File tree

6 files changed

+517
-6
lines changed

6 files changed

+517
-6
lines changed

.changeset/public-crabs-heal.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`MultiSignerERC7913Weighted`: Extension of `MultiSignerERC7913` that supports assigning different weights to each signer, enabling more flexible governance schemes.

contracts/mocks/account/AccountMock.sol

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {SignerRSA} from "../../utils/cryptography/signers/SignerRSA.sol";
1919
import {SignerERC7702} from "../../utils/cryptography/signers/SignerERC7702.sol";
2020
import {SignerERC7913} from "../../utils/cryptography/signers/SignerERC7913.sol";
2121
import {MultiSignerERC7913} from "../../utils/cryptography/signers/MultiSignerERC7913.sol";
22+
import {MultiSignerERC7913Weighted} from "../../utils/cryptography/signers/MultiSignerERC7913Weighted.sol";
2223

2324
abstract contract AccountMock is Account, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
2425
/// Validates a user operation with a boolean signature.
@@ -139,6 +140,21 @@ abstract contract AccountERC7579HookedMock is AccountERC7579Hooked {
139140
}
140141
}
141142

143+
abstract contract AccountERC7913Mock is Account, SignerERC7913, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
144+
constructor(bytes memory _signer) {
145+
_setSigner(_signer);
146+
}
147+
148+
/// @inheritdoc ERC7821
149+
function _erc7821AuthorizedExecutor(
150+
address caller,
151+
bytes32 mode,
152+
bytes calldata executionData
153+
) internal view virtual override returns (bool) {
154+
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
155+
}
156+
}
157+
142158
abstract contract AccountMultiSignerMock is Account, MultiSignerERC7913, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
143159
constructor(bytes[] memory signers, uint64 threshold) {
144160
_addSigners(signers);
@@ -155,9 +171,18 @@ abstract contract AccountMultiSignerMock is Account, MultiSignerERC7913, ERC7739
155171
}
156172
}
157173

158-
abstract contract AccountERC7913Mock is Account, SignerERC7913, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
159-
constructor(bytes memory _signer) {
160-
_setSigner(_signer);
174+
abstract contract AccountMultiSignerWeightedMock is
175+
Account,
176+
MultiSignerERC7913Weighted,
177+
ERC7739,
178+
ERC7821,
179+
ERC721Holder,
180+
ERC1155Holder
181+
{
182+
constructor(bytes[] memory signers, uint64[] memory weights, uint64 threshold) {
183+
_addSigners(signers);
184+
_setSignerWeights(signers, weights);
185+
_setThreshold(threshold);
161186
}
162187

163188
/// @inheritdoc ERC7821

contracts/utils/cryptography/README.adoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ A collection of contracts and libraries that implement various signature validat
1717
* {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from {ERC7739Utils}.
1818
* {SignerECDSA}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms.
1919
* {SignerERC7702}: Implementation of {AbstractSigner} that validates signatures using the contract's own address as the signer, useful for delegated accounts following EIP-7702.
20-
* {SignerERC7913}, {MultiSignerERC7913}: Implementations of {AbstractSigner} that validate signatures based on ERC-7913. Including a simple multisignature scheme.
20+
* {SignerERC7913}, {MultiSignerERC7913}, {MultiSignerERC7913Weighted}: Implementations of {AbstractSigner} that validate signatures based on ERC-7913. Including a simple and weighted multisignature scheme.
2121
* {ERC7913P256Verifier}, {ERC7913RSAVerifier}: Ready to use ERC-7913 signature verifiers for P256 and RSA keys.
2222

2323
== Utils
@@ -58,6 +58,8 @@ A collection of contracts and libraries that implement various signature validat
5858

5959
{{MultiSignerERC7913}}
6060

61+
{{MultiSignerERC7913Weighted}}
62+
6163
== Verifiers
6264

6365
{{ERC7913P256Verifier}}

contracts/utils/cryptography/signers/MultiSignerERC7913.sol

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ import {EnumerableSet} from "../../structs/EnumerableSet.sol";
1818
*
1919
* ```solidity
2020
* contract MyMultiSignerAccount is Account, MultiSignerERC7913, Initializable {
21-
* constructor() EIP712("MyMultiSignerAccount", "1") {}
22-
*
2321
* function initialize(bytes[] memory signers, uint64 threshold) public initializer {
2422
* _addSigners(signers);
2523
* _setThreshold(threshold);
@@ -84,6 +82,11 @@ abstract contract MultiSignerERC7913 is AbstractSigner {
8482
return _signers.values(start, end);
8583
}
8684

85+
/// @dev Returns the number of authorized signers
86+
function getSignerCount() public view virtual returns (uint256) {
87+
return _signers.length();
88+
}
89+
8790
/// @dev Returns whether the `signer` is an authorized signer.
8891
function isSigner(bytes memory signer) public view virtual returns (bool) {
8992
return _signers.contains(signer);
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {SafeCast} from "../../math/SafeCast.sol";
6+
import {MultiSignerERC7913} from "./MultiSignerERC7913.sol";
7+
8+
/**
9+
* @dev Extension of {MultiSignerERC7913} that supports weighted signatures.
10+
*
11+
* This contract allows assigning different weights to each signer, enabling more
12+
* flexible governance schemes. For example, some signers could have higher weight
13+
* than others, allowing for weighted voting or prioritized authorization.
14+
*
15+
* Example of usage:
16+
*
17+
* ```solidity
18+
* contract MyWeightedMultiSignerAccount is Account, MultiSignerERC7913Weighted, Initializable {
19+
* function initialize(bytes[] memory signers, uint64[] memory weights, uint64 threshold) public initializer {
20+
* _addSigners(signers);
21+
* _setSignerWeights(signers, weights);
22+
* _setThreshold(threshold);
23+
* }
24+
*
25+
* function addSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
26+
* _addSigners(signers);
27+
* }
28+
*
29+
* function removeSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
30+
* _removeSigners(signers);
31+
* }
32+
*
33+
* function setThreshold(uint64 threshold) public onlyEntryPointOrSelf {
34+
* _setThreshold(threshold);
35+
* }
36+
*
37+
* function setSignerWeights(bytes[] memory signers, uint64[] memory weights) public onlyEntryPointOrSelf {
38+
* _setSignerWeights(signers, weights);
39+
* }
40+
* }
41+
* ```
42+
*
43+
* IMPORTANT: When setting a threshold value, ensure it matches the scale used for signer weights.
44+
* For example, if signers have weights like 1, 2, or 3, then a threshold of 4 would require at
45+
* least two signers (e.g., one with weight 1 and one with weight 3). See {signerWeight}.
46+
*/
47+
abstract contract MultiSignerERC7913Weighted is MultiSignerERC7913 {
48+
using SafeCast for *;
49+
50+
// Sum of all the extra weights of all signers. Storage packed with `MultiSignerERC7913._threshold`
51+
uint64 private _totalExtraWeight;
52+
53+
// Mapping from signer to extraWeight (in addition to all authorized signers having weight 1)
54+
mapping(bytes signer => uint64) private _extraWeights;
55+
56+
/**
57+
* @dev Emitted when a signer's weight is changed.
58+
*
59+
* NOTE: Not emitted in {_addSigners} or {_removeSigners}. Indexers must rely on {ERC7913SignerAdded}
60+
* and {ERC7913SignerRemoved} to index a default weight of 1. See {signerWeight}.
61+
*/
62+
event ERC7913SignerWeightChanged(bytes indexed signer, uint64 weight);
63+
64+
/// @dev Thrown when a signer's weight is invalid.
65+
error MultiSignerERC7913WeightedInvalidWeight(bytes signer, uint64 weight);
66+
67+
/// @dev Thrown when the arrays lengths don't match. See {_setSignerWeights}.
68+
error MultiSignerERC7913WeightedMismatchedLength();
69+
70+
/// @dev Gets the weight of a signer. Returns 0 if the signer is not authorized.
71+
function signerWeight(bytes memory signer) public view virtual returns (uint64) {
72+
unchecked {
73+
// Safe cast, _setSignerWeights guarantees 1+_extraWeights is a uint64
74+
return uint64(isSigner(signer).toUint() * (1 + _extraWeights[signer]));
75+
}
76+
}
77+
78+
/// @dev Gets the total weight of all signers.
79+
function totalWeight() public view virtual returns (uint64) {
80+
return (getSignerCount() + _totalExtraWeight).toUint64();
81+
}
82+
83+
/**
84+
* @dev Sets weights for multiple signers at once. Internal version without access control.
85+
*
86+
* Requirements:
87+
*
88+
* * `signers` and `weights` arrays must have the same length. Reverts with {MultiSignerERC7913WeightedMismatchedLength} on mismatch.
89+
* * Each signer must exist in the set of authorized signers. Otherwise reverts with {MultiSignerERC7913NonexistentSigner}
90+
* * Each weight must be greater than 0. Otherwise reverts with {MultiSignerERC7913WeightedInvalidWeight}
91+
* * See {_validateReachableThreshold} for the threshold validation.
92+
*
93+
* Emits {ERC7913SignerWeightChanged} for each signer.
94+
*/
95+
function _setSignerWeights(bytes[] memory signers, uint64[] memory weights) internal virtual {
96+
require(signers.length == weights.length, MultiSignerERC7913WeightedMismatchedLength());
97+
98+
uint256 extraWeightAdded = 0;
99+
uint256 extraWeightRemoved = 0;
100+
for (uint256 i = 0; i < signers.length; ++i) {
101+
bytes memory signer = signers[i];
102+
uint64 weight = weights[i];
103+
104+
require(isSigner(signer), MultiSignerERC7913NonexistentSigner(signer));
105+
require(weight > 0, MultiSignerERC7913WeightedInvalidWeight(signer, weight));
106+
107+
unchecked {
108+
// Overflow impossible: weight values are bounded by uint64 and economic constraints
109+
extraWeightRemoved += _extraWeights[signer];
110+
extraWeightAdded += _extraWeights[signer] = weight - 1;
111+
}
112+
113+
emit ERC7913SignerWeightChanged(signer, weight);
114+
}
115+
unchecked {
116+
// Safe from underflow: `extraWeightRemoved` is bounded by `_totalExtraWeight` by construction
117+
// and weight values are bounded by uint64 and economic constraints
118+
_totalExtraWeight = (uint256(_totalExtraWeight) + extraWeightAdded - extraWeightRemoved).toUint64();
119+
}
120+
_validateReachableThreshold();
121+
}
122+
123+
/**
124+
* @dev See {MultiSignerERC7913-_removeSigners}.
125+
*
126+
* Just like {_addSigners}, this function does not emit {ERC7913SignerWeightChanged} events. The
127+
* {ERC7913SignerRemoved} event emitted by {MultiSignerERC7913-_removeSigners} is enough to track weights here.
128+
*/
129+
function _removeSigners(bytes[] memory signers) internal virtual override {
130+
// Clean up weights for removed signers
131+
//
132+
// The `extraWeightRemoved` is bounded by `_totalExtraWeight`. The `super._removeSigners` function will revert
133+
// if the signers array contains any duplicates, ensuring each signer's weight is only counted once. Since
134+
// `_totalExtraWeight` is stored as a `uint64`, the final subtraction operation is also safe.
135+
unchecked {
136+
uint64 extraWeightRemoved = 0;
137+
for (uint256 i = 0; i < signers.length; ++i) {
138+
bytes memory signer = signers[i];
139+
140+
extraWeightRemoved += _extraWeights[signer];
141+
delete _extraWeights[signer];
142+
}
143+
_totalExtraWeight -= extraWeightRemoved;
144+
}
145+
super._removeSigners(signers);
146+
}
147+
148+
/**
149+
* @dev Sets the threshold for the multisignature operation. Internal version without access control.
150+
*
151+
* Requirements:
152+
*
153+
* * The {totalWeight} must be `>=` the {threshold}. Otherwise reverts with {MultiSignerERC7913UnreachableThreshold}
154+
*
155+
* NOTE: This function intentionally does not call `super._validateReachableThreshold` because the base implementation
156+
* assumes each signer has a weight of 1, which is a subset of this weighted implementation. Consider that multiple
157+
* implementations of this function may exist in the contract, so important side effects may be missed
158+
* depending on the linearization order.
159+
*/
160+
function _validateReachableThreshold() internal view virtual override {
161+
uint64 weight = totalWeight();
162+
uint64 currentThreshold = threshold();
163+
require(weight >= currentThreshold, MultiSignerERC7913UnreachableThreshold(weight, currentThreshold));
164+
}
165+
166+
/**
167+
* @dev Validates that the total weight of signers meets the threshold requirement.
168+
*
169+
* NOTE: This function intentionally does not call `super._validateThreshold` because the base implementation
170+
* assumes each signer has a weight of 1, which is a subset of this weighted implementation. Consider that multiple
171+
* implementations of this function may exist in the contract, so important side effects may be missed
172+
* depending on the linearization order.
173+
*/
174+
function _validateThreshold(bytes[] memory signers) internal view virtual override returns (bool) {
175+
unchecked {
176+
uint64 weight = 0;
177+
for (uint256 i = 0; i < signers.length; ++i) {
178+
// Overflow impossible: weight values are bounded by uint64 and economic constraints
179+
weight += signerWeight(signers[i]);
180+
}
181+
return weight >= threshold();
182+
}
183+
}
184+
}

0 commit comments

Comments
 (0)