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

If no fjord points are bid in an auction, the `auctionToken` will get stuck in `FjordAuctionFactory`

Summary

The FjordAuctionFactory creates FjordAuction contracts using the CREATE2 opcode. Users can bid FjordPoints in these auctions to receive tokens proportional to their bids. If no bids are placed and the auction ends, the allocated tokens are returned to the owner of the FjordAuction contract. The owner of the FjordAuction is set in its constructor to msg.sender, which makes the FjordAuctionFactory the owner. However, the FjordAuctionFactory lacks the functionality to withdraw these returned tokens, causing them to become permanently stuck in the contract.

Vulnerability Details

The FjordAuctionFactory contract allows its owner to create new FjordAuction contracts using the createAuction function:

function createAuction(
address auctionToken,
uint256 biddingTime,
uint256 totalTokens,
bytes32 salt
) external onlyOwner {
address auctionAddress = address(
new FjordAuction{ salt: salt }(fjordPoints, auctionToken, biddingTime, totalTokens)
);
// Transfer the auction tokens from the msg.sender to the new auction contract
IERC20(auctionToken).transferFrom(msg.sender, auctionAddress, totalTokens);
emit AuctionCreated(auctionAddress);
}

This function deploys a new FjordAuction contract using CREATE2 and transfers totalTokens from msg.sender to the newly created auctionAddress.

The FjordAuction contract includes an auctionEnd function that can be called by anyone to finalize the auction. If no bids have been placed (totalBids == 0), this function transfers all auctionToken tokens back to the owner:

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

Since the owner of the FjordAuction is set to msg.sender in its constructor, the msg.sender in this context will be the FjordAuctionFactory contract:

constructor(
address _fjordPoints,
address _auctionToken,
uint256 _biddingTime,
uint256 _totalTokens
) {
if (_fjordPoints == address(0)) {
revert InvalidFjordPointsAddress();
}
if (_auctionToken == address(0)) {
revert InvalidAuctionTokenAddress();
}
fjordPoints = ERC20Burnable(_fjordPoints);
auctionToken = IERC20(_auctionToken);
@> owner = msg.sender;
auctionEndTime = block.timestamp.add(_biddingTime);
totalTokens = _totalTokens;
}

However, the FjordAuctionFactory does not have a mechanism to withdraw these returned tokens, leaving them permanently locked within the factory contract.

forge inspect src/FjordAuctionFactory.sol:AuctionFactory methodIdentifiers
{
"createAuction(address,uint256,uint256,bytes32)": "56c0703a",
"fjordPoints()": "eb702257",
"owner()": "8da5cb5b",
"setOwner(address)": "13af4035"
}

Impact

The lack of functionality in the FjordAuctionFactory to withdraw ERC20 tokens results in any tokens returned from auctions where no bids are placed becoming permanently inaccessible. This effectively locks up potentially large amounts of arbitrary ERC20 tokens within the factory contract, which cannot be recovered or used again.

Code Snippet

https://github.com/Cyfrin/2024-08-fjord/blob/0312fa9dca29fa7ed9fc432fdcd05545b736575d/src/FjordAuction.sol#L134

https://github.com/Cyfrin/2024-08-fjord/blob/0312fa9dca29fa7ed9fc432fdcd05545b736575d/src/FjordAuction.sol#L192-L195

Tools Used

Manual Review

Recommendations

Add a withdrawReturnedTokens function to enable the ownerof the FjordAuctionFactory contract to recover ERC20 tokens that are returned from auctions with no bids. This function should allow the owner to transfer the tokens from the factory contract to their own address, making it possible to redistribute or use these tokens in future auctions.

function withdrawReturnedTokens(address auctionToken) external onlyOwner
{
uint256 availableBalance = IERC20(auctionToken).balanceOf(address(this));
require(availableBalance > 0, "No tokens to withdraw");
IERC20(auctionToken).transfer(msg.sender, availableBalance);
}
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.