Santa's List

AI First Flight #3
Beginner FriendlyFoundry
EXP
View results
Submission Details
Impact: low
Likelihood: low
Invalid

MultiSigWallet lacks reentrancy guard -- cross-transaction atomic drain

Description

  • The executeTransaction() function sets txn.executed = true before the external call{value}, which correctly prevents same-txId reentrancy.

  • However, if multiple transactions are pre-approved, a malicious recipient contract can re-enter executeTransaction() with a different txId during the external call's callback. This allows executing multiple transactions atomically in a single call frame, preventing the second owner from reacting or revoking between executions.

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; // @> protects same-txId reentrancy
(bool success,) = payable(txn.to).call{value: txn.value}(""); // @> external call allows re-entry with different txId
require(success, "Transaction failed");
}

Risk

Likelihood:

  • Requires both owners to have approved multiple transactions AND the recipient to be a malicious contract -- a narrow scenario

  • Both owners must be in on it or one must have been tricked into pre-approving multiple transactions

Impact:

  • Multiple pre-approved transactions execute atomically, preventing the second owner from revoking between them

  • No additional funds are stolen beyond what was already approved, but timing control is lost

Proof of Concept

// Malicious contract that re-enters on receive
contract ReentrancyAttacker {
MultiSigWallet public wallet;
uint256 public secondTxId;
constructor(address _wallet, uint256 _secondTxId) {
wallet = MultiSigWallet(payable(_wallet));
secondTxId = _secondTxId;
}
receive() external payable {
if (!wallet.transactions(secondTxId).executed) {
wallet.executeTransaction(secondTxId);
}
}
}

Recommended Mitigation

+ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
- contract MultiSigWallet {
+ contract MultiSigWallet is ReentrancyGuard {
- function executeTransaction(uint256 _txId) external onlyOwners {
+ function executeTransaction(uint256 _txId) external onlyOwners nonReentrant {
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!