MultiSig Timelock

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

Transaction proposal access control contradicts documented signer permissions

Author Revealed upon completion

Transaction proposal access control contradicts documented signer permissions

Description

According to the project documentation (README), any address holding the SIGNING_ROLE is allowed to propose new transactions. The documentation explicitly states that transaction proposal rights are tied to the signer role, not exclusively to the contract owner.

However, the implementation of the proposeTransaction function restricts access using the onlyOwner modifier:

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

As a result, only the owner can propose transactions, while other valid signers are unable to do so. This behavior directly contradicts the documented role model and the expected multisig workflow.

Risk

Impact: Medium

This issue does not directly enable fund theft or unauthorized execution. However, it introduces an unintended centralization point by preventing non-owner signers from proposing transactions, despite being explicitly allowed to do so according to the documentation. This weakens the intended governance and operational model of the multisig wallet.

Likelihood: High

The issue affects normal protocol usage and will occur deterministically. Any signer (other than the owner) attempting to propose a transaction as described in the README will consistently encounter a revert.

Proof of Concept

The following Foundry test can be copied directly into the existing MultiSigTimeLockTest contract and executed without additional setup.

function test_signerCannotProposeTransactionDespiteDocumentation() public {
multiSigTimelock.grantSigningRole(SIGNER_TWO);
assertTrue(multiSigTimelock.isSigner(SIGNER_TWO), "Signer should have SIGNING_ROLE");
vm.prank(SIGNER_TWO);
vm.expectRevert(); // reverts due to onlyOwner modifier
multiSigTimelock.proposeTransaction(
SPENDER_ONE,
1 ether,
""
);
}

This test demonstrates that a valid signer, as defined by the protocol documentation, is unable to propose a transaction due to the onlyOwner restriction.

Recommended Mitigation

Align the access control logic with the documented role model. Either:

  • Replace the onlyOwner modifier with a role-based check that allows any address holding the SIGNING_ROLE to propose transactions

function proposeTransaction(address to, uint256 value, bytes calldata data)
external
nonReentrant
noneZeroAddress(to)
- onlyOwner
+ onlyRole(SIGNING_ROLE)
returns (uint256)
{
return _proposeTransaction(to, value, data);
}
  • Or update the documentation to explicitly state that only the contract owner can propose transactions.

Ensuring consistency between implementation and documentation is critical for correct security assumptions, proper governance, and safe protocol usage.

Support

FAQs

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

Give us feedback!