MultiSig Timelock

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

Owner-only proposal breaks the “any signer can propose” design

Author Revealed upon completion

Root + Impact

onlyOwner restriction contradicts intended any signer can propose design

Description

The project documentation states that any signer should be able to propose a transaction. However, the implemented code enforces an onlyOwner restriction, which prevents non-owner signers from creating proposals.
This creates a mismatch between the intended functionality the actual code. Signers who believe they can propose transactions will be unable to do so, centralizing all proposal authority to the owner rather than the signer group.

function proposeTransaction(address to, uint256 value, bytes calldata data)
external
nonReentrant
noneZeroAddress(to)
onlyOwner
returns (uint256)
{
return _proposeTransaction(to, value, data);
}

Risk

Likelihood:

high

Impact:

high because mismatch between the intended functionality the actual code, effectively bricking the contract’s usage and goal of multisig permissioning.

Proof of Concept

Assume two signers:

  • Owner = 0xABC...

  • Signer (non-owner) = 0xDEF...

The non-owner signer attempts to call:

Assume two signers:

  • Owner = 0xABC...

  • Signer (non-owner) = 0xDEF...

The non-owner signer attempts to call:

proposeTransaction(0x1234..., 1 ether, "0x");

This transaction will revert with an onlyOwner error because the caller does not satisfy the onlyOwner modifier. This demonstrates that, contrary to the intended multisig design, non-owner signers are not allowed to propose transactions.

Expected behavior (per docs): Any signer can propose a transaction.
Actual behavior (per code): Only the owner can propose a transaction

Recommended Mitigation

Update the access control to be signer-based instead of owner-only. For example, replace the onlyOwner modifier with a role check for signers:

function proposeTransaction(address to, uint256 value, bytes calldata data)
external
nonReentrant
noneZeroAddress(to)
onlyRole(SIGNING_ROLE)
returns (uint256)
{
return _proposeTransaction(to, value, data);
}

Support

FAQs

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

Give us feedback!