Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/public-crabs-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`MultiSignerERC7913Weighted`: Extension of `MultiSignerERC7913` that supports assigning different weights to each signer, enabling more flexible governance schemes.
31 changes: 28 additions & 3 deletions contracts/mocks/account/AccountMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {SignerRSA} from "../../utils/cryptography/signers/SignerRSA.sol";
import {SignerERC7702} from "../../utils/cryptography/signers/SignerERC7702.sol";
import {SignerERC7913} from "../../utils/cryptography/signers/SignerERC7913.sol";
import {MultiSignerERC7913} from "../../utils/cryptography/signers/MultiSignerERC7913.sol";
import {MultiSignerERC7913Weighted} from "../../utils/cryptography/signers/MultiSignerERC7913Weighted.sol";

abstract contract AccountMock is Account, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
/// Validates a user operation with a boolean signature.
Expand Down Expand Up @@ -139,6 +140,21 @@ abstract contract AccountERC7579HookedMock is AccountERC7579Hooked {
}
}

abstract contract AccountERC7913Mock is Account, SignerERC7913, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
constructor(bytes memory _signer) {
_setSigner(_signer);
}

/// @inheritdoc ERC7821
function _erc7821AuthorizedExecutor(
address caller,
bytes32 mode,
bytes calldata executionData
) internal view virtual override returns (bool) {
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
}
}

abstract contract AccountMultiSignerMock is Account, MultiSignerERC7913, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
constructor(bytes[] memory signers, uint64 threshold) {
_addSigners(signers);
Expand All @@ -155,9 +171,18 @@ abstract contract AccountMultiSignerMock is Account, MultiSignerERC7913, ERC7739
}
}

