Bid Beasts

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

Critical Unauthorized Withdrawal in withdrawAllFailedCredits()

Root + Impact

Description

  • The normal behavior of withdrawAllFailedCredits should allow a user to withdraw their own failed transfer credits that accumulated when a direct transfer to them failed.

  • The specific issue is that the function uses an arbitrary _receiver address parameter to read the credit amount, but then sends this amount to msg.sender while zeroing out msg.sender's credits. This allows any user to steal another user's credits.

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");
}

Risk

Likelihood: High

  • Any user with knowledge of another user's address who has accumulated failed transfer credits can extract those credits.

  • The vulnerability is trivially exploitable with a single transaction and no special conditions required beyond knowing an address with credits.

Impact: Critical

  • Complete theft of accumulated failed transfer credits from any user.

  • Double-spending of contract funds, potentially leading to contract insolvency when legitimate users attempt to withdraw their credits.

Proof of Concept

The vulnerability in withdrawAllFailedCredits allows any user to steal another user's failed transfer credits. Here's a detailed explanation of how the exploit works:
The PoC demonstrates a real-world attack scenario where an attacker creates a contract to steal another user's failed transfer credits:

  1. Setup: The attack assumes Alice has accumulated failed transfer credits (e.g., 10 ETH) in the marketplace contract.

  2. Attack Vector: The vulnerability stems from the parameter confusion in the withdrawAllFailedCredits function, where the function:

    • Takes an arbitrary address as input (_receiver)

    • Reads the credits amount from that address's balance

    • But zeroes out the caller's balance

    • And sends the funds to the caller

  3. Execution Flow:

    • Attacker deploys the Exploit contract targeting the marketplace

    • Calls attack(alice_address) which executes withdrawAllFailedCredits(alice_address)

    • The function reads Alice's credit balance (10 ETH)

    • Zeroes the Exploit contract's credits (which were likely 0 anyway)

    • Transfers 10 ETH to the Exploit contract

  4. Result:

    • The attacker now possesses Alice's 10 ETH

    • Alice's credits remain intact in storage (they were never zeroed)

    • When Alice attempts to withdraw her credits later, the contract will try to pay her again

    • This creates a double-spending situation that could drain the contract

    • The exploit requires no special conditions or permissions, making it extremely easy to execute against any user with accumulated failed transfer credits.

// Assume Alice has 10 ETH in failedTransferCredits
// Attacker contract
contract Exploit {
BidBeastsNFTMarket target;
constructor(address _target) {
target = BidBeastsNFTMarket(_target);
}
function attack(address victim) external {
// Steal victim's failed transfer credits
target.withdrawAllFailedCredits(victim);
}
receive() external payable {}
}
// Attack flow:
// 1. Deploy Exploit contract
// 2. Call attack(alice_address)
// 3. Attacker now has Alice's credits
// 4. Alice can still withdraw the same amount later, potentially draining the contract

Recommended Mitigation

The proposed mitigation fixes the fundamental design flaw in the withdrawAllFailedCredits function by ensuring users can only withdraw their own failed transfer credits:

  1. Remove the Parameter:

    • Eliminate the _receiver parameter entirely, which removes the ability to specify another user's address

    • This ensures users can only interact with their own credits

  2. Self-Referential Logic:

    • Use msg.sender consistently throughout the function for both:

    • Reading the credit amount

    • Zeroing the appropriate balance

    • Sending the funds to the rightful owner

  3. Function Renaming:

    • Change the function name to withdrawFailedCredits to better reflect its purpose and avoid confusion

  4. Benefits of the Fix:

    • Eliminates the possibility of credit theft

    • Ensures proper accounting of funds

    • Prevents potential contract insolvency from double-spending

    • Maintains the original functionality of allowing users to withdraw their failed credits

    • Follows the principle of least privilege by restricting users to their own assets

- 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 withdrawFailedCredits() 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 21 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.