MultiSig Timelock

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

proposeTransaction Restricted to Owner Only - Breaking Core Multisig Functionality

Description

  • According to the protocol specification in the README, any signer with SIGNING_ROLE should be able to propose new transactions: "Propose new transactions (permission is tied to the role, so any signer can propose)"

  • However, the proposeTransaction function uses the onlyOwner modifier instead of onlyRole(SIGNING_ROLE), which restricts transaction proposals to only the contract owner/deployer, completely breaking the intended functionality for other signers.

// @> proposeTransaction uses onlyOwner instead of onlyRole(SIGNING_ROLE)
function proposeTransaction(address to, uint256 value, bytes calldata data)
external
nonReentrant
noneZeroAddress(to)
onlyOwner // @> ROOT CAUSE: Should be onlyRole(SIGNING_ROLE)
returns (uint256)
{
return _proposeTransaction(to, value, data);
}

Risk

Likelihood:

  • Every time a non-owner signer attempts to propose a transaction, the call will revert

  • This is the default behavior - it happens 100% of the time for 4 out of 5 possible signers

Impact:

  • Core protocol functionality is broken - signers cannot propose transactions as documented

  • The multisig is effectively centralized around the owner for proposal creation

  • 80% of authorized signers (4 out of 5) cannot use the proposal feature they are supposed to have

  • Violates the principle of distributed governance that multisigs are designed to provide

Proof of Concept

function testSignerCannotProposeTransaction() public {
// Setup: Grant signing role to SIGNER_TWO
multiSigTimelock.grantSigningRole(SIGNER_TWO);
// Verify SIGNER_TWO has SIGNING_ROLE
assertTrue(multiSigTimelock.hasRole(multiSigTimelock.getSigningRole(), SIGNER_TWO));
// Fund the contract
vm.deal(address(multiSigTimelock), 1 ether);
// SIGNER_TWO attempts to propose a transaction - THIS WILL REVERT
vm.prank(SIGNER_TWO);
vm.expectRevert(); // Reverts because SIGNER_TWO is not the owner
multiSigTimelock.proposeTransaction(address(0x123), 0.5 ether, "");
}

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!