Normal Behavior
When a user has a stored failed-transfer credit, calling the withdrawAllFailedCredits function should let that user reclaim their own credited ETH: the contract should read the credit for the beneficiary, clear the beneficiary’s stored credit, and transfer the funds to that beneficiary. In short — read the correct mapping key → zero the same key (effects) → send ETH to the rightful recipient (interaction).
Issue
withdrawAllFailedCredits(address _receiver) reads the credit from failedTransferCredits[_receiver] but then zeros and pays failedTransferCredits[msg.sender] (and transfers ETH to msg.sender). Because the mapping key read, the mapping key cleared, and the transfer recipient do not match, an attacker can call the function with _receiver set to a victim who has credits and receive those funds themselves; the victim’s credit remains uncleared, enabling repeated withdrawals and draining protocol funds.
Relevent Github Link: https://github.com/CodeHawks-Contests/2025-09-bid-beasts/blob/449341c55a57d3f078d1250051a7b34625d3aa04/src/BidBeastsNFTMarketPlace.sol#L238C5-L246C6
Likelihood:
Publicly Exposed Function –
withdrawAllFailedCredits is external and callable by anyone. No restrictions = anyone can trigger it.
No Special Conditions –
The exploit doesn’t depend on rare timing, complex reentrancy, or specific on-chain state. It only relies on faulty accounting, unauthorized access, which is always present.
Low Cost to Attack –
The gas cost of repeatedly calling the function is small compared to the potential reward (protocol balance).
Repeatable Drain –
Because credits are not cleared properly, the attacker can loop calls until the contract is drained.
Impact:
Any caller can specify another address that actually has credits and receive those funds instead. This allows repeated theft (drain) until the protocal balance is fully drained.
An attacker created two contracts (ExploitFromSeller and ExploitFromBidder). The Seller contract listed an NFT on the marketplace but intentionally did not implement receive()/payable fallback(), so any ETH sent to it reverts. The Bidder contract calls internally to Seller contract to listNFT() after that immediately executed a placeBid (direct purchase) and became the winner. When the marketplace tried to pay the Seller, the transfer failed and the marketplace recorded the amount in failedTransferCredits[Seller]. Because the withdraw function is buggy, the attacker then repeatedly called withdrawAllFailedCredits in a loop and siphoned the protocol’s ETH.
This fixes the unauthorized access problem by ensuring:
Only the credited user (msg.sender) can withdraw their own funds.
Attackers can no longer withdraw credits that belong to someone else.
withdrawAllFailedCredits allows any user to withdraw another account’s failed transfer credits due to improper use of msg.sender instead of _receiver for balance reset and transfer.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.