MultiSig Timelock

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

`MultiSigTimelock::proposeTransaction` is restricted to the owner, preventing signers from proposing transactions.

Author Revealed upon completion

MultiSigTimelock::proposeTransaction is restricted to the owner, preventing signers from proposing transactions.

Description

  • Any account with the SIGNING_ROLE should be able to propose new transactions, allowing equal participation of all signers in the multisig governance flow.

  • Only the owner can propose transactions, as the proposeTransaction() function is protected with onlyOwner, preventing other signers from initiating proposals despite having the signing role.

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

Risk

Likelihood: Medium

  • The behavior occurs whenever a signer who is not the owner tries to propose a transaction.

  • The contract enforces this flow systematically via onlyOwner, with no alternative configuration.

Impact: Medium

  • Centralizes proposal creation to a single address, reducing the decentralization of the multisig.

  • Allows censorship of legitimate proposals by the owner, weakening the described governance model.

Proof of Concept

This test demonstrates that, even with the signer role, an account that is not the owner cannot propose a transaction, as the function is restricted with onlyOwner.

function test_SignersCannotPropose() public {
// Define a set of 5 addresses (owner + 4 signers)
address[5] memory signers;
signers[0] = OWNER;
signers[1] = SIGNER_TWO;
signers[2] = SIGNER_THREE;
signers[3] = SIGNER_FOUR;
signers[4] = SIGNER_FIVE;
// The owner assigns the signer role to the other accounts
for (uint256 i = 1; i < signers.length; i++) {
vm.prank(OWNER);
multiSigTimelock.grantSigningRole(signers[i]);
}
// A signer (who is NOT the owner) tries to propose a transaction
vm.prank(SIGNER_THREE);
// Expect a revert because proposeTransaction is protected with onlyOwner
vm.expectRevert(
abi.encodeWithSelector(
Ownable.OwnableUnauthorizedAccount.selector,
SIGNER_THREE
)
);
// The proposal fails even though SIGNER_THREE has the SIGNING_ROLE
multiSigTimelock.proposeTransaction(address(1), 1 ether, "");
}
[PASS] test_SignersCannotPropose() (gas: 335891)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 21.12ms (3.61ms CPU time)

Recommended Mitigation

Allow any account with the SIGNING_ROLE to propose transactions, aligning contract behavior with the documented governance model.
Replacing onlyOwner with onlyRole(SIGNING_ROLE) removes unnecessary centralization and ensures all signers can participate in the proposal process.

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!