Losing bids can be refunded twice #665
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/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L124-L130
Vulnerability details
Impact
At
block.timestamp == minter.getAuctionEndTime(_tokenid)
, it is possible to call both of these functions. This oversight creates an opportunity for a severe theft of funds.Proof of Concept
claimAuction
.claimAuction
refunds his first bid, which triggers Adversary's fallback, which callscancelBid
, which successfully refunds Adversary's bid again. The same happens for each of adversary's bids, except the winning one.Example of Adversary's fallback:
Full Foundry PoC (works with and without
require(success)
)Depending on whether the mitigation for the M-01 finding from the bot race is implemented, there are two ways of how the Step 4 will play out:
require(success)
is not implemented:cancelBid
for the respective bid.require(success)
is implemented:require(success)
after each call will make sure that every ether transfer has to be successful, or the whole transaction reverts. This means that the owner and all other bidders must get their ether back. If there is only one auction, and the Adversary tries to execute the original attack, the txn will revert, as at least one of the refunds will fail (due to insufficient balance of AuctionDemo).Thus, the attack will not work unless there are other funds in the contract, but there usually will be, because the AuctionDemo contract may host multiple auctions at once.
To sum up:
if
require(success)
is used, the Adversary will need at least one other auction going in parallel for the attack to be successful;if
require(success)
is not used, the attack won't have such requirement and will work out without other auctions.Option to cancel the winning bid
The Adversary can also decide if he wants to pay for the NFT, or just get his winning bid back. Similarly to the previous strategy, he may add a call to his fallback, that would cancel his winning bid. This will essentially make the owner keep the NFT, and refund every bidder of the auction.
If the winning bid is cancelled, the condition will always be false, as the highest bid will have
status == false
Draining of all other auctions
The contract also allows to make bids at
t == minter.getAuctionEndTime(_tokenid)
. So the doubling strategy can be upgraded to draining all other auctions' funds.Scenario:
Steps 3-6 are repeated, each time with a bigger bid.
However, this particular scenario becomes impossible if the array length is cached (G-20 from the bot report).
Tools Used
Foundry
Recommended Mitigation Steps
Assessed type
Timing
The text was updated successfully, but these errors were encountered: