MultiSig Timelock

First Flight #55
Beginner FriendlyWallet
100 EXP
Submission Details
Impact: medium
Likelihood: medium

Owner centralization over signer set (no time-lock/multisig)

Author Revealed upon completion

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:

  • owner is a single EOA; social-engineering or key theft lets an at-
    tacker reconfigure signers in one tx; no timelock/quorum on signer changes.

Impact:

  • attacker-controlled quorum drains funds or executes arbitrary calls;
    governance trust collapses.

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);
+ }

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!