abstract contract AccountERC7913Mock is Account, SignerERC7913, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
constructor(bytes memory _signer) {
_setSigner(_signer);
abstract contract AccountMultiSignerWeightedMock is
Account,
MultiSignerERC7913Weighted,
ERC7739,
ERC7821,
ERC721Holder,
ERC1155Holder
{
constructor(bytes[] memory signers, uint64[] memory weights, uint64 threshold) {
_addSigners(signers);
_setSignerWeights(signers, weights);
_setThreshold(threshold);
}

/// @inheritdoc ERC7821
Expand Down
4 changes: 3 additions & 1 deletion contracts/utils/cryptography/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ A collection of contracts and libraries that implement various signature validat
* {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from {ERC7739Utils}.
* {SignerECDSA}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms.
* {SignerERC7702}: Implementation of {AbstractSigner} that validates signatures using the contract's own address as the signer, useful for delegated accounts following EIP-7702.
* {SignerERC7913}, {MultiSignerERC7913}: Implementations of {AbstractSigner} that validate signatures based on ERC-7913. Including a simple multisignature scheme.
* {SignerERC7913}, {MultiSignerERC7913}, {MultiSignerERC7913Weighted}: Implementations of {AbstractSigner} that validate signatures based on ERC-7913. Including a simple and weighted multisignature scheme.
* {ERC7913P256Verifier}, {ERC7913RSAVerifier}: Ready to use ERC-7913 signature verifiers for P256 and RSA keys.

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

{{MultiSignerERC7913}}

{{MultiSignerERC7913Weighted}}

== Verifiers

{{ERC7913P256Verifier}}
Expand Down
7 changes: 5 additions & 2 deletions contracts/utils/cryptography/signers/MultiSignerERC7913.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import {EnumerableSet} from "../../structs/EnumerableSet.sol";
*
* ```solidity
* contract MyMultiSignerAccount is Account, MultiSignerERC7913, Initializable {
* constructor() EIP712("MyMultiSignerAccount", "1") {}
*
* function initialize(bytes[] memory signers, uint64 threshold) public initializer {
* _addSigners(signers);
* _setThreshold(threshold);
Expand Down Expand Up @@ -84,6 +82,11 @@ abstract contract MultiSignerERC7913 is AbstractSigner {
return _signers.values(start, end);
}

/// @dev Returns the number of authorized signers
function getSignerCount() public view virtual returns (uint256) {
return _signers.length();
}

/// @dev Returns whether the `signer` is an authorized signer.
function isSigner(bytes memory signer) public view virtual returns (bool) {
return _signers.contains(signer);
Expand Down
184 changes: 184 additions & 0 deletions contracts/utils/cryptography/signers/MultiSignerERC7913Weighted.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {SafeCast} from "../../math/SafeCast.sol";
import {MultiSignerERC7913} from "./MultiSignerERC7913.sol";

/**
* @dev Extension of {MultiSignerERC7913} that supports weighted signatures.
*
* This contract allows assigning different weights to each signer, enabling more
* flexible governance schemes. For example, some signers could have higher weight
* than others, allowing for weighted voting or prioritized authorization.
*
* Example of usage:
*
* ```solidity
* contract MyWeightedMultiSignerAccount is Account, MultiSignerERC7913Weighted, Initializable {
* function initialize(bytes[] memory signers, uint64[] memory weights, uint64 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(uint64 threshold) public onlyEntryPointOrSelf {
* _setThreshold(threshold);
* }
*
* function setSignerWeights(bytes[] memory signers, uint64[] memory weights) public onlyEntryPointOrSelf {
* _setSignerWeights(signers, weights);
* }
* }
* ```
*
* IMPORTANT: When setting a threshold value, ensure it 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). See {signerWeight}.
*/
abstract contract MultiSignerERC7913Weighted is MultiSignerERC7913 {
using SafeCast for *;

// Sum of all the extra weights of all signers. Storage packed with `MultiSignerERC7913._threshold`
uint64 private _totalExtraWeight;

// Mapping from signer to extraWeight (in addition to all authorized signers having weight 1)
mapping(bytes signer => uint64) private _extraWeights;

/**
* @dev Emitted when a signer's weight is changed.
*
* NOTE: Not emitted in {_addSigners} or {_removeSigners}. Indexers must rely on {ERC7913SignerAdded}
* and {ERC7913SignerRemoved} to index a default weight of 1. See {signerWeight}.
*/
event ERC7913SignerWeightChanged(bytes indexed signer, uint64 weight);

/// @dev Thrown when a signer's weight is invalid.
error MultiSignerERC7913WeightedInvalidWeight(bytes signer, uint64 weight);

/// @dev Thrown when the arrays lengths don't match. See {_setSignerWeights}.
error MultiSignerERC7913WeightedMismatchedLength();

/// @dev Gets the weight of a signer. Returns 0 if the signer is not authorized.
function signerWeight(bytes memory signer) public view virtual returns (uint64) {
unchecked {
// Safe cast, _setSignerWeights guarantees 1+_extraWeights is a uint64
return uint64(isSigner(signer).toUint() * (1 + _extraWeights[signer]));
}
}

/// @dev Gets the total weight of all signers.
function totalWeight() public view virtual returns (uint64) {
return (getSignerCount() + _totalExtraWeight).toUint64();
}

/**
* @dev Sets weights for multiple signers at once. Internal version without access control.
*
* Requirements:
*
* * `signers` and `weights` arrays must have the same length. Reverts with {MultiSignerERC7913WeightedMismatchedLength} on mismatch.
* * Each signer must exist in the set of authorized signers. Otherwise reverts with {MultiSignerERC7913NonexistentSigner}
* * Each weight must be greater than 0. Otherwise reverts with {MultiSignerERC7913WeightedInvalidWeight}
* * See {_validateReachableThreshold} for the threshold validation.
*
* Emits {ERC7913SignerWeightChanged} for each signer.
*/
function _setSignerWeights(bytes[] memory signers, uint64[] memory weights) internal virtual {
require(signers.length == weights.length, MultiSignerERC7913WeightedMismatchedLength());

uint256 extraWeightAdded = 0;
uint256 extraWeightRemoved = 0;
for (uint256 i = 0; i < signers.length; ++i) {
bytes memory signer = signers[i];
uint64 weight = weights[i];

require(isSigner(signer), MultiSignerERC7913NonexistentSigner(signer));
require(weight > 0, MultiSignerERC7913WeightedInvalidWeight(signer, weight));

unchecked {
// Overflow impossible: weight values are bounded by uint64 and economic constraints
extraWeightRemoved += _extraWeights[signer];
extraWeightAdded += _extraWeights[signer] = weight - 1;
}

emit ERC7913SignerWeightChanged(signer, weight);
}
unchecked {
// Safe from underflow: `extraWeightRemoved` is bounded by `_totalExtraWeight` by construction
// and weight values are bounded by uint64 and economic constraints
_totalExtraWeight = (uint256(_totalExtraWeight) + extraWeightAdded - extraWeightRemoved).toUint64();
}
_validateReachableThreshold();
}

/**
* @dev See {MultiSignerERC7913-_removeSigners}.
*
* Just like {_addSigners}, this function does not emit {ERC7913SignerWeightChanged} events. The
* {ERC7913SignerRemoved} event emitted by {MultiSignerERC7913-_removeSigners} is enough to track weights here.
*/
function _removeSigners(bytes[] memory signers) internal virtual override {
// Clean up weights for removed signers
//
// The `extraWeightRemoved` is bounded by `_totalExtraWeight`. The `super._removeSigners` function will revert
// if the signers array contains any duplicates, ensuring each signer's weight is only counted once. Since
// `_totalExtraWeight` is stored as a `uint64`, the final subtraction operation is also safe.
unchecked {
uint64 extraWeightRemoved = 0;
for (uint256 i = 0; i < signers.length; ++i) {
bytes memory signer = signers[i];

extraWeightRemoved += _extraWeights[signer];
delete _extraWeights[signer];
}
_totalExtraWeight -= extraWeightRemoved;
}
super._removeSigners(signers);
}

/**
* @dev Sets the threshold for the multisignature operation. Internal version without access control.
*
* Requirements:
*
* * The {totalWeight} must be `>=` the {threshold}. Otherwise reverts with {MultiSignerERC7913UnreachableThreshold}
*
* NOTE: This function intentionally does not call `super._validateReachableThreshold` because the base implementation
* assumes each signer has a weight of 1, which is a subset of this weighted implementation. Consider that multiple
* implementations of this function may exist in the contract, so important side effects may be missed
* depending on the linearization order.
*/
function _validateReachableThreshold() internal view virtual override {
uint64 weight = totalWeight();
uint64 currentThreshold = threshold();
require(weight >= currentThreshold, MultiSignerERC7913UnreachableThreshold(weight, currentThreshold));
}

/**
* @dev Validates that the total weight of signers meets the threshold requirement.
*
* NOTE: This function intentionally does not call `super._validateThreshold` because the base implementation
* assumes each signer has a weight of 1, which is a subset of this weighted implementation. Consider that multiple
* implementations of this function may exist in the contract, so important side effects may be missed
* depending on the linearization order.
*/
function _validateThreshold(bytes[] memory signers) internal view virtual override returns (bool) {
unchecked {
uint64 weight = 0;
for (uint256 i = 0; i < signers.length; ++i) {
// Overflow impossible: weight values are bounded by uint64 and economic constraints
weight += signerWeight(signers[i]);
}
return weight >= threshold();
}
}
}
Loading