MultiSig Timelock

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

Only the Owner Can Propose Transactions

Author Revealed upon completion


The MultiSigTimelock contract is designed as a multi-signature wallet where up to 5 signers (holders of the SIGNING_ROLE) collaborate to manage funds securely. According to the project specification, any signer should be able to propose new transactions, as proposal permission is explicitly tied to the SIGNING_ROLE rather than sole ownership. This enables decentralized governance, allowing team members or co-owners to initiate transfers without relying on a single privileged account.

However, the proposeTransaction function is incorrectly restricted with the onlyOwner modifier inherited from Ownable. This means only the original deployer (contract owner) can call it to propose transactions, while other added signers cannot initiate proposals despite holding the SIGNING_ROLE.

https://github.com/CodeHawks-Contests/2025-12-multisig-timelock/blob/3c88fea850b25724b71778bdc7bfe96c3bd97b63/src/MultiSigTimelock.sol#L253

Risk

Likelihood: High

  • The owner is always a signer (granted SIGNING_ROLE in constructor), and additional signers are added via grantSigningRole (onlyOwner).

  • In any realistic deployment with multiple signers (2–5 total), non-owner signers will attempt to propose transactions as per the documented behavior.

  • Impact: High

    • Severely limits wallet usability: Added signers can only confirm, revoke, or execute existing proposals but cannot initiate any new transfers or contract interactions.

    • Defeats the multi-signature governance model, centralizing proposal power with the owner and contradicting the spec's description of equal powers among signers (except role management).

    • In team or DAO treasuries, this creates a single point of failure/bottleneck for fund movement, potentially leading to operational deadlock if the owner is unavailable or compromised.

Proof of Concept

pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {MultiSigTimelock} from "../src/MultiSigTimelock.sol";
contract MultiSigTimelockTest is Test {
MultiSigTimelock wallet;
address owner = address(0xA1);
address signer1 = address(0xB1);
address signer2 = address(0xB2);
address recipient = address(0xC1);
function setUp() public {
vm.prank(owner);
wallet = new MultiSigTimelock();
vm.startPrank(owner);
wallet.grantSigningRole(signer1);
wallet.grantSigningRole(signer2);
vm.stopPrank();
vm.deal(address(wallet), 10 ether);
}
function test_OwnerCanProposeTransaction() public {
vm.prank(owner);
uint256 txId = wallet.proposeTransaction(recipient, 0.5 ether, "");
assertEq(txId, 0);
(address to,,,) = wallet.getTransaction(txId);
assertEq(to, recipient);
}
function testFail_SignerCannotProposeTransaction() public {
vm.prank(signer1);
wallet.proposeTransaction(recipient, 0.1 ether, "");
}
function test_SignerHasSigningRoleButStillCannotPropose() public {
bytes32 SIGNING_ROLE = wallet.getSigningRole();
bool hasRole = wallet.hasRole(SIGNING_ROLE, signer1);
assertTrue(hasRole, "signer1 should have SIGNING_ROLE");
vm.expectRevert();
vm.prank(signer1);
wallet.proposeTransaction(recipient, 0.1 ether, "");
}
function test_RevertReasonIsOnlyOwner() public {
vm.expectRevert("Ownable: caller is not the owner");
vm.prank(signer2);
wallet.proposeTransaction(recipient, 0.2 ether, "");
}
}

Recommended Mitigation

function proposeTransaction(address to, uint256 value, bytes calldata data)
external
nonReentrant
noneZeroAddress(to)
onlyRole(SIGNING_ROLE) ← Fixed
returns (uint256 transactionId)
{
transactionId = _proposeTransaction(to, value, data);
_confirmTransaction(transactionId);
}
Remove onlyOwner from proposeTransaction.
Replace with onlyRole(SIGNING_ROLE) to align with the specification and standard multisig design.
Optionally, after proposing, automatically confirm the transaction on behalf of the proposer (call _confirmTransaction internally) to count it as the first confirmation and improve UX (common in multisig implementations like Gnosis Safe).

Support

FAQs

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

Give us feedback!