Skip to content

Commit d2bef0e

Browse files
Transpile 82bdda813
1 parent c1b79c3 commit d2bef0e

File tree

8 files changed

+564
-12
lines changed

8 files changed

+564
-12
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/WithInit.sol

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,16 +172,23 @@ contract AccountERC7579HookedMockUpgradeableWithInit is AccountERC7579HookedMock
172172
}
173173
import "./account/AccountMockUpgradeable.sol";
174174

175+
contract AccountERC7913MockUpgradeableWithInit is AccountERC7913MockUpgradeable {
176+
constructor(bytes memory _signer) payable initializer {
177+
__AccountERC7913Mock_init(_signer);
178+
}
179+
}
180+
import "./account/AccountMockUpgradeable.sol";
181+
175182
contract AccountMultiSignerMockUpgradeableWithInit is AccountMultiSignerMockUpgradeable {
176183
constructor(bytes[] memory signers, uint64 threshold) payable initializer {
177184
__AccountMultiSignerMock_init(signers, threshold);
178185
}
179186
}
180187
import "./account/AccountMockUpgradeable.sol";
181188

182-
contract AccountERC7913MockUpgradeableWithInit is AccountERC7913MockUpgradeable {
183-
constructor(bytes memory _signer) payable initializer {
184-
__AccountERC7913Mock_init(_signer);
189+
contract AccountMultiSignerWeightedMockUpgradeableWithInit is AccountMultiSignerWeightedMockUpgradeable {
190+
constructor(bytes[] memory signers, uint64[] memory weights, uint64 threshold) payable initializer {
191+
__AccountMultiSignerWeightedMock_init(signers, weights, threshold);
185192
}
186193
}
187194
import "./account/modules/ERC7579MockUpgradeable.sol";
@@ -1375,6 +1382,13 @@ contract MultiSignerERC7913UpgradeableWithInit is MultiSignerERC7913Upgradeable
13751382
__MultiSignerERC7913_init();
13761383
}
13771384
}
1385+
import "../utils/cryptography/signers/MultiSignerERC7913WeightedUpgradeable.sol";
1386+
1387+
contract MultiSignerERC7913WeightedUpgradeableWithInit is MultiSignerERC7913WeightedUpgradeable {
1388+
constructor() payable initializer {
1389+
__MultiSignerERC7913Weighted_init();
1390+
}
1391+
}
13781392
import "../utils/cryptography/signers/SignerECDSAUpgradeable.sol";
13791393

13801394
contract SignerECDSAUpgradeableWithInit is SignerECDSAUpgradeable {

contracts/mocks/account/AccountMockUpgradeable.sol

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {SignerRSAUpgradeable} from "../../utils/cryptography/signers/SignerRSAUp
1919
import {SignerERC7702} from "@openzeppelin/contracts/utils/cryptography/signers/SignerERC7702.sol";
2020
import {SignerERC7913Upgradeable} from "../../utils/cryptography/signers/SignerERC7913Upgradeable.sol";
2121
import {MultiSignerERC7913Upgradeable} from "../../utils/cryptography/signers/MultiSignerERC7913Upgradeable.sol";
22+
import {MultiSignerERC7913WeightedUpgradeable} from "../../utils/cryptography/signers/MultiSignerERC7913WeightedUpgradeable.sol";
2223
import {Initializable} from "../../proxy/utils/Initializable.sol";
2324

2425
abstract contract AccountMockUpgradeable is Initializable, Account, ERC7739Upgradeable, ERC7821, ERC721HolderUpgradeable, ERC1155HolderUpgradeable {
@@ -175,6 +176,25 @@ abstract contract AccountERC7579HookedMockUpgradeable is Initializable, AccountE
175176
}
176177
}
177178

179+
abstract contract AccountERC7913MockUpgradeable is Initializable, Account, SignerERC7913Upgradeable, ERC7739Upgradeable, ERC7821, ERC721HolderUpgradeable, ERC1155HolderUpgradeable {
180+
function __AccountERC7913Mock_init(bytes memory _signer) internal onlyInitializing {
181+
__AccountERC7913Mock_init_unchained(_signer);
182+
}
183+
184+
function __AccountERC7913Mock_init_unchained(bytes memory _signer) internal onlyInitializing {
185+
_setSigner(_signer);
186+
}
187+
188+
/// @inheritdoc ERC7821
189+
function _erc7821AuthorizedExecutor(
190+
address caller,
191+
bytes32 mode,
192+
bytes calldata executionData
193+
) internal view virtual override returns (bool) {
194+
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
195+
}
196+
}
197+
178198
abstract contract AccountMultiSignerMockUpgradeable is Initializable, Account, MultiSignerERC7913Upgradeable, ERC7739Upgradeable, ERC7821, ERC721HolderUpgradeable, ERC1155HolderUpgradeable {
179199
function __AccountMultiSignerMock_init(bytes[] memory signers, uint64 threshold) internal onlyInitializing {
180200
__AccountMultiSignerMock_init_unchained(signers, threshold);
@@ -195,13 +215,22 @@ abstract contract AccountMultiSignerMockUpgradeable is Initializable, Account, M
195215
}
196216
}
197217

198-
abstract contract AccountERC7913MockUpgradeable is Initializable, Account, SignerERC7913Upgradeable, ERC7739Upgradeable, ERC7821, ERC721HolderUpgradeable, ERC1155HolderUpgradeable {
199-
function __AccountERC7913Mock_init(bytes memory _signer) internal onlyInitializing {
200-
__AccountERC7913Mock_init_unchained(_signer);
218+
abstract contract AccountMultiSignerWeightedMockUpgradeable is
219+
Initializable, Account,
220+
MultiSignerERC7913WeightedUpgradeable,
221+
ERC7739Upgradeable,
222+
ERC7821,
223+
ERC721HolderUpgradeable,
224+
ERC1155HolderUpgradeable
225+
{
226+
function __AccountMultiSignerWeightedMock_init(bytes[] memory signers, uint64[] memory weights, uint64 threshold) internal onlyInitializing {
227+
__AccountMultiSignerWeightedMock_init_unchained(signers, weights, threshold);
201228
}
202229

203-
function __AccountERC7913Mock_init_unchained(bytes memory _signer) internal onlyInitializing {
204-
_setSigner(_signer);
230+
function __AccountMultiSignerWeightedMock_init_unchained(bytes[] memory signers, uint64[] memory weights, uint64 threshold) internal onlyInitializing {
231+
_addSigners(signers);
232+
_setSignerWeights(signers, weights);
233+
_setThreshold(threshold);
205234
}
206235

207236
/// @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/MultiSignerERC7913Upgradeable.sol

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ import {Initializable} from "../../../proxy/utils/Initializable.sol";
1919
*
2020
* ```solidity
2121
* contract MyMultiSignerAccount is Account, MultiSignerERC7913, Initializable {
22-
* constructor() EIP712("MyMultiSignerAccount", "1") {}
23-
*
2422
* function initialize(bytes[] memory signers, uint64 threshold) public initializer {
2523
* _addSigners(signers);
2624
* _setThreshold(threshold);
@@ -103,6 +101,12 @@ abstract contract MultiSignerERC7913Upgradeable is Initializable, AbstractSigner
103101
return $._signers.values(start, end);
104102
}
105103

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

0 commit comments

Comments
 (0)