Bid Beasts

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

Unauthorized withdrawal

Root: withdrawAllFailedCredits(address _receiver) reads failedTransferCredits[_receiver] but clears/sends to msg.sender (failedTransferCredits[msg.sender] = 0) — wrong mapping key / state update.

Impact: Any attacker can withdraw another user’s credits and repeat the call (victim’s balance not cleared), enabling repeated theft of contract-held ETH.


Description** **

WithdrawAllFailedCredits should allow a user to withdraw only their own failed credits, reducing their balance and transferring the correct amount securely. The function reads from _receiver but clears msg.sender, creating a mismatch that lets attackers drain other users’ balances and repeatedly steal ETH.



// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

  • Any EOA can call withdrawalfailedcredits and receive funds

  • Contract holds non tribal ETH and multiple users have credit

Impact:

  • Loss of user fund

  • Trust collapse

Proof of Concept

// PoC: Unauthorized withdrawal of another user's credits
// Assume Victim has 10 ETH stored in failedTransferCredits[victim]
contract Attacker {
BidBeastsNFTMarket market;
address victim;
constructor(address _market, address _victim) {
market = BidBeastsNFTMarket(_market);
victim = _victim;
}
// Attack function
function drain() external {
// Call withdrawAllFailedCredits with victim as _receiver
// Credits are read from victim but cleared for msg.sender
market.withdrawAllFailedCredits(victim);
// Attacker (msg.sender) receives victim’s credits
}
}

Recommended Mitigation

- 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() external nonReentrant {
uint256 amount = failedTransferCredits[msg.sender];
require(amount > 0, "No credits to withdraw");
// Effects: clear before external call
failedTransferCredits[msg.sender] = 0;
// Interaction
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Withdraw failed");
}
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.