Bid Beasts

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

Stealing of people's failed credit

Root + Impact

Description

  • Normal Behaviour: Users withdraw their own failed-transfer credit(pull payments) - the contract reads the stored credit for the caller and sends it to that caller.

  • Issue: The function reads <failedTransferCredits[_receiver] but zeroes and pays <msg.sender>. This allows any caller to request withdrawl of another address credits and receive them.

// Root cause in the codebase with @> marks to highlight the relevant section
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:

  • Reason 1 A caller can repeatedly call withdrawAllFailedCredits(victim) when victim has credits (occurs whenever any failed payouts exist for victim).

  • Reason 2: Automated bots or any scripts will easily abuse this because only one straightforward call is required.

Impact:

  • Impact 1: Permanent loss of funds for victims (credits moved to attacker)

  • Impact 2: Severr trust/legal consequences- marketplace users lose funds and reputation of the platform is destroyed

Proof of Concept

// PoC: attacker steals victim's failed-transfer credits by calling withdrawAllFailedCredits(victim).
// Steps (Ethers / Foundry style pseudo-code):
// 1) Assume victim has a failed credit of 1 ether recorded:
// failedTransferCredits[victim] == 1 ether
//
// 2) Attacker calls the vulnerable function specifying the victim's address:
await marketplace.connect(attacker).withdrawAllFailedCredits(victim.address);
// 3) Internal behavior (matching vulnerable code):
// uint256 amount = failedTransferCredits[_receiver]; // reads victim's 1 ETH
// require(amount > 0, "No credits to withdraw");
// failedTransferCredits[msg.sender] = 0; // clears attacker's slot (likely 0)
// (bool success, ) = payable(msg.sender).call{value: amount}(""); // pays attacker
// Result:
// - Attacker receives 1 ETH.
// - Mapping slot failedTransferCredits[victim] remains unchanged (still shows 1 ETH),
// or bookkeeping is inconsistent. Victim lost funds; attacker profited.
//
// Explanation:
// - The contract reads credits using the caller-specified `_receiver` but writes and pays `msg.sender`.
// - Because of that mix-up, specifying any address that has credits causes the contract to send those credits to the caller.
// - This requires only one external call and will succeed whenever a non-zero credit exists for the supplied victim address.

Recommended Mitigation

- remove this code
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");
}
//looks up how much ETH is credited to the _receiver address
//It requires that the amount is > 0.
//It then clears the balance for msg.sender, not for _receiver and sends the victim’s credits (amount) to msg.sender
+ add this code
/// @notice Emitted when a user successfully withdraws their credited funds.
event FailedCreditWithdrawn(address indexed who, uint256 amount);
/// @notice Withdraw caller's failed-transfer credits (pull pattern).
/// @dev Uses Checks-Effects-Interactions and nonReentrant to prevent reentrancy.
function withdrawFailedCredits() external nonReentrant {
uint256 amount = failedTransferCredits[msg.sender];
require(amount > 0, "No credits to withdraw");
// Effects: clear the mapping before external call
failedTransferCredits[msg.sender] = 0;
// Interaction: attempt to send funds to the caller
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Withdraw failed");
emit FailedCreditWithdrawn(msg.sender, amount);
}
//The fixed code removes the ability to specify someone else’s address, makes the mapping authoritative, clears state before sending funds, and blocks reentrancy — together these changes eliminate the original theft vector and make withdrawals safe and auditable.
Updates

Lead Judging Commences

cryptoghost Lead Judge 2 months 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.

Give us feedback!