Medium [M-1] Owner centralization over signer set (no time-lock/multisig)
Description
Owner can grant/revoke signers immediately; a compromised owner can add 3 attacker signers, remove honest ones, and pass any transaction.
Full loss of funds/governance follows owner compromise.
Recommendation: gate signer management behind multisig + timelock; or transfer ownership to governance/multisig.
- function grantSigningRole(address _account) external nonReentrant onlyOwner noneZeroAddress(
- if (s_isSigner[_account]) revert MultiSigTimelock__AccountIsAlreadyASigner();
- if (s_signerCount >= MAX_SIGNER_COUNT) revert MultiSigTimelock__MaximumSignersReached();
- s_signers[s_signerCount] = _account;
- s_isSigner[_account] = true;
- s_signerCount += 1;
- _grantRole(SIGNING_ROLE, _account);
- }
+ function revokeSigningRole(address _account) external nonReentrant onlyOwner noneZeroAddress
+ if (!s_isSigner[_account]) revert MultiSigTimelock__AccountIsNotASigner();
+ if (s_signerCount <= 1) revert MultiSigTimelock__CannotRevokeLastSigner();
...
+ s_isSigner[_account] = false;
+ _revokeRole(SIGNING_ROLE, _account);
+ }
Risk
Likelihood:
Impact:
Proof of Concept
Operational: attacker with owner key calls grantSigningRole three times to
attacker EOAs, revokeSigningRole on honest signers, then confirms and exe-
cutes any transaction.
Recommended Mitigation
- function grantSigningRole(address _account) external nonReentrant onlyOwner noneZeroAddress(
- if (s_isSigner[_account]) revert MultiSigTimelock__AccountIsAlreadyASigner();
- if (s_signerCount >= MAX_SIGNER_COUNT) revert MultiSigTimelock__MaximumSignersReached();
- s_signers[s_signerCount] = _account;
- s_isSigner[_account] = true;
- s_signerCount += 1;
- _grantRole(SIGNING_ROLE, _account);
- }
+ function revokeSigningRole(address _account) external nonReentrant onlyOwner noneZeroAddress
+ if (!s_isSigner[_account]) revert MultiSigTimelock__AccountIsNotASigner();
+ if (s_signerCount <= 1) revert MultiSigTimelock__CannotRevokeLastSigner();
...
+ s_isSigner[_account] = false;
+ _revokeRole(SIGNING_ROLE, _account);
+ }