MultiSig Timelock

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

Signers are documented as able to propose transactions, but implementation restricts proposals to owner only

Author Revealed upon completion

Description

  • Normal behavior: According to the About section, any account holding the SIGNING_ROLE is expected to be able to propose new transactions. The documentation explicitly states that transaction proposal permissions are tied to the signer role, allowing signers to independently initiate proposals as part of the multisig workflow.

  • Problem: In the actual implementation, proposeTransaction is restricted by the onlyOwner modifier, preventing signers (who are not the owner) from proposing transactions. As a result, signers cannot perform an action that is explicitly described as part of their role in the documentation, creating a mismatch between documented behavior and contract logic.

function proposeTransaction(address to, uint256 value, bytes calldata data)
external
nonReentrant
noneZeroAddress(to)
@> onlyOwner // only owner can propose a tx
returns (uint256)
{
return _proposeTransaction(to, value, data);
}

Risk

Likelihood:

  • This behavior occurs whenever a non-owner signer attempts to propose a transaction, as proposeTransaction is unconditionally restricted by onlyOwner in the implementation.

  • The mismatch is deterministic and affects all signer accounts by design, since the documented signer permissions are never actually granted in the contract logic.

Impact:

  • Signers are unable to perform an action explicitly described as part of their role, leading to confusion and incorrect assumptions about the multisig’s governance model.

  • Users and integrators relying on the documentation may incorrectly assume decentralized proposal capabilities, while in practice transaction initiation is centralized under the owner.

Proof of Concept

function test_NonOwnerSignerCannotProposeTransaction() public {
// Grant signing role to non-owner signer
multiSigTimelock.grantSigningRole(SIGNER_TWO);
// Check signers count
uint256 signersCount = multiSigTimelock.getSignerCount();
assertEq(signersCount, 2);
// User can`t propose a tx
vm.prank(SIGNER_TWO);
vm.expectRevert();
multiSigTimelock.proposeTransaction(SPENDER_ONE, OWNER_BALANCE_ONE, hex"");
}

Recommended Mitigation

Either update the documentation to reflect that only the owner can propose transactions, or

  • align implementation with documentation by allowing signers to propose (replace onlyOwner with onlyRole(SIGNING_ROLE) on proposeTransaction, or permit both owner and signers).

function proposeTransaction(address to, uint256 value, bytes calldata data)
external
nonReentrant
noneZeroAddress(to)
- onlyOwner
+ 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!