Summary
When the totalBids
of a FjordAuction
are zero, and the auctionEnd
function is called, then the totalTokens
amount from the FjordAuction
contract are getting locked into the AuctionFactory
contract. This is because the AuctionFactory
is the owner of the FjordAuction
.
Vulnerability Details
When a FjordAuction
contract is deployed from the AuctionFactory
contract using the createAuction
function, the AuctionFactory
is the msg.sender
of 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 AuctionFactory
contract 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 auctionToken
to the owner
of the FjordAuction
, but the owner
is AuctionFactory
and 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 createAuction
function to deploy a new FjordAuction
with a specific totalTokens
amount.
auctionEndTime
has passed, there are no bids (totalBids == 0
) and someone calls auctionEnd
An amount of totalTokens
auction tokens are sent to the AuctionFactory
The AuctionFactory
does 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 FjordAuction
to 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 AuctionFactory
callable only by the owner of the factory so the tokens can be transferred and are not getting locked.