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

Race condition allows unfair token distribution after auction end

Summary

Bid can be placed after the auction has ended, allowing a malicious user to exploit the multiplier calculation and withdraw more tokens than they should have received at the expense of other users.

Vulnerability Details

At the end of the auction, the system calculates a multiplier using the formula totalTokens / totalBids, which is then stored as a state variable. This multiplier determines how many tokens each bidder receives based on their bids. Typically, with each subsequent bid, this multiplier should decrease.

function auctionEnd() external {
if (block.timestamp < auctionEndTime) {
revert AuctionNotYetEnded();
}
if (ended) {
revert AuctionEndAlreadyCalled();
}
ended = true;
emit AuctionEnded(totalBids, totalTokens);
if (totalBids == 0) {
auctionToken.transfer(owner, totalTokens);
return;
}
multiplier = totalTokens.mul(PRECISION_18).div(totalBids);
...
}

https://github.com/Cyfrin/2024-08-fjord/blob/14cab810598ddda6008d9523d0ed4a428b1b1153/src/FjordAuction.sol#L181-L202

However, when block.timestamp equals auctionEndTime and ended is set to true, the contract still allows users to place bids. A malicious actor can exploit this by first calling auctionEnd to lock in the multiplier, and then placing a bid within the same block. This bid is accepted without adjusting the multiplier, allowing the malicious user to claim more tokens than their fair share, leaving fewer tokens for legitimate bidders.

function bid(uint256 amount) external {
if (block.timestamp > auctionEndTime) {
revert AuctionAlreadyEnded();
}
bids[msg.sender] = bids[msg.sender].add(amount);
totalBids = totalBids.add(amount);
fjordPoints.transferFrom(msg.sender, address(this), amount);
emit BidAdded(msg.sender, amount);
}

https://github.com/Cyfrin/2024-08-fjord/blob/14cab810598ddda6008d9523d0ed4a428b1b1153/src/FjordAuction.sol#L143-L153

Consider a scenario when:

  • Alice is the sole bidder, placing a bid of 100 fjordPoints for 100 auction tokens.

  • As the auction ends (when block.timestamp equals auctionEndTime), Bob (a malicious user) calls auctionEnd. The system calculates a multiplier of 1e18 (totalTokens / totalBids).

  • Immediately after, within the same block, Bob places a bid of 100 fjordPoints. Despite the bid, the multiplier remains 1e18.

  • Bob then calls claimTokens, withdrawing 100 auction tokens. As a result, Alice is left with no tokens to withdraw, despite her legitimate bid.

Impact

This vulnerability results in an unfair token distribution, allowing malicious actors to disproportionately benefit at the expense of legitimate participants. This can lead to significant financial losses for honest bidders who receive fewer tokens than expected or none at all.

Tools Used

Manual

Recommendations

Ensure that users cannot place bids after the auction has ended by modifying the bid function to include a stricter time check. Specifically, update the condition to check if block.timestamp is greater than or equal to auctionEndTime, or validate using the ended flag.

function bid(uint256 amount) external {
- if (block.timestamp > auctionEndTime) {
+ if (block.timestamp >= auctionEndTime) {
revert AuctionAlreadyEnded();
}
// ... rest of the function
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months 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.