DeFiFoundry
20,000 USDC
View results
Submission Details
Severity: medium
Valid

Users can bid or unbid at the same moment an auction ends

Summary

The condition to check if a user can bid or unbid intersects with the condition to end an auction, allowing these actions to be executed concurrently when the current timestamp matches the auction end time.

Vulnerability Details

In the FjordAuction contract, users are allowed to bid or unbid until the auctionEndTime timestamp:

143: function bid(uint256 amount) external {
144: if (block.timestamp > auctionEndTime) {
145: revert AuctionAlreadyEnded();
146: }
159: function unbid(uint256 amount) external {
160: if (block.timestamp > auctionEndTime) {
161: revert AuctionAlreadyEnded();
162: }

Note here that the limit is inclusive , i.e. the action is allowed if block.timestamp == auctionEndTime.

Similarly, the function to end an auction checks the auctionEndTime timestamp:

181: function auctionEnd() external {
182: if (block.timestamp < auctionEndTime) {
183: revert AuctionNotYetEnded();
184: }

Note here that the limit is inclusive too, and the function can also be called when block.timestamp == auctionEndTime.

This intersection in the conditions of the validations allow the bid() and unbid() functions to be called after the auctionEnd() function has been executed.

Impact

Unbidding is likely to fail if bundled after the call to end an auction since ending an auction burns all of the fjord points, causing the transfer in bid to fail.

However, bidding would certainly lead to issues as a user can bid after the multiplier has been fixed, allowing them to enter the auction without affecting the total amount of bids.

If executed maliciously, this can be used to bid any amount of points without affecting the multiplier, and will ultimately cause accounting issues as not all auctioned tokens will be enough to cover all of the bidders positions. Eventually, one or more honest bidders will be left with nothing to claim.

Proof of Concept

As a quick example to show the issue, let's say there are 100 tokens to auction and users bidded 100 fjord points. A malicious user could submit the following batch of actions at the auction end timestamp:

  1. Call auctionEnd() to end the auction and fix the multiplier, which will be 1 (1e18) since total tokens equals total bids.

  2. Call bid() with 100 fjord points (the same as the current total bids). Here totalBids is increased but since the multiplier has been fixed, the modification has no effect.

  3. Call claimTokens() to withdraw the auction tokens. The position of the attacker will be calculated as 100 tokens (100 * 1e18 / 1e18), effectively withdrawing the total of the auctioned tokens. All honest bidders will be left with nothing to claim.

Important to note that step 2 doesn't really require to send an equal amount of points as the total bidded, the attacker can take advantage of the fixed multiplier with any given amount of points, and would also result in a loss for a honest bidder as some portion of the auctioned tokens will be redirected to the attacker.

Tools Used

None.

Recommendations

Change the condition in the auctionEnd() function so that the check also discards the exact auctionEndTime moment, avoiding the clashing with bid() or unbid().

function auctionEnd() external {
- if (block.timestamp < auctionEndTime) {
+ if (block.timestamp <= auctionEndTime) {
revert AuctionNotYetEnded();
}
Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Users can bid in the same block when the actionEnd could be called (`block.timestamp==actionEndTime`), depending on the order of txs in block they could lose funds

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.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!