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

Auction Tokens Returned to FjordAuctionFactory Will Be Lost

Summary

The FjordAuction contract incorrectly assigns ownership to the FjordAuctionFactory during deployment. This design flaw results in auction tokens being sent to the factory contract in the event of no bids, making the tokens irrecoverable. The issue arises because the FjordAuctionFactory is not equipped to handle or recover these tokens, leading to potential loss of funds.

Vulnerability Details

The root cause of the vulnerability is that ownership of the FjordAuction contract is assigned to the FjordAuctionFactory contract instead of an external account (EOA).

  • Found in src/FjordAuctionFactory.sol at Line 59

    @>: FjordAuction is deployed by FjordAuctionFactory, which makes msg.sender FjordAuctionFactory's address in FjordAuction's constructor

    52: function createAuction(
    ...
    58: address auctionAddress = address(
    59:@> new FjordAuction{ salt: salt }(fjordPoints, auctionToken, biddingTime, totalTokens)
    60: );
    ...
    66: }
  • Found in src/FjordAuction.sol at Line 134

    @>: msg.sender is FjordAuctionFactory address

    120: constructor(
    ...
    133: auctionToken = IERC20(_auctionToken);
    134:@> owner = msg.sender;
    135: auctionEndTime = block.timestamp.add(_biddingTime);
    ...
    137: }

This setup leads to a situation where, in case of no bids, auction tokens are sent to the factory contract.

  • Found in src/FjordAuction.sol at Line 193

    @>: owner is FjordAuctionFactory which is not equipped to handle auctionToken. In case of no bids, the returned totalTokens amount of auctionToken will be stuck inside the FjordAuctionFactory

    181: function auctionEnd() external {
    ...
    192: if (totalBids == 0) {
    193:@> auctionToken.transfer(owner, totalTokens);
    194: return;
    ...
    202: }

Impact

This vulnerability is critical because it results in the permanent loss of auction tokens when there are no bids. Since the FjordAuctionFactory is a contract and not an EOA, it cannot recover the tokens sent to it, leading to irreversible loss of funds.

POC

// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity =0.8.21;
import "forge-std/Test.sol";
import "src/FjordAuction.sol";
import "src/FjordAuctionFactory.sol";
import { ERC20BurnableMock } from "../mocks/ERC20BurnableMock.sol";
import { SafeMath } from "lib/openzeppelin-contracts/contracts/utils/math/SafeMath.sol";
contract TestAuctionPOC is Test {
using SafeMath for uint256;
event AuctionCreated(address indexed auctionAddress);
AuctionFactory public auctionFactory;
FjordAuction public auction;
ERC20BurnableMock public fjordPoints;
ERC20BurnableMock public auctionToken;
uint256 public biddingTime = 1 weeks;
uint256 public totalTokens = 1000 ether;
function setUp() public {
fjordPoints = new ERC20BurnableMock("FjordPoints", "fjoPTS");
auctionToken = new ERC20BurnableMock("AuctionToken", "AUCT");
auctionFactory =
new AuctionFactory(address(fjordPoints));
deal(address(auctionToken), address(this), totalTokens);
auctionToken.approve(address(auctionFactory), type(uint256).max);
}
function testAuctionEndWithNoBids() public {
bytes32 salt = keccak256(abi.encodePacked(block.timestamp, msg.sender));
vm.recordLogs();
auctionFactory.createAuction(address(auctionToken), biddingTime, totalTokens, salt);
Vm.Log[] memory entries = vm.getRecordedLogs();
assertEq(entries.length, 2); // 0: Transfer, 1: AuctionCreated
// extract auction address from AuctionCreated event
assertEq(entries[1].topics.length, 2);
assertEq(entries[1].topics[0], keccak256("AuctionCreated(address)"));
auction = FjordAuction(address(uint160(uint256(entries[1].topics[1]))));
skip(biddingTime);
// confirm auction's owner is actually auctionFactory
assertEq(auction.owner(), address(auctionFactory));
// confirm auctionToken returned to auctionFactory
uint256 balBefore = auctionToken.balanceOf(address(auctionFactory));
auction.auctionEnd();
uint256 balAfter = auctionToken.balanceOf(address(auctionFactory));
assertEq(auction.ended(), true);
// following assert confirms `totalTokens` of auctionToken stuck in auctionFactory
assertEq(balAfter - balBefore, totalTokens);
}
}

Run: forge test -vvv --mc TestAuctionPOC --mt testAuctionEndWithNoBids

A PASS result confirms the auctionToken will be stuck inside auctionFactory permanently.

Tools Used

Foundry test

Recommendations

Forward the actual FjordAuctionFactory owner's address, which should be an EOA (externally owned account) to the FjordAuction contract. This ensures that the correct owner can recover the tokens if the auction receives no bids.

Updates

Lead Judging Commences

inallhonesty Lead Judge 10 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.