MultiSig Timelock

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

Self‑Call Transaction Abuse - MultiSigTimelock Allows Calls to Its Own Contract

Author Revealed upon completion

Root + Impact

Due to absence of validation preventing self‑calls during transaction proposal or execution. The contract can execute its own external functions

Description

The timelock’s MultiSigTimelock::_executeTransaction function allows arbitrary calls via:.call. There is no restriction preventing txn.to from being the timelock contract itself (address(this)). This means signers can propose and execute transactions that call back into the timelock’s own functions. Such self‑targeted execution erodes the boundary between governance and execution, enabling recursive calls, denial‑of‑service scenarios, or governance self‑mutation.


function _executeTransaction(uint256 txnId) internal {
Transaction storage txn = s_transactions[txnId];
// CHECKS
// 1. Check if enough confirmations
if (txn.confirmations < REQUIRED_CONFIRMATIONS) {
revert MultiSigTimelock__InsufficientConfirmations(REQUIRED_CONFIRMATIONS, txn.confirmations);
}
// 2. Check if timelock period has passed
uint256 requiredDelay = _getTimelockDelay(txn.value);
uint256 executionTime = txn.proposedAt + requiredDelay;
if (block.timestamp < executionTime) {
revert MultiSigTimelock__TimelockHasNotExpired(executionTime);
}
if (txn.value > address(this).balance) {
revert MultiSigTimelock__InsufficientBalance(address(this).balance);
}
// EFFECTS
// 3. Mark as executed BEFORE the external call (prevent reentrancy)
txn.executed = true;
// INTERACTIONS
// 4. Execute the transaction
@> (bool success,) = payable(txn.to).call{value: txn.value}(txn.data);
if (!success) {
revert MultiSigTimelock__ExecutionFailed();
}
// 5. Emit eventt
emit TransactionExecuted(txnId, txn.to, txn.value);
}

Risk

Likelihood:

  • Any signer can trivially propose a transaction with txn.to = address(this). No special conditions are required.

Impact:

  • Signers can schedule transactions that call grantRole, revokeRole, or other admin functions on the timelock itself, mutating governance in unexpected ways.

Proof of Concept

Signers propose a transaction

Transaction passes quorum + timelock

During execution:

  • Without this guard `require(txn.to != address(this)`, a signer can propose and execute a transaction that calls back into the timelock contract itself,

- A signer can propose a transaction where the target address (txn.to) is the timelock contract itself.
- The calldata of that transaction can be crafted to call one of the timelock’s own public functions (for example, grantRole).
- Other signers confirm the transaction until quorum is reached.
- After the timelock delay expires, any signer can execute it.
- When executed, the timelock ends up calling itself and successfully runs the internal function — in the PoC, this resulted in granting a new role to an account.

Recommended Mitigation

Reject self-targeted transactions.

Can also put in proposeTransaction This will prevents such transactions from ever being queued.

function _executeTransaction(uint256 txnId) internal {
Transaction storage txn = s_transactions[txnId];
if (txn.confirmations < REQUIRED_CONFIRMATIONS) {
revert MultiSigTimelock__InsufficientConfirmations(REQUIRED_CONFIRMATIONS, txn.confirmations);
}
uint256 requiredDelay = _getTimelockDelay(txn.value);
uint256 executionTime = txn.proposedAt + requiredDelay;
if (block.timestamp < executionTime) {
revert MultiSigTimelock__TimelockHasNotExpired(executionTime);
}
if (txn.value > address(this).balance) {
revert MultiSigTimelock__InsufficientBalance(address(this).balance);
}
+
require(txn.to != address(this), "Cannot target timelock contract itself");
txn.executed = true;
(bool success,) = payable(txn.to).call{value: txn.value}(txn.data);
if (!success) {
revert MultiSigTimelock__ExecutionFailed();
}
emit TransactionExecuted(txnId, txn.to, txn.value);
}

Support

FAQs

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

Give us feedback!