Description
-
The contract requires 3 confirmations (REQUIRED_CONFIRMATIONS = 3) to execute any transaction, but the constructor only adds 1 signer (the deployer).
-
This means the multisig wallet is completely non-functional after deployment until the owner manually adds at least 2 more signers. Any ETH deposited during this period is effectively locked.
uint256 private constant REQUIRED_CONFIRMATIONS = 3;
constructor() Ownable(msg.sender) {
s_signers[0] = msg.sender;
s_isSigner[msg.sender] = true;
s_signerCount = 1;
_grantRole(SIGNING_ROLE, msg.sender);
}
Risk
Likelihood:
Impact:
-
Funds deposited to the contract are locked until owner adds 2+ more signers
-
If owner loses access before adding signers, funds are permanently locked
-
No emergency withdrawal mechanism exists
-
Violates the principle that a deployed contract should be functional
Proof of Concept
function testContractUnusableAfterDeployment() public {
MultiSigTimelock freshMultisig = new MultiSigTimelock();
vm.deal(address(freshMultisig), 10 ether);
uint256 txnId = freshMultisig.proposeTransaction(address(0x123), 1 ether, "");
freshMultisig.confirmTransaction(txnId);
vm.expectRevert(
abi.encodeWithSelector(
MultiSigTimelock.MultiSigTimelock__InsufficientConfirmations.selector,
3,
1
)
);
freshMultisig.executeTransaction(txnId);
}
Recommended Mitigation
Option 1: Require minimum signers at deployment:
- constructor() Ownable(msg.sender) {
+ constructor(address[] memory _initialSigners) Ownable(msg.sender) {
+ require(_initialSigners.length >= 3, "Need at least 3 signers");
+ require(_initialSigners.length <= 5, "Max 5 signers");
+
+ for (uint256 i = 0; i < _initialSigners.length; i++) {
+ require(_initialSigners[i] != address(0), "Invalid signer");
+ require(!s_isSigner[_initialSigners[i]], "Duplicate signer");
+
+ s_signers[i] = _initialSigners[i];
+ s_isSigner[_initialSigners[i]] = true;
+ _grantRole(SIGNING_ROLE, _initialSigners[i]);
+ }
+ s_signerCount = _initialSigners.length;
- }
+ }
Option 2: Add an emergency withdrawal function for owner when signer count < 3:
+ function emergencyWithdraw(address to, uint256 amount) external onlyOwner {
+ require(s_signerCount < REQUIRED_CONFIRMATIONS, "Use normal flow");
+ require(amount <= address(this).balance, "Insufficient balance");
+ (bool success,) = payable(to).call{value: amount}("");
+ require(success, "Transfer failed");
+ }