Add MultiSignerERC7913Weighted (#5718)

This commit is contained in:
Ernesto García
2025-06-12 12:09:50 -06:00
committed by GitHub
parent 5c79432e40
commit 7be5dde82d
6 changed files with 517 additions and 6 deletions

View File

@ -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
@ -58,6 +58,8 @@ A collection of contracts and libraries that implement various signature validat
{{MultiSignerERC7913}}
{{MultiSignerERC7913Weighted}}
== Verifiers
{{ERC7913P256Verifier}}

View File

@ -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);
@ -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);

View File

@ -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();
}
}
}