MultiSig Timelock

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

Signer can not propose new transaction

Author Revealed upon completion

Description

  • According to the project description, any signer (holder of SIGNING_ROLE) should be able to propose new transactions for multisig review.

  • In the implementation, proposeTransaction is restricted with onlyOwner. As a result, non‑owner signers cannot propose transactions and any attempt will revert, breaking the expected multisig workflow and centralizing proposal power in a single account.

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

Risk

Likelihood: High

  • In normal operations where proposal duties are delegated to signers, any non‑owner signer attempting to propose will immediately hit a revert.

  • This will occur whenever the owner is offline/busy and signers are expected to initiate routine payments or maintenance transactions.

Impact: High

  • Operational bottleneck and governance centralization: only the owner can initiate proposals, creating a single point of failure and delays.Impact 1

  • Mismatch between documentation and behavior can lead to broken tooling/tests and confusion for users/integrators relying on signer‑initiated proposals.

Proof of Concept

  • Copy the code below to MultiSigTimeLockTest.t.sol.

  • Run command forge test --mt testSignerCanNotProposeTransaction -vvvv.

function testSignerCanNotProposeTransaction() public {
// Arrange: give SIGNER_TWO the SIGNING_ROLE (but they are not the owner)
multiSigTimelock.grantSigningRole(SIGNER_TWO);
// Act & Assert: proposing as SIGNER_TWO reverts due to `onlyOwner`
vm.prank(SIGNER_TWO);
vm.expectRevert();
multiSigTimelock.proposeTransaction(SPENDER_ONE, OWNER_BALANCE_ONE, hex"");
}

Output:

[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/unit/MultiSigTimelockTest.t.sol:MultiSigTimeLockTest
[PASS] testSignerCanNotProposeTransaction() (gas: 101658)
Traces:
[104458] MultiSigTimeLockTest::testSignerCanNotProposeTransaction()
├─ [83181] MultiSigTimelock::grantSigningRole(signer_two: [0x884D93fF97A786C23d91993E20382Fc4C26Bc2aa])
│ ├─ emit RoleGranted(role: 0x8d12c52fe7c286acb23e8b6fad43ba0a7b1bdee59ad6818c044e478cc487c15b, account: signer_two: [0x884D93fF97A786C23d91993E20382Fc4C26Bc2aa], sender: MultiSigTimeLockTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
│ └─ ← [Stop]
├─ [0] VM::prank(signer_two: [0x884D93fF97A786C23d91993E20382Fc4C26Bc2aa])
│ └─ ← [Return]
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [4789] MultiSigTimelock::proposeTransaction(spender_one: [0x488dE584674d70E8Da70C42Cd4CC73Cd63d3dB23], 500000000000000000 [5e17], 0x)
│ └─ ← [Revert] OwnableUnauthorizedAccount(0x884D93fF97A786C23d91993E20382Fc4C26Bc2aa)
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.11ms (178.80µs CPU time)
Ran 1 test suite in 19.07ms (3.11ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

  • Replace onlyOwner with onlyRole(SIGNING_ROLE):

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