MultiSig Timelock

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

Single Point of Failure: proposeTransaction is restricted to onlyOwner instead of all signers

Author Revealed upon completion

Root + Impact

Description

  • The README explicitly states that the project is a decentralized multi-signature wallet where permission to propose transactions is tied to the SIGNING_ROLE: "Propose new transactions (permission is tied to the role, so any signer can propose)".

  • However, the implementation of proposeTransaction uses the onlyOwner modifier, which restricts this core functionality to the deployer only.

function proposeTransaction(address to, uint256 value, bytes calldata data)
external
nonReentrant
noneZeroAddress(to)
@> onlyOwner // @audit-issue: Should be onlyRole(SIGNING_ROLE)
returns (uint256)

Risk

Likelihood:

  • This occurs consistently as it is a hardcoded restriction.

Impact:

  • Centralization Risk: The wallet has a Single Point of Failure. If the Owner's private key is lost or compromised, the entire multi-sig becomes non-functional as no other signers can initiate a transaction.

  • Business Logic Violation: The contract fails to meet its primary objective of distributed governance.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {MultiSigTimelock} from "src/MultiSigTimelock.sol";
contract PoC_Centralization is Test {
MultiSigTimelock public multiSig;
address public OWNER = address(this);
address public SIGNER_2 = makeAddr("signer2");
function setUp() public {
multiSig = new MultiSigTimelock();
multiSig.grantSigningRole(SIGNER_2);
}
function test_SignerCannotProposeTransaction() public {
vm.startPrank(SIGNER_2);
vm.expectRevert();
multiSig.proposeTransaction(address(0x123), 1 ether, "");
vm.stopPrank();
console.log("Verified: Signer cannot propose a transaction due to onlyOwner restriction.");
}
}

Run:

forge test --match-path test/PoC_Centralization.t.sol -vvvv

Output:

Ran 1 test for test/PoC_Centralization.t.sol:PoC_Centralization
[PASS] test_SignerCannotProposeTransaction() (gas: 24835)
Logs:
Verified: Signer cannot propose a transaction due to onlyOwner restriction.
Traces:
[24835] PoC_Centralization::test_SignerCannotProposeTransaction()
├─ //..........
├─ [0] console::log("Verified: Signer cannot propose a transaction due to onlyOwner restriction.") [staticcall]
│ └─ ← [Stop]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped;

Recommended Mitigation

Change the onlyOwner modifier to onlyRole(SIGNING_ROLE) or add a s_isSigner[msg.sender] check:

- function proposeTransaction(address to, uint256 value, bytes calldata data) external nonReentrant noneZeroAddress(to) onlyOwner returns (uint256)
+ function proposeTransaction(address to, uint256 value, bytes calldata data) external nonReentrant noneZeroAddress(to) onlyRole(SIGNING_ROLE) returns (uint256)

Support

FAQs

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

Give us feedback!