Summary
The contract's refund logic can be blocked if the receiving address (msg.sender) is a contract that cannot accept ETH transfers (i.e., lacks a receive() or fallback() function), potentially leading to stuck transactions and locked funds.
Vulnerability Details
The vulnerability exists in both trickOrTreat()
and resolveTrick()
functions where refunds are processed:
if (totalPaid > requiredCost) {
uint256 refund = totalPaid - requiredCost;
(bool refundSuccess,) = msg.sender.call{value: refund}("");
require(refundSuccess, "Refund failed");
}
Example of a contract that would cause this issue:
contract NonReceivingContract {
SpookyTreats spookyTreats;
constructor(address _spookyTreats) {
spookyTreats = SpookyTreats(_spookyTreats);
}
function buyTreat(string memory treatName) external payable {
spookyTreats.trickOrTreat{value: 1 ether}(treatName);
}
}
Impact
The vulnerability can lead to several serious issues:
-
Denial of Service:
Malicious contracts can block their own transactions
This could be used to manipulate the NFT market
Could prevent legitimate sales from completing
-
Locked Funds:
-
Failed Transactions:
Tools Used
Manual code review
Recommendations
Pull Over Push Pattern
contract SpookySwap {
mapping(address => uint256) public pendingRefunds;
function trickOrTreat(string memory _treatName) public payable {
if (msg.value > requiredCost) {
pendingRefunds[msg.sender] += msg.value - requiredCost;
}
}
+ function withdrawRefund() external {
uint256 refundAmount = pendingRefunds[msg.sender];
require(refundAmount > 0, "No refund available");
pendingRefunds[msg.sender] = 0;
(bool success,) = msg.sender.call{value: refundAmount}("");
require(success, "Withdrawal failed");
}
}
+ mapping(address => uint256) public pendingRefunds;
function trickOrTreat(string memory _treatName) public payable {
// ... existing code ...
- if (totalPaid > requiredCost) {
- uint256 refund = totalPaid - requiredCost;
- (bool refundSuccess,) = msg.sender.call{value: refund}("");
- require(refundSuccess, "Refund failed");
- }
+ if (msg.value > requiredCost) {
+ pendingRefunds[msg.sender] += msg.value - requiredCost;
+ }
}
+ function withdrawRefund() external {
+ uint256 refundAmount = pendingRefunds[msg.sender];
+ require(refundAmount > 0, "No refund available");
+
+ pendingRefunds[msg.sender] = 0;
+ (bool success,) = msg.sender.call{value: refundAmount}("");
+ require(success, "Withdrawal failed");
+}