The users can either bid/unbid if the auction period is active and if it ends they can't and we should close the auction at this time. The problem is that this check is not handled correctly.
FjordAuction.sol#L143-L146 | FjordAuction.sol#L159-L162 | FjordAuction.sol#L181-L184
Both bid() and unbid() functions check that block.timestamp is not greater than auctionEndTime, i.e it should be smaller than or equal auctionEndTime.
auctionEnd() checks that block.timestamp is not smaller than auctionEndTime, i.e it should be greater than or equal auctionEndTime.
The issue is that there is one second which is when block.timestamp = auctionEndTime allows us to call both bid/unbid() and auctionEnd(). This will allow users to End the auction then Bid once again and this is what the issue comes from.
The auction process depends on the distribution of auctiontokens according to the amount of fjordToken deposited by users. If we said that the auction starts with total auctionToken = 10 and there is a total of 10 fjordTokens, then for each 1 fjordToken bidding the bidder will claim 1 auction token.
The amount of auctionToken to fjordToken bidder is determined by multiplier which is calculated when ending the auction via auctionEnd().
Now A malicious user can simply unbid() his tokens to decrease the totalBids value, call auctionEnd() which will result in larger multiblier, call bid() again to receive more tokens than he deserves, preventing other users from claiming there tokens.
Auction starts
There are 10 auction tokens
UserA bids 5 fjord
UserB bids 5 fjord
Attacker bids 5 fjord
No one bids more as it is not profitable anymore
The auction is about to end, and for each 1 fjord, users will receive 10/15 ~= 0.667 auction token.
Each bidder should receive 5 * 0.667 ~= 3.33 auctionToken.
The attacker prepared his malicious transaction and executed it just before the auction ends and it gets executed at block.timestamp = auctionEndTime
Attacker called unbid() taking his 5 fjord
totalBids are now 15 - 5 = 10
Attacker called auctionEnd()
multiplier = totalTokens / totalBids = 10 / 10 = 1
This means for each 1 fjord token we will receive 1 auctionToken not 0.667
Attacker called bid() bidding his 5 fjord tokens he unbidded
Now UserA, UserB, and Attacker have 5 fjord tokens
Attacker just called claimTokens()
Attacker claimable amount = bid * multiplier = 5 * 1 = 5
The attacker managed to receive 5 auctionToken instead of 3.33
Time passes and UserB called claimTokens(), and he also claimed 5 tokens
auctionTokens are not 0 and if UserA tried to claimTokens() it will revert because there is no tokens left
since there is a new block each ~10-13 seconds the possibility of the attack ranges from 7% to 10% of success when trying to do it (not a small percentage).
Attack flow will get executed as MultiCall using either an attacker contract, Smart Contract wallet, AA wallet.
The Attacker can even use this advantages in price to not just take more than he deserves but to drain all the auction with less price. in our example he can bid 10 instead of 5 which will make him pay 10 fjordTokens to take 10 auction when they acctually worth 15 fjordTokens. which will result in the loss of funds for all other bidders not just some of them
Attacker will take more than he deserves.
Preventing other bidders or even all of them from receiving there auctionTokens they deserve.
Manual Review
Prevet bidding and unbidding if block.timestamp equals auctionEndTime.
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.