The normal behavior is that a user should only be able to withdraw their own accumulated failedTransferCredits, and the contract should decrease that user’s credit balance atomically upon payout.
The issue is in withdrawAllFailedCredits(address _receiver): the function reads credits from _receiver, but zeros out msg.sender’s bucket and then pays msg.sender. The credited bucket for _receiver is never decremented, enabling repeated withdrawals by anyone (not just _receiver) as long as the contract holds ETH.
Likelihood:
Occurs whenever failedTransferCredits[_receiver] becomes non-zero (e.g., a refund/payout failed and was credited).
Any caller can trigger the withdrawal by passing _receiver with a positive balance, regardless of caller identity.
Impact:
Unauthorized withdrawal: An attacker calls with _receiver = victim address → receives the victim’s credits to attacker’s address.
Double-withdrawal: Since _receiver’s bucket is not decremented, the same credits can be withdrawn repeatedly until the contract ETH is depleted.
The bug comes from mixing _receiver (victim’s slot) with msg.sender (attacker). The contract always pays msg.sender but never reduces _receiver’s balance, creating unlimited withdrawal opportunities for attackers.
The fix removes the _receiver parameter entirely. Users can now only withdraw their own credits (msg.sender). The correct bucket is read and then cleared before funds are transferred. This prevents attackers from targeting other users’ balances and stops infinite re-withdrawals.
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.