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

Possibility of Auction Tokens getting stuck in the AuctionFactory when there are no bids on the auction

Summary

The AuctionFactory is designed to create FjordAuction contracts in a deterministic manner using the CREATE2 opcode, which allows the address of each FjordAuction to be known beforehand. Upon creation, auction tokens are sent directly to the FjordAuction contract. The key issue identified in this report arises when the AuctionFactory becomes the owner of the FjordAuction contract, creating a significant risk of token loss if the auction concludes without any bids. Specifically, if the auctionEnd function is called under these circumstances, all auction tokens are transferred to the AuctionFactory, which lacks functionality for recovering or managing these tokens, causing them to become permanently locked.

Vulnerability Details

Auction Tokens and Ownership:

  • Upon creation, the FjordAuction contract is owned by the AuctionFactory, and auction tokens are sent to the FjordAuction for bidding.

contract AuctionFactory {
......
function createAuction(
address auctionToken,
uint256 biddingTime,
uint256 totalTokens,
bytes32 salt
) external onlyOwner {
// create inside the factory contract makes factory the owner
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);
}
.......
}
contract FjordAuction {
.....
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;
}
......
}

Auction Ending Mechanism:

  • If an auction ends without receiving any bids, the auctionEnd function can be called, which transfers all remaining tokens to the contract owner—AuctionFactory.

Lack of Token Management in AuctionFactory:

  • The AuctionFactory does not implement any functionality to withdraw or transfer tokens it holds. This means that once tokens are transferred to the AuctionFactory, they cannot be retrieved, leading to a permanent loss.

Proof of Concept

  1. Admin approves 1000 ether to AuctionFactory

  2. Admin calls createAuction, which creates a new FjordAuction and sends 1000 ether of auction tokens to the newly created FjordAuction

  3. AuctionFactory becomes the owner of the FjordAuction

  4. Auction starts

  5. Auction ends with no bids

  6. Admin or anyone else calls auctionEnd

  7. All auction tokens are sent to the owner of the FjordAuction which is the AuctionFactory

  8. AuctionFactory does not have any function to withdraw or transfer auction tokens

  9. Auction tokens are stuck in AuctionFactory

Proof of Code

FjordAuction public auction;
ERC20BurnableMock public fjordPoints;
ERC20BurnableMock public auctionToken;
AuctionFactory public auctionFactory;
address public owner = address(0x1);
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));
auction =
new FjordAuction(address(fjordPoints), address(auctionToken), biddingTime, totalTokens);
deal(address(auctionToken), address(auction), totalTokens);
}
function testFE2E() public {
deal(address(auctionToken), address(this), 1000 ether);
auctionToken.approve(address(auctionFactory), 1000 ether);
// address = 0x76207838144EDc5F8625575563B9fed42D32e79c
auctionFactory.createAuction(
address(auctionToken), biddingTime, 1000 ether, bytes32("salt")
);
address auctionAddress = 0x76207838144EDc5F8625575563B9fed42D32e79c;
skip(biddingTime);
// funds should be sent to owner -
FjordAuction(auctionAddress).auctionEnd();
// All funds have been transfered to the Auction Factory
assertEq(auctionToken.balanceOf(address(auctionFactory)), 1000 ether);
}

Impact

If the auction ends without any bids, the resulting transfer of tokens to the AuctionFactory could result in the permanent loss of those tokens, as the factory contract does not support any mechanism to withdraw or manage these tokens.

Tools Used

Manual Review

Recommendations

Implement a Token Recovery Mechanism

The AuctionFactory should be equipped with a function that allows the recovery of tokens. This function should be restricted to authorized entities to prevent misuse.

function recoverTokens(address token, uint256 amount, address recipient) external onlyOwner {
IERC20(token).transfer(recipient, amount);
}

Adjust the Ownership Structure

Consider revising the ownership model so that the auction tokens are not automatically transferred to the AuctionFactory. The FjordAuction contract could include a feature to return tokens to the original owner if no bids are placed.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year 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.