MultiSig Timelock

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

Missing DEFAULT_ADMIN_ROLE Grant Breaks Signer Management

Author Revealed upon completion

Upon deployment, the contract is intended to initialize the deployer as both the contract owner and the first authorized signer, enabling them to later add or remove additional signers via grantSigningRole and revokeSigningRole.

However, the constructor fails to grant the DEFAULT_ADMIN_ROLE to the deployer. Since OpenZeppelin’s AccessControl requires the admin role to manage other roles, this omission permanently disables all signer management functionality — rendering grantSigningRole and revokeSigningRole unusable.

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

Likelihood: High

  • The issue occurs on every single deployment of the contract in its current state.

  • It is deterministic and unavoidable — not dependent on user behavior, edge cases, or external conditions.

  • Since the constructor never grants DEFAULT_ADMIN_ROLE, role management is guaranteed to fail for all future calls to grantSigningRole and revokeSigningRole.

impact

  • Breaks core functionality: The contract cannot add or remove signers after deployment.

  • Reduces security model: A 3-of-5 multisig becomes a 1-of-1 wallet (only the deployer can propose and — if confirmations are counted — confirm), completely undermining the multisig guarantee.Grant DEFAULT_ADMIN_ROLE to the deployer in the constructor:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
contract BrokenMultiSig is AccessControl {
bytes32 public constant SIGNING_ROLE = keccak256("SIGNING_ROLE");
constructor() {
_grantRole(SIGNING_ROLE, msg.sender);
}
function addSigner(address account) external {
_grantRole(SIGNING_ROLE, account);
}
}
import "forge-std/Test.sol";
contract PoCTest is Test {
BrokenMultiSig multisig;
function setUp() public {
multisig = new BrokenMultiSig();
}
function testAddSigner_RevertsDueToMissingAdminRole() public {
address newSigner = address(0x2222);
Expect revert from AccessControl: sender not admin
vm.expectRevert(
abi.encodeWithSelector(
AccessControl.AccessControlUnauthorizedAccount.selector,
address(this),
bytes32(0) DEFAULT_ADMIN_ROLE = bytes32(0)
)
);
multisig.addSigner(newSigner);
}
}

Recommended Mitigation

Grant DEFAULT_ADMIN_ROLE to the deployer in the constructor:
constructor() Ownable(msg.sender) {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(SIGNING_ROLE, msg.sender);
s_signers[0] = msg.sender;
s_isSigner[msg.sender] = true;
s_signerCount = 1;
}

Support

FAQs

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

Give us feedback!