Both the bid() and auctionEnd() functions can be called when block.timestamp == auctionEndTime. This can lead to a malicious attacker being able to close the auction first and then bid. Which will lead to loss of funds for all other users of the auction.
Assume that many users have bid in an auction, and the block.timestamp == auctionEndTime. At this point a malicious attacker(bob) calls the auctionEnd() function which will calculate the multiplier and end the auction. In the same transaction(block.timestamp is still equal to auctionEndTime) bob calls the bid() function too. So Bob's bid gets registered and then when Bob calls the claimTokens() function, he will get tokens corresponding to his bid. This however will lead to some/all of the other users not getting the auction tokens. And when Bob calls the unbid function in the same transaction he gets his FjordPoints refunded too.
Scenario
Auction has started with bidding time = 1week, and 1000e18 reward tokens.
Alice bids 20e18 FjordPoints.
Auction reaches the end time. (Now block.timestamp == AuctionEndTime).
Bob calls the auctionEndTime() function which ends the auction.In this function the multiplier is calculated as (https://github.com/Cyfrin/2024-08-fjord/blob/0312fa9dca29fa7ed9fc432fdcd05545b736575d/src/FjordAuction.sol#L181-L202)
This means with alice's bid of 20e18, and multiplier = 50*PRECISION_18, when alice claims tokens, she should recieve all of it.
Bob calls the bid() function in the same transaction and bids 20e18 FjordPoints. (https://github.com/Cyfrin/2024-08-fjord/blob/0312fa9dca29fa7ed9fc432fdcd05545b736575d/src/FjordAuction.sol#L143-L153)
Bob then calls the claimTokens() function which uses bob's bid amount (20e18) and multiplier. Bob is rewarded 1000e18 reward tokens. (https://github.com/Cyfrin/2024-08-fjord/blob/0312fa9dca29fa7ed9fc432fdcd05545b736575d/src/FjordAuction.sol#L207-L222)
Bob then calls the unbid() function in the same transaction. Here bob is refunded his FjordPoints.
When Alice calls the claimTokens() function, it will revert since bob has already drained the funds. Leading alice to face a loss of 20e18 Fjord Points (the amount she bid)
Note: If bob doesnt call the unbid function, this also leads to Bob's bid (20e18) to be forever stuck in the FjordAuction contract. Since it cant be burned ever again, this will affect the totalSupply() of the FjordToken. This will lead to limited supply of FjordPoints in the openmarket.
POC
To run the POC
1. Add the following function in FjordPoints.sol (this is needed since while testing, "deal" doesnt increase the totalSupply so mint is to be used).
Replace the entire test/unit/auction.t.sol file with the below code snippet.
OUTPUT
We can see that Alice gets nothing but Bob gets everything.
Bob gets free auct tokens, and it also causes a loss of funds for other users, and some of the FjordPoints forever being stuck in FjordAuction.sol (affecting the totalSupply).
This is of high/critical severity since any user(bob) can steal 100% of the auct tokens free of cost(disregarding gas cost).
Manual Review
To mitigate this issue, change the timestamp check in auctionEnd() function as follows(This will prevent anyone from ending the auction and bidding thereafter):
The protocol doesn't properly treat the `block.timestamp == auctionEndTime` case. Impact: High - There are at least two possible impacts here: 1. By chance, user bids could land in a block after the `auctionEnd()` is called, not including them in the multiplier calculation, leading to a situation where there are insufficient funds to pay everyone's claim; 2. By malice, where someone can use a script to call `auctionEnd()` + `bid(totalBids)` + `claimTokens()`, effectively depriving all good faith bidders from tokens. Likelihood: Low – The chances of getting a `block.timestamp == auctionEndTime` are pretty slim, but it’s definitely possible.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.