DatingDapp

First Flight #33
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: low
Invalid

[M-2] Reentrancy: Reentrancy Risk in MultiSigWallet::executeTransaction Function

Summary

The executeTransaction function in the MultiSigWallet contract is vulnerable to reentrancy attacks because it makes an external call (call{value: txn.value}) before updating state variables or emitting events. A malicious contract can re-enter and potentially trigger another transaction execution before the first one completes, leading to double execution or unexpected state changes.

Vulnerability Details

Code Snippet:

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; // State change happens before external call
- (bool success,) = payable(txn.to).call{value: txn.value}(""); // ❌ External call before emitting event
- require(success, "Transaction failed");
- emit TransactionExecuted(_txId, txn.to, txn.value); // ❌ Event emitted AFTER external call
}

Exploit Scenario

  1. Attacker creates a malicious contract that has a fallback function capable of re-entering executeTransaction.

  2. The attacker gets a transaction approved and calls executeTransaction.

  3. The external .call{value: txn.value} sends funds to the attacker's contract.

  4. The attacker's fallback function re-enters executeTransaction before the first call completes, executing the same transaction multiple times before txn.executed = true takes effect.

  5. The attacker withdraws multiple times, effectively draining funds.

PoC:

contract MaliciousContract {
MultiSigWallet public target;
uint256 public txId;
constructor(address _target, uint256 _txId) {
target = MultiSigWallet(_target);
txId = _txId;
}
function attack() external {
target.executeTransaction(txId);
}
fallback() external payable {
if (address(target).balance > 0) {
target.executeTransaction(txId); // Re-entering the function
}
}
}

Impact

Medium Severity: Attackers can re-enter executeTransaction before state changes take effect, leading to double execution or fund loss.
Funds Drain: If the attacker controls an approved transaction, they can continuously withdraw funds by exploiting the reentrancy loophole.
Broken MultiSig Security: A compromised owner or an attacker can bypass the intended transaction execution logic.

Tools Used

Slither (automated static analysis)
Manual Review (to confirm reentrancy risk)

Recommendations

To prevent reentrancy, follow the Checks-Effects-Interactions pattern by:

  1. Updating state before making external calls (✅ Already done in this case).

  2. Emitting events before making external calls (❌ Currently incorrect).

  3. Using OpenZeppelin's ReentrancyGuard to prevent reentrancy.

function executeTransaction(uint256 _txId) external onlyOwners nonReentrant { // ✅ Add reentrancy guard
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; // ✅ State change BEFORE external call
+ emit TransactionExecuted(_txId, txn.to, txn.value); // ✅ Emit event BEFORE external call
+ (bool success,) = payable(txn.to).call{value: txn.value}(""); // ✅ External call LAST
require(success, "Transaction failed");
}

Additional Considerations:

  1. Use ReentrancyGuard (nonReentrant modifier) to prevent multiple function calls in the same transaction.

  2. Emit events before external calls to ensure state consistency.

  3. Limit gas for external calls to prevent complex fallback function execution.

  4. By implementing these fixes, executeTransaction will be secured against reentrancy attacks and prevent potential fund loss or double execution exploits.

Updates

Appeal created

n0kto Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

invalid_reentrancy_with_no_impact

matchRewards: Contract is created just before and is the one called. No impact. executeTransaction: CEI is followed. Emitting an event in disorder is informational in that context. withdraw: CEI is followed.

Support

FAQs

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