The auction protocol will transfer the NFT to highest bidder when owner ends the auction. Owner shall then be able to withdraw the highest bid amount whilst other bidders could withdraw their unsuccessful bid tokens back. However, highest biddest could still call the withdraw
function, allowing them to get back their bid token amount and still owns the NFT towards the ends. As the highest bidder makes the withdrawal which they are not supposed to, it causes other users including the owner who is eligible to make the withdraw could fail to withdraw their tokens back due to insufficient balance in the protocol.
#[test]
#[should_panic(expected: 'ERC20: insufficient balance')]
fn test_audit_highest_bidder_can_still_withdraw() {
let (auction_dispatcher, auction_contract, erc20_contract_address, erc721_contract_address) = deploy_auction_contract();
let erc20_dispatcher = IMockERC20TokenDispatcher { contract_address: erc20_contract_address };
let erc721_dispatcher = IERC721Dispatcher { contract_address: erc721_contract_address };
auction_dispatcher.start(86400, 10);
let first_bidder_address: ContractAddress = 123.try_into().unwrap();
start_cheat_caller_address_global(first_bidder_address);
erc20_dispatcher.mint(first_bidder_address, 15);
erc20_dispatcher.token_approve(auction_contract, 15);
auction_dispatcher.bid(15);
stop_cheat_caller_address_global();
let second_bidder_address: ContractAddress = 456.try_into().unwrap();
start_cheat_caller_address_global(second_bidder_address);
erc20_dispatcher.mint(second_bidder_address, 18);
erc20_dispatcher.token_approve(auction_contract, 18);
auction_dispatcher.bid(18);
let erc20_balance_after_all_bids = erc20_dispatcher.token_balance_of(auction_contract);
println!("erc20_balance_after_all_bids: {}", erc20_balance_after_all_bids);
let erc20_balance_second_bidder_after_bid = erc20_dispatcher.token_balance_of(second_bidder_address);
assert(erc20_balance_second_bidder_after_bid == 0, 'Balance must be 0');
println!("erc20_balance_second_bidder_after_bid: {}", erc20_balance_second_bidder_after_bid);
let nft_balance_second_bidder_after_bid = erc721_dispatcher.balance_of(second_bidder_address);
assert(nft_balance_second_bidder_after_bid == 0, 'NFT balance must be 0');
println!("nft_balance_second_bidder_after_bid: {}", nft_balance_second_bidder_after_bid);
stop_cheat_caller_address_global();
let time = get_block_timestamp();
start_cheat_block_timestamp(auction_contract, time + 86401);
start_cheat_caller_address_global(auction_contract);
erc20_dispatcher.token_approve(auction_contract, 18);
erc20_dispatcher.token_approve(first_bidder_address, 15);
erc20_dispatcher.token_approve(second_bidder_address, 18);
erc721_dispatcher.approve(second_bidder_address, 1);
stop_cheat_caller_address_global();
auction_dispatcher.end();
start_cheat_caller_address_global(second_bidder_address);
auction_dispatcher.withdraw();
stop_cheat_caller_address_global();
let erc20_balance_second_bidder_after_withdraw = erc20_dispatcher.token_balance_of(second_bidder_address);
assert(erc20_balance_second_bidder_after_withdraw == 18, 'Balance must be 18');
println!("erc20_balance_second_bidder_after_withdraw: {}", erc20_balance_second_bidder_after_withdraw);
let nft_balance_second_bidder_after_withdraw = erc721_dispatcher.balance_of(second_bidder_address);
assert(nft_balance_second_bidder_after_withdraw == 1, 'NFT balance must be 1');
println!("nft_balance_second_bidder_after_withdraw: {}", nft_balance_second_bidder_after_withdraw);
auction_dispatcher.withdraw();
stop_cheat_block_timestamp(auction_contract);
}
The test passes, indicating that highest bidder is able to withdraw the bid amount of 18 and yet still owns the NFT. Whilst the owner who only makes the withdraw
call after the highest bidder will fail to withdraw due to insufficient balance in the protocol. The test case supported the panic via #[should_panic(expected: 'ERC20: insufficient balance')]
Highest bidder could still withdraw the bid token despite already successfully bidding and owning the NFT whilst users who are eligible to withdraw their bid tokens back could fail to withdraw due to insufficient balance left in the protocol
fn withdraw(ref self: ContractState) {
assert(self.started.read(), 'Auction is not started');
assert(self.ended.read(), 'Auction is not ended');
let caller = get_caller_address();
let sender = get_contract_address();
let erc20_dispatcher = IERC20Dispatcher { contract_address: self.erc20_token.read() };
let amount = self.bid_values.entry(caller).read();
let amount_owner = self.highest_bid.read();
if caller == self.nft_owner.read() {
self.highest_bid.write(0);
erc20_dispatcher.transfer_from(sender, caller, amount_owner.into());
}
if amount > 0 {
+ assert(caller != self.highest_bidder.read(), 'Not eligible to withdraw');
let sender = get_contract_address();
erc20_dispatcher.transfer_from(sender, caller, amount.into());
}
self.emit(Withdraw {amount: amount, caller: caller});
}