MultiSig Timelock

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

Timelock Bypass via Operation-Type Blind Enforcement

Author Revealed upon completion

Root + Impact

Description

  • A multisig timelock must enforce a mandatory delay for all security-critical operations, ensuring signers and observers have time to react before execution.

  • The timelock enforcement is applied uniformly without distinguishing operation type, allowing privilege-changing or governance actions (e.g., signer additions, role grants) to execute after the same delay as regular transfers. This collapses the security boundary between fund movement and authority escalation, enabling rapid governance takeover.

// @> Timelock does not differentiate sensitive operations
function executeTransaction(uint256 txId) external {
require(block.timestamp >= transactions[txId].eta, "Timelock not expired");
_execute(txId);
}

Risk

Likelihood:

  • Governance or role-management transactions are proposed regularly.

Signers assume timelock sufficiently protects all operations equally.

Impact:

  • Attackers gain signer/admin roles with minimal delay.

Full multisig takeover possible without external detection window.

Proof of Concept

  • Although the timelock expires correctly, it does not account for risk asymmetry between transfers and governance actions. A single signer-set mutation permanently compromises system trust.

// Propose role escalation
submitTransaction(
address(accessControl),
0,
abi.encodeWithSelector(
grantRole.selector,
SIGNER_ROLE,
attacker
)
);
// Wait minimal delay
confirmTransaction(txId);
executeTransaction(txId);
// Attacker is now a signer → governance compromised

Recommended Mitigation

- remove this code
+ add this code
+ function executeTransaction(uint256 txId) external {
+ if (_isGovernanceOperation(transactions[txId])) {
+ require(
+ block.timestamp >= transactions[txId].eta + GOVERNANCE_DELAY,
+ "Extended timelock for governance"
+ );
+ }
require(block.timestamp >= transactions[txId].eta, "Timelock not expired");
_execute(txId);
}

Support

FAQs

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

Give us feedback!