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,
data: data,
confirmations: 0,
proposedAt: block.timestamp,
executed: false
});
s_transactionCount++;
emit TransactionProposed(transactionId, to, value);
return transactionId;
}
Risk
Likelihood:
Multiple signers confirm such transactions unaware they will fail.
Impact:
Wasted effort and time for signers who approve transactions that cannot succeed.
Proof of Concept
uint256 balanceBefore = address(multiSigTimelock).balance;
uint txnId = multiSigTimelock.proposeTransaction(target, 5 ether, "");
multiSigTimelock.confirmTransaction(txnId);
multiSigTimelock.confirmTransaction(txnId);
multiSigTimelock.confirmTransaction(txnId);
multiSigTimelock.executeTransaction(txnId);
uint256 balanceAfter = address(multiSigTimelock).balance;
assert(balanceBefore == balanceAfter);
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;
}