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

Auction ends before the end time in FjordAuction contract

Summary

The FjordAuction::auctionEnd function in the contract has a vulnerability where the auction can potentially be ended before the auction end time. This issue arises because the contract does not enforce the auction end time correctly, which can lead to unauthorized calls to the auctionEnd function before the auction's designated end time.

Vulnerability Details

The contract's auctionEnd function should prevent the auction from being ended before the predefined end time. However, the test testCannotEndAuctionBeforeEndTime fails to revert as expected, indicating that the auctionEnd function does not enforce the auction end time correctly.

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

Impact

If an attacker or unauthorized user can call auctionEnd before the auction's end time, it could disrupt the auction process, potentially leading to unfair outcomes or loss of auction integrity.

I have created POC for demonstrating the vulnerability regarding auction ends before the end time. I have created test script FjordAuctionTest.t.soland added function testCannotEndAuctionBeforeEndTime().

// SPDX-Licens'e-Identifier: AGPL-3.0-only
pragma solidity =0.8.21;
import "lib/forge-std/src/Test.sol";
import { FjordAuction } from "../src/FjordAuction.sol";
import { ERC20Mock } from "lib/openzeppelin-contracts/contracts/mocks/token/ERC20Mock.sol";
import { ERC20BurnableMock } from "./mocks/ERC20BurnableMock.sol";
contract FjordAuctionTest is Test {
ERC20Mock public auctionToken;
ERC20BurnableMock public fjordPoints;
FjordAuction public auction;
function setUp() public {
// Initialize ERC20Mock with appropriate parameters
auctionToken = new ERC20Mock(); // Adjust the constructor if needed
fjordPoints = new ERC20BurnableMock("FjordPoints", "FP");
auction = new FjordAuction(address(fjordPoints), address(auctionToken), 2 days, 1000 ether);
// Mint tokens for testing
fjordPoints.mint(address(this), 1000 ether); // Mint tokens to this contract
fjordPoints.approve(address(auction), 1000 ether); // Approve tokens for auction contract
auctionToken.mint(address(this), 1000 ether); // Mint tokens to this contract
auctionToken.transfer(address(auction), 1000 ether); // Transfer tokens to auction contract
}
function testCannotDeployWithInvalidFjordPointsAddress() public {
vm.expectRevert(FjordAuction.InvalidFjordPointsAddress.selector);
new FjordAuction(address(0), address(auctionToken), 2 days, 10 ether);
}
function testCannotDeployWithInvalidAuctionTokenAddress() public {
vm.expectRevert(FjordAuction.InvalidAuctionTokenAddress.selector);
new FjordAuction(address(fjordPoints), address(0), 1 days, 10 ether);
}
function testCannotBidAfterAuctionEnded() public {
vm.warp(block.timestamp + 2 days);
vm.expectRevert(FjordAuction.AuctionAlreadyEnded.selector);
auction.bid(100 ether);
}
function testCannotUnbidMoreThanBidAmount() public {
fjordPoints.approve(address(auction), 100 ether);
auction.bid(100 ether);
vm.expectRevert(FjordAuction.InvalidUnbidAmount.selector);
auction.unbid(200 ether);
}
function testCannotUnbidWithoutBids() public {
vm.expectRevert(FjordAuction.NoBidsToWithdraw.selector);
auction.unbid(100 ether);
}
function testCannotClaimTokensBeforeAuctionEnd() public {
vm.expectRevert(FjordAuction.AuctionNotYetEnded.selector);
auction.claimTokens();
}
function testCannotClaimTokensWithNoBids() public {
vm.warp(block.timestamp + 1 days);
auction.auctionEnd();
vm.expectRevert(FjordAuction.NoTokensToClaim.selector);
auction.claimTokens();
}
function testCannotEndAuctionBeforeEndTime() public {
vm.expectRevert(FjordAuction.AuctionNotYetEnded.selector);
auction.auctionEnd();
}
function testCannotEndAuctionTwice() public {
vm.warp(block.timestamp + 1 days);
auction.auctionEnd();
vm.expectRevert(FjordAuction.AuctionEndAlreadyCalled.selector);
auction.auctionEnd();
}
function testClaimTokensWithNoAuctionTokens() public {
// Deploy a fresh auction with no tokens
FjordAuction auctionWithNoTokens = new FjordAuction(
address(fjordPoints),
address(auctionToken),
2 days,
0
);
// Mint and transfer tokens to this contract
fjordPoints.mint(address(this), 100 ether);
fjordPoints.approve(address(auctionWithNoTokens), 100 ether);
auctionWithNoTokens.bid(100 ether);
// Fast forward time to end the auction
vm.warp(block.timestamp + 2 days);
auctionWithNoTokens.auctionEnd();
// Record initial balance
uint256 initialBalance = auctionToken.balanceOf(address(this));
// Attempt to claim tokens
try auctionWithNoTokens.claimTokens() {
assertEq(auctionToken.balanceOf(address(this)), initialBalance);
} catch (bytes memory reason) {
assert(false);
}
}
function testBidAtExactAuctionEndTime() public {
FjordAuction shortAuction = new FjordAuction(
address(fjordPoints),
address(auctionToken),
1 minutes,
100 ether
);
vm.warp(block.timestamp + 1 minutes);
fjordPoints.approve(address(shortAuction), 100 ether);
shortAuction.bid(100 ether);
assertEq(shortAuction.bids(address(this)), 100 ether);
}
function testSmallestPossibleBid() public {
fjordPoints.approve(address(auction), 1);
auction.bid(1);
assertEq(auction.bids(address(this)), 1);
}
function testAuctionEndWithZeroBids() public {
// Use the existing `auction` variable
assertEq(auction.totalBids(), 0); // Ensure there are no bids before ending the auction
// Fast forward time to the auction end time
vm.warp(block.timestamp + 2 days);
// End the auction
auction.auctionEnd();
// Check the balance of auction tokens returned to this contract
assertEq(auctionToken.balanceOf(address(this)), 1000 ether);
// Optionally, check for events emitted (if any)
// vm.expectEmit(true, true, true, true);
// emit AuctionEnded(0); // Example: adjust to the actual event signature and parameters
}
function testBidAtExactAuctionEndTime_Success() public {
// Set up a short auction
FjordAuction shortAuction = new FjordAuction(
address(fjordPoints),
address(auctionToken),
1 minutes, // Duration of 1 minute
100 ether // Minimum bid amount
);
// Warp to the exact end time of the auction
vm.warp(block.timestamp + 1 minutes);
// Approve the short auction contract to spend fjordPoints
fjordPoints.approve(address(shortAuction), 100 ether);
// Place a bid exactly at the end time
shortAuction.bid(100 ether);
// Verify that the bid was successfully recorded
assertEq(shortAuction.bids(address(this)), 100 ether);
}
function testBidAtAfterAuctionEndTime_Fail() public {
// Set up a short auction
FjordAuction shortAuction = new FjordAuction(
address(fjordPoints),
address(auctionToken),
1 minutes, // Duration of 1 minute
100 ether // Minimum bid amount
);
// Warp to just after the end time of the auction
vm.warp(block.timestamp + 1 minutes + 1 seconds);
// Approve the short auction contract to spend fjordPoints
fjordPoints.approve(address(shortAuction), 100 ether);
// Expect a revert because the auction has already ended
vm.expectRevert(FjordAuction.AuctionAlreadyEnded.selector);
shortAuction.bid(100 ether);
}
}

When you run this command forge test --match-test testCannotEndAuctionBeforeEndTime

The output will be

Tools Used

Foundry

Recommendations

Ensure that the auctionEnd function properly checks if the current time is beyond the auctionEndTime before proceeding to end the auction.

Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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