Summary
The executeTransaction
function in the MultiSigWallet
contract does not specify a gas limit for the external call, which could be exploited by a malicious owner to perform DoS attacks or manipulate gas.
Vulnerability Details
In MultiSig.sol
, the executeTransaction
function:
Allows any owner to execute an approved transaction
Imposes no gas limit on the external call
Trusts both owners to act in good faith
function executeTransaction(uint256 _txId) external onlyOwners {
require(_txId < transactions.length, "Invalid transaction ID");
Transaction storage txn = transactions[_txId];
require(!txn.executed, "Transaction already executed");
require(txn.approvedByOwner1 && txn.approvedByOwner2, "Not enough approvals");
txn.executed = true;
(bool success,) = payable(txn.to).call{value: txn.value}("");
require(success, "Transaction failed");
emit TransactionExecuted(_txId, txn.to, txn.value);
}
Impact
DoS Attack : A malicious owner could create a transaction to a contract that intentionally consumes all the gas
Gas Manipulation : Possibility of forcing transactions to fail by manipulating gas consumption
MultiSig Blockage : In the worst case, the MultiSig could become unusable
Severity: LOW - Requires an owner to be malicious and the other owner to approve the malicious transaction
Tools Used
Proof of Concept
contract MaliciousContract {
uint256 public counter;
receive() external payable {
while(true) {
counter++;
}
}
}
function testGasManipulation() public {
MaliciousContract malicious = new MaliciousContract();
vm.prank(owner1);
multiSig.submitTransaction(address(malicious), 1 ether);
vm.prank(owner1);
multiSig.approveTransaction(0);
vm.prank(owner2);
multiSig.approveTransaction(0);
vm.prank(owner1);
vm.expectRevert();
multiSig.executeTransaction(0);
}
Recommendations
Add an explicit gas limit:
function executeTransaction(uint256 _txId) external onlyOwners {
(bool success,) = payable(txn.to).call{value: txn.value, gas: 50000}("");
require(success, "Transaction failed");
}
Implement a destination code verification:
function executeTransaction(uint256 _txId) external onlyOwners {
uint256 size;
address to = txn.to;
assembly {
size := extcodesize(to)
}
if (size > 0) {
(bool success,) = payable(to).call{value: txn.value, gas: 50000}("");
require(success, "Transaction failed");
} else {
(bool success,) = payable(to).call{value: txn.value}("");
require(success, "Transfer failed");
}
}
Add a security delay for transactions to contracts:
mapping(uint256 => uint256) public transactionTimelocks;
function approveTransaction(uint256 _txId) external onlyOwners {
uint256 size;
assembly {
size := extcodesize(txn.to)
}
if (size > 0) {
transactionTimelocks[_txId] = block.timestamp + 24 hours;
}
}
function executeTransaction(uint256 _txId) external onlyOwners {
if (transactionTimelocks[_txId] > 0) {
require(
block.timestamp >= transactionTimelocks[_txId],
"Transaction is timelocked"
);
}
}