Vanguard

First Flight #56
Beginner FriendlyDeFiFoundry
0 EXP
Submission Details
Impact: high
Likelihood: high

Timelock Protection Can Be Bypassed via Atomic Multi-Operation Governance Execution

Author Revealed upon completion

Root + Impact

Description

  • The timelock is designed to enforce a minimum delay between governance proposal approval and execution, ensuring users have time to react to critical changes.

The system allows multiple governance actions to be executed atomically within a single transaction, enabling a sequence where the timelock is modified (or weakened) and immediately relied upon within the same execution context, effectively bypassing its protective delay.

// @> Timelock state is updated and relied upon in the same transaction
executeTransaction(txId); // modifies delay / permissions
executeTransaction(nextTxId); // executes privileged action immediately

Risk

Likelihood:

  • Occurs during standard multisig governance workflows that batch or chain executions

Common when governance tooling executes multiple queued transactions sequentially

Impact:

  • Timelock delay guarantees are bypassed without violating explicit access control

Enables immediate execution of sensitive governance actions (upgrades, signer changes, fund transfers)

Proof of Concept

  • The timelock enforces delay between scheduling and execution, but not between state transitions inside the same transaction. Once the delay is reduced mid-execution, subsequent actions inherit the weakened state.

// Transaction A: reduce timelock delay
submitTransaction(
address(this),
0,
abi.encodeWithSelector(setDelay.selector, 0)
);
// Transaction B: privileged action
submitTransaction(
treasury,
0,
abi.encodeWithSelector(drain.selector)
);
// After delay expires
executeTransaction(txA);
executeTransaction(txB); // executes instantly in same tx

Recommended Mitigation

  • Enforce transaction-scoped timelock invariants so that timelock configuration changes only take effect in a future block or epoch.

- remove this code
+ add this code
function setDelay(uint256 newDelay) external {
- delay = newDelay;
+ pendingDelay = newDelay;
+ delayActivationBlock = block.number + 1;
}
function getDelay() public view returns (uint256) {
+ if (block.number < delayActivationBlock) return delay;
+ return pendingDelay;
}

Support

FAQs

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

Give us feedback!