MultiSig Timelock

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

Gas inefficiency and wasted signer effort due to missing upfront balance check in transaction proposals

Author Revealed upon completion

Gas inefficiency and wasted signer effort due to missing upfront balance check in transaction proposals

Description

  • In a multisig wallet, proposing a transaction should allow signers to review and approve transactions that can actually be executed successfully, without wasting gas or time.


  • The proposeTransaction function does not validate that the proposed transaction value is less than or equal to the contract’s current balance. The validation only occurs during execution, causing signers to potentially confirm transactions that will inevitably fail.

function _proposeTransaction(address to, uint256 value, bytes memory data) internal returns (uint256) {
uint256 transactionId = s_transactionCount;
s_transactions[transactionId] = Transaction({
to: to,
value: value, // @> No check here to ensure value <= address(this).balance
data: data,
confirmations: 0,
proposedAt: block.timestamp,
executed: false
});
s_transactionCount++;
emit TransactionProposed(transactionId, to, value);
return transactionId;
}

Risk

Likelihood:

  • Transactions are proposed by the owner without verifying available balance.


Multiple signers confirm such transactions unaware they will fail.

Impact:

  • Wasted gas for both proposing and executing the failing transaction.


Wasted effort and time for signers who approve transactions that cannot succeed.

Proof of Concept

// Assume the contract balance is 1 ether
uint256 balanceBefore = address(multiSigTimelock).balance;
// Owner proposes a transaction of 5 ether
uint txnId = multiSigTimelock.proposeTransaction(target, 5 ether, "");
// Signers confirm the transaction
multiSigTimelock.confirmTransaction(txnId);
multiSigTimelock.confirmTransaction(txnId);
multiSigTimelock.confirmTransaction(txnId);
// Attempt to execute fails due to insufficient balance
multiSigTimelock.executeTransaction(txnId); // Reverts with MultiSigTimelock__InsufficientBalance
uint256 balanceAfter = address(multiSigTimelock).balance;
assert(balanceBefore == balanceAfter); // No ETH moved, but gas wasted

Recommended Mitigation

function _proposeTransaction(address to, uint256 value, bytes memory data) internal returns (uint256) {
uint256 transactionId = s_transactionCount;
s_transactions[transactionId] = Transaction({
to: to,
+ require(value <= address(this).balance, "Insufficient contract balance for proposed transaction");
value: value, // @> No check here to ensure value <= address(this).balance
data: data,
confirmations: 0,
proposedAt: block.timestamp,
executed: false
});
s_transactionCount++;
emit TransactionProposed(transactionId, to, value);
return transactionId;
}

Support

FAQs

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

Give us feedback!