Summary
When the totalBids of a FjordAuction are zero, and the auctionEnd function is called, then the totalTokens amount from the FjordAuctioncontract are getting locked into the AuctionFactory contract. This is because the AuctionFactoryis the owner of the FjordAuction.
Vulnerability Details
When a FjordAuctioncontract is deployed from the AuctionFactory contract using the createAuction function, the AuctionFactoryis the msg.senderof the FjordAuction contract's constructor:
contract AuctionFactory {
...
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);
}
}
contract FjordAuction {
...
constructor(
address _fjordPoints,
address _auctionToken,
uint256 _biddingTime,
uint256 _totalTokens
) {
...
owner = msg.sender;
...
}
...
}
contract FjordAuction {
...
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);
uint256 pointsToBurn = fjordPoints.balanceOf(address(this));
fjordPoints.burn(pointsToBurn);
}
...
}
As a result the AuctionFactorycontract is the owner of any FjordAuction created from createAuction function.
When the totalBids are 0 and the function auctionEnd is called, it is sending a totalTokens amount of auctionTokento the ownerof the FjordAuction, but the owneris AuctionFactoryand it is not deisgned to handle any tokens in its possesion. As a result the tokens are locked.
Proof of concept:
The owner of the AuctionFactory is calling createAuctionfunction to deploy a new FjordAuctionwith a specific totalTokensamount.
auctionEndTimehas passed, there are no bids (totalBids == 0) and someone calls auctionEnd
An amount of totalTokensauction tokens are sent to the AuctionFactory
The AuctionFactorydoes not handle any tokens and they remain locked in the contract.
Proof of code:
Add the following test to test/unit/auction.t.sol
function testAuctionEndWhenNoBidders() public {
address alice = makeAddr("alice");
deal(address(auctionToken), alice, totalTokens);
console.log("Alice balance: ", auctionToken.balanceOf(alice));
vm.startPrank(alice);
AuctionFactory auctionFactory = new AuctionFactory(address(fjordPoints));
auctionToken.approve(address(auctionFactory), totalTokens);
bytes32 salt = keccak256(abi.encodePacked(block.timestamp, msg.sender));
vm.recordLogs();
auctionFactory.createAuction(address(auctionToken), biddingTime, totalTokens, salt);
Vm.Log[] memory logs = vm.getRecordedLogs();
address auctionAddress;
for (uint256 i = 0; i < logs.length; i++) {
if (logs[i].topics[0] == keccak256("AuctionCreated(address)")) {
auctionAddress = address(uint160(uint256(logs[i].topics[1])));
break;
}
}
console.log("Auction Address:", auctionAddress);
FjordAuction fjordAuction = FjordAuction(auctionAddress);
console.log("---------BEFORE auctionEnd()---------");
console.log("fjordAuction's auctionToken balance: ", auctionToken.balanceOf(address(fjordAuction)));
console.log("alice's auctionToken balance: ", auctionToken.balanceOf(alice));
console.log("auctionFactory's auctionToken balance: ", auctionToken.balanceOf(address(auctionFactory)));
console.log("---------BEFORE auctionEnd()---------");
skip(biddingTime);
fjordAuction.auctionEnd();
console.log("----------AFTER auctionEnd()---------");
console.log("fjordAuction's auctionToken balance: ", auctionToken.balanceOf(address(fjordAuction)));
console.log("alice's auctionToken balance: ", auctionToken.balanceOf(alice));
console.log("auctionFactory's auctionToken balance: ", auctionToken.balanceOf(address(auctionFactory)));
console.log("----------AFTER auctionEnd()---------");
vm.stopPrank();
}
Impact
The tokens are getting locked into the AuctionFactory contract.
Tools Used
Manual review & foundry
Recommendations
Either add the possibility the owner of the FjordAuctionto be passed to the constructor so it can be passed the same as the owner of the AuctionFactory(or any other address who know what to do with the tokens) OR add a withdrawTokens function to the AuctionFactorycallable only by the owner of the factory so the tokens can be transferred and are not getting locked.