Bid Beasts

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

Unauthorized Withdrawal Vulnerability in withdrawAllFailedCredits Function

Root + Impact

Description

  • Normally, the withdrawAllFailedCredits function should allow only the user who owns failed credits to withdraw them. The function is meant to protect users from failed direct transfers by storing their owed balance in failedTransferCredits.

  • However, the implementation mistakenly allows anyone to pass an arbitrary _receiver address and drain its funds. This happens because the function looks up the balance with _receiver but resets the balance of msg.sender, leading to a mismatch.

function withdrawAllFailedCredits(address _receiver) external {
uint256 amount = failedTransferCredits[_receiver];
require(amount > 0, "No credits to withdraw");
@> failedTransferCredits[msg.sender] = 0; // <-- should reset _receiver, not msg.sender
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Withdraw failed");
}

Risk

Likelihood:

  • Whenever any user has a non-zero balance in failedTransferCredits, another user can call this function.

  • Attackers do not need special privileges or ownership of the _receiver account to exploit it.

Impact:

  • Attackers can steal Ether from other users who have pending failed transfers.

  • This leads to a direct loss of funds and undermines trust in the marketplace.

Proof of Concept

The PoC demonstrates how an attacker can exploit the bug in withdrawAllFailedCredits.

Normally, a failed transfer is tracked in failedTransferCredits[user], and only that user should be able to withdraw it.

But in this code:

uint256 amount = failedTransferCredits[_receiver];
failedTransferCredits[msg.sender] = 0;
payable(msg.sender).call{value: amount}("");
  • The function fetches the balance of _receiver (which can be anyone’s address).

  • Then it resets the balance of msg.sender, not _receiver.

  • Finally, it sends the funds to msg.sender, even though they may not own that balance.

So, in the PoC:

  1. Alice has credits in the contract.

  2. Attacker calls withdrawAllFailedCredits(alice).

  3. The contract sends Alice’s credits to the attacker instead of Alice.

This proves an unauthorized withdrawal vulnerability that leads to fund theft.

Recommended Mitigation

The mitigation ensures that only the rightful account holder (msg.sender) can withdraw their stored credits.

By changing:

uint256 amount = failedTransferCredits[_receiver];

to

uint256 amount = failedTransferCredits[msg.sender];

We enforce that:

  • Users can only withdraw their own balance, not anyone else’s.

  • The balance is reduced correctly from their own account.

  • Funds are always transferred to the rightful owner (msg.sender).

This aligns the logic of storage lookup, balance reset, and payout to the same address, closing the loophole.

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

Lead Judging Commences

cryptoghost Lead Judge 26 days 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.