MultiSig Timelock

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

Documentation Claims Any Signer Can Propose Transactions, but proposeTransaction Is Restricted to onlyOwner

Root + Impact

Description

  • Normal behavior: In a multisig workflow, any authorized signer should be able to create/propose a transaction for the other signers to review and confirm, matching the README statement: “permission is tied to the role, so any signer can propose”.


  • Issue: The implementation restricts transaction proposal creation to the contract owner (onlyOwner). As a result, signers who hold SIGNING_ROLE cannot propose transactions, contradicting the documented behavior and materially changing the governance/user-interaction model.


function proposeTransaction(address to, uint256 value, bytes calldata data)
external
nonReentrant
noneZeroAddress(to)
// @> onlyOwner // contradicts README claim that any signer can propose
returns (uint256)
{
return _proposeTransaction(to, value, data);
}

Risk

Likelihood:

  • In normal operation, teams onboard multiple signers and expect them to initiate proposals based on the README-described workflow.


Impact:

  • The system operates as “owner proposes, signers approve” instead of the documented “any signer proposes”, centralizing proposal power in the owner account


Proof of Concept

Paste this test into MultiSigTimelock.t.sol. It shows that an account with SIGNING_ROLE still cannot call proposeTransaction (it reverts due to onlyOwner).

function test_SignerCannotProposeDespiteREADMEClaim() public grantSigningRoles {
// ARRANGE
// Fund the signer just to remove any confusion about balances;
// proposeTransaction itself doesn't spend msg.sender ETH, but this keeps the setup clean.
vm.deal(SIGNER_TWO, OWNER_BALANCE_ONE);
// Sanity: SIGNER_TWO is a valid signer per AccessControl
assertTrue(multiSigTimelock.hasRole(multiSigTimelock.getSigningRole(), SIGNER_TWO));
// ACT & ASSERT
// README claims any signer can propose; implementation requires onlyOwner and should revert.
vm.prank(SIGNER_TWO);
vm.expectRevert(); // Ownable revert (string-based in OZ), selector not guaranteed
multiSigTimelock.proposeTransaction(SPENDER_ONE, OWNER_BALANCE_ONE, hex"");
}

Recommended Mitigation

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

Lead Judging Commences

kelechikizito Lead Judge
11 days ago
kelechikizito Lead Judge 4 days ago
Submission Judgement Published
Invalidated
Reason: Other

Support

FAQs

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

Give us feedback!