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

Auction Tokens stuck in AuctionFactory contract at Zero Bids

Vulnerability Details

The AuctionFactory contract is used for deploying new FjordAuction contracts. When the owner calls the AuctionFactory::createAuction function, a new FjordAuction contract is created with FjordAuction::owner set to the AuctionFactory contract.

/* FjordAuction::constructor function */
constructor(
address _fjordPoints,
address _auctionToken,
uint256 _biddingTime,
uint256 _totalTokens
) {
...
fjordPoints = ERC20Burnable(_fjordPoints);
auctionToken = IERC20(_auctionToken);
=> owner = msg.sender;
auctionEndTime = block.timestamp + _biddingTime;
totalTokens = _totalTokens;
}

So when the FjordAuction::auctionEnd function is called and FjordAuction::totalBids equals 0, all auction tokens will be transferred to the AuctionFactory contract and get stuck there permanently.

/* FjordAuction::auctionEnd function */
function auctionEnd() external {
...
if (totalBids == 0) {
=> auctionToken.transfer(owner, totalTokens);
return;
}
...
}

Impact

All auction tokens would get stuck permanently in the AuctionFactory contract.

Proof Of Concept

Add the following test cases in a new file FjordAuctionFactory.t.sol and run the command forge test --mp test/unit/FjordAuctionFactory.t.sol:

import "forge-std/Test.sol";
import {AuctionFactory} from "src/FjordAuctionFactory.sol";
import {FjordAuction} from "src/FjordAuction.sol";
import {ERC20BurnableMock} from "../mocks/ERC20BurnableMock.sol";
contract AuctionFactoryTest is Test {
address public factoryOwner = address(this);
AuctionFactory public auctionFactory;
ERC20BurnableMock public fjordPoints;
ERC20BurnableMock public auctionToken;
uint256 public constant BIDDING_TIME = 1 weeks;
uint256 public constant TOTAL_TOKENS = 1000 ether;
function setUp() public {
fjordPoints = new ERC20BurnableMock("FjordPoints", "FJB");
auctionToken = new ERC20BurnableMock("AuctionToken", "AUC");
auctionFactory = new AuctionFactory(address(fjordPoints));
deal(address(auctionToken), factoryOwner, TOTAL_TOKENS);
auctionToken.approve(address(auctionFactory), TOTAL_TOKENS);
}
function testInitialize() public {
assertEq(auctionToken.balanceOf(factoryOwner), TOTAL_TOKENS);
assertEq(auctionFactory.owner(), factoryOwner);
assertEq(auctionFactory.fjordPoints(), address(fjordPoints));
}
function testEndAuctionWithZeroBids() public {
// Deploy new auction contract
bytes32 salt = keccak256(abi.encode("Random string"));
vm.recordLogs();
auctionFactory.createAuction(
address(auctionToken),
BIDDING_TIME,
TOTAL_TOKENS,
salt
);
Vm.Log[] memory emmittedEvents = vm.getRecordedLogs();
FjordAuction auctionContract;
for (uint256 i = 0; i < emmittedEvents.length; i++) {
Vm.Log memory emmittedEvent = emmittedEvents[i];
if (emmittedEvent.topics[0] == keccak256("AuctionCreated(address)")) {
auctionContract = FjordAuction(address(uint160(uint256(emmittedEvent.topics[1]))));
break;
}
}
// Check auction contract
assertEq(auctionContract.owner(), address(auctionFactory));
assertEq(auctionContract.totalBids(), 0);
assertEq(auctionToken.balanceOf(factoryOwner), 0);
assertEq(auctionToken.balanceOf(address(auctionContract)), TOTAL_TOKENS);
// End the auction when the total bids equal 0
vm.warp(block.timestamp + BIDDING_TIME + 1 days);
auctionContract.auctionEnd();
// All auction tokens were stuck in the factory contract
assertEq(auctionToken.balanceOf(factoryOwner), 0);
assertEq(auctionToken.balanceOf(address(auctionContract)), 0);
assertEq(auctionToken.balanceOf(address(auctionFactory)), TOTAL_TOKENS);
}
}

Recommendations

In the FjordAuction::constructor function, pass an additional parameter, _owner, to set FjordAuction::owner equal to _owner:

/* FjordAuction::constructor function */
constructor(
+ address _owner,
address _fjordPoints,
address _auctionToken,
uint256 _biddingTime,
uint256 _totalTokens
) {
...
fjordPoints = ERC20Burnable(_fjordPoints);
auctionToken = IERC20(_auctionToken);
- owner = msg.sender;
+ owner = _owner;
auctionEndTime = block.timestamp + _biddingTime;
totalTokens = _totalTokens;
}

And then update the AuctionFactory::createAuction function:

/* AuctionFactory::createAuction function */
function createAuction(
address auctionToken,
uint256 biddingTime,
uint256 totalTokens,
bytes32 salt
) external onlyOwner {
...
address auctionAddress = address(
new FjordAuction{salt: salt}(
+ msg.sender,
fjordPoints,
auctionToken,
biddingTime,
totalTokens
)
);
...
}
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.