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

Funds Locked in `AuctionFactory` Contract When Auctions End Without Bids

Summary

When an auction ends without any bids, all ERC20 auction tokens are returned to the owner. However, since an auction
is created through the AuctionFactory contract, it will be the owner. Consequently, all tokens are transferred to the AuctionFactory, which lacks a withdrawal mechanism. This results in the tokens becoming permanently locked within the AuctionFactory contract.

Vulnerability Details

FjordAuction::auctionEnd can be called once an auction ends. In the case no bids were placed, all auction tokens are transferred to the owner (FjordAuction line 192-195):

if (totalBids == 0) {
auctionToken.transfer(owner, totalTokens);
return;
}

In the constructor of the FjordAuction contract the owner is set to the msg.sender (line 134). The issue here is that the auction is created through the AuctionFactory contract (AuctionFactory line 52-66):

function createAuction(
address auctionToken,
uint256 biddingTime,
uint256 totalTokens,
bytes32 salt
) external onlyOwner {
address auctionAddress = address(
@> new FjordAuction{ salt: salt }(fjordPoints, auctionToken, biddingTime, totalTokens)
);
IERC20(auctionToken).transferFrom(msg.sender, auctionAddress, totalTokens);
emit AuctionCreated(auctionAddress);
}

As a result, the AuctionFactory contract becomes the owner of each FjordAuction contract, not the caller of AuctionFactory::createAuction. Consequently, all auction tokens from unsuccessful auctions are sent to the AuctionFactory contract, where they become stuck due to the lack of a withdrawal mechanism.

Proof of Concept

  1. Run the test with -vvvv

  2. Retreive the auctionAddress from this log: emit AuctionCreated(auctionAddress: FjordAuction: [AUCTION_ADDRESS])

  3. Insert the auctionAddress in [INSERT_AUCTION_ADDRESS]

A forge test demonstrating this vulnerability has been provided. The test creates an auction, allows it to end without bids, and verifies that the tokens are indeed transferred to the AuctionFactory contract. Copy the code below into a solidity file in the test directory, follow the steps above and run the test.

Actors:

  • Deployer: Deployer of the FjordAuctionFactory contract who should receive the auction tokens.

  • User: The user who ends the auction without any bids.

Working Test Case:

// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity =0.8.21;
import {Test} from "forge-std/Test.sol";
import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {FjordAuction} from "../src/FjordAuction.sol";
import {AuctionFactory} from "../src/FjordAuctionFactory.sol";
import {FjordPoints} from "../src/FjordPoints.sol";
contract AuctionERC20 is ERC20 {
constructor() ERC20("Auction Token", "AT") {
_mint(msg.sender, 1_000_000e18);
}
}
contract AuditTest is Test {
FjordAuction public fjordAuction;
AuctionFactory public auctionFactory;
FjordPoints public fjordPoints;
AuctionERC20 public auctionToken;
FjordAuction public auction;
address deployer = makeAddr("deployer");
address user = makeAddr("user");
function setUp() public {
vm.startPrank(deployer);
fjordPoints = new FjordPoints();
auctionFactory = new AuctionFactory(address(fjordPoints));
auctionToken = new AuctionERC20();
auctionToken.approve(address(auctionFactory), 100_000 * 10 ** 18);
// Create an auction with 100_000 tokens of the auctionToken
auctionFactory.createAuction(
address(auctionToken),
block.timestamp + 100,
100_000e18,
bytes32(0)
);
auction = FjordAuction([INSERT_AUCTION_ADDRESS]);
vm.stopPrank();
}
function test_auction_end_without_bids_will_lock_funds() public {
vm.startPrank(user);
// Move time past the auction end
vm.warp(block.timestamp + 101);
auction.auctionEnd();
vm.stopPrank();
// Funds will be stuck in the auction factory which doesn't have a way to withdraw them
assertEq(auctionToken.balanceOf(address(auctionFactory)), 100_000e18);
// The auction owner is the auction factory
assertEq(auction.owner(), address(auctionFactory));
}
}

Impact

  • All auction tokens without any bids will be stuck in the AuctionFactory contract.

  • Impact: High

  • Likelihood: Medium (depends on the number of auctions without bids)

-> Severity: High

Tools Used

  • Manual code review

  • Forge unit test

Recommendations

There are two options to fix this issue:

  1. Implement a withdrawal mechanism in the AuctionFactory contract to allow the owner to withdraw the auction tokens.

  2. Change the owner of the FjordAuction contract to the same owner of the AuctionFactory.

Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

If no bids are placed during the auction, the `auctionToken` will be permanently locked within the `AuctionFactory`

An auction with 0 bids will get the `totalTokens` stuck inside the contract. Impact: High - Tokens are forever lost Likelihood - Low - Super small chances of happening, but not impossible

Support

FAQs

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