AuctionDemo.sol: Bidding on auctions, cancelling bids, and ending/claiming of auctions can occur in the same block, resulting in a myriad of consequences including locking of user funds, refunding users too much ETH, and reentrancy drainage of the contract #915
Labels
3 (High Risk)
Assets can be stolen/lost/compromised directly
bug
Something isn't working
duplicate-1323
edited-by-warden
satisfactory
satisfies C4 submission criteria; eligible for awards
Lines of code
https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L57-L61
https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L104-L143
Vulnerability details
Impact
If a call to
participateToAuction()
(bidding) is included in the same block after the call toclaimAuction()
for that auction, then the user's bid will be stuck in the contract with no easy way to get it out. If a call tocancelBid()
for a specific auction is included in the same block after the call toclaimAuction()
for that auction, then the user will be refunded double the amount of that bid. Draining of the contract via reentrancy is also possible.The only way to retrieve the stuck bidding funds is to exploit the contract.
Proof of Concept
The three functions below implement bidding, claiming, and cancelling a bid:
claimAuction
should not be called until the auction is over, whereas bidding and cancelling bids should not be possible unless the auction is still active. However, notice that while all three functions checkblock.timestamp
againstminter.getAuctionEndTime()
, the checks are all<=
or>=
. This is a problem, becausecancelBid()
andparticipateToAuction()
can be called afterclaimAuction()
ifblock.timestamp
is equal tominter.getAuctionEndTime()
and all the transactions are included in the same block. Note that there are no other checks that prevent this from happening.Note that
cancelBid()
setsauctionInfoData[_tokenid][index].status
tofalse
in order to prevent one bid from being cancelled multiple times (and refunding the user multiple times). However,claimAuction()
doesn't setauctionInfoData[_tokenid][index].status
tofalse
. Therefore, if a user callscancelBid()
and the transaction is executed after the relevantclaimAuction()
transaction, the user will be refunded for double their bid. (If the user happens to be the highest bidder, then the user will be refunded and receive the NFT for free.)Another issue can occur if
participateToAuction()
is called afterclaimAuction()
. In this case, the bid ETH will be stuck in the contract sinceclaimAuction()
can only be executed once for an auction, and the bidder cannot cancel the bid for a refund after the auction has ended.Finally, the most severe consequence is an effectively complete drainage of the funds in
AuctionDemo
. See the example below:claimAuction()
before many bids are refunded back to other users. The total bid amount by the attacker should be approximately half of the total ETH balance of theAuctionDemo
contract. Note that the attacker can bid a greater total amount if anticipating that other users will bid more after the attacker.block.timestamp
does not coincide with the auction ending time, the attacker's bids are refunded to the attacker. Ifblock.timestamp
does coincide with the ending time, then the following steps occur:claimAuction()
is called, and when the last bid from the attacker is refunded to the attacker's contract, the attacker's payable fallback function reenters the contract.AuctionDemo
, callingcancelAllBids()
. (See above code block. This function does the same thing ascancelBid()
, but for all ofmsg.sender
's bids. The attacker's contract can also reentercancelBid()
multiple times to achieve the same effect, or to achieve maximum drainage if the total of the attacker's bids is greater than half the balance ofAuctionDemo
.)AuctionDemo
.claimAuction()
finishes executing, but the low-level ETH transfers to other users fail since the contract's balance is too low.Since none of the
success
bools returned by the low-level transfers are checked, it's easy for the attacker to completely drain the contract;claimAuction()
won't error if it attempts to transfer bids back to other users when the ETH balance of the contract is too low.Note that the attacker does not want to call
cancelAllBids()
before all of the transfers to the attacker inclaimAuction()
occur, sinceclaimAuction()
checks thatauctionInfoData[_tokenid][i].status == true
, andcancelAllBids()
will set these values for the attacker's bids tofalse
.Recommended Mitigation
The fix for all these issues is simple; eliminate the possibility of claiming occurring in the same block as bidding and cancelling bids:
Additional Recommendation
A reentrancy lock should be added to the relevant functions to eliminate the risk of reentrancy vulnerabilities, although it's not strictly necessary to fix the issues here.
Assessed type
Timing
The text was updated successfully, but these errors were encountered: