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

Wrong `FjordAuction` owner will result in loss of all the tokens in case of 0 bids

Summary

The FjordAuction.sol contract initializes its owner address during construction. This owner address is used to send the totalTokens back at the end of the auction in the case that there are no bids. However, when creating a new FjordAuction through the FjordAuctionFactory contract, the FjordAuction's owner will be set to be the FjordAuctionFactory address. Sending any tokens to that address will lock them forever, since there is no withdrawal mechanism implemented.

Vulnerability Details

FjordAuction.sol
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;
}

We see that the owner of the FjordAuction contract is set to the address that deploys the contract. Creating a new auction through the FjordAuctionFactory contract, makes FjordAuctionFactory's address the deployer of the contract and the owner of the FjordAuctions it deploys.

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

If there are 0 bids in the auction, when it ends it sends all the tokens to the owner. Any tokens sent to the FjordAuctionFactory will be lost, because there is no way to withdraw/recover any tokens from this contract.

Impact

In case there are 0 bids in an auction, all the tokens of the auction will be sent to FjordAuctionFactory's address and will be lost forever.

Proof Of Concept

Create a new file test/unit/auctionFactory.t.sol and paste the following code:

// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity =0.8.21;
import "forge-std/Test.sol";
import "src/FjordAuction.sol";
import "src/FjordAuctionFactory.sol";
import { ERC20BurnableMock } from "../mocks/ERC20BurnableMock.sol";
import { SafeMath } from "lib/openzeppelin-contracts/contracts/utils/math/SafeMath.sol";
contract TestAuctionFactory is Test {
using SafeMath for uint256;
AuctionFactory public auctionFactory;
ERC20BurnableMock public fjordPoints;
ERC20BurnableMock public auctionToken;
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");
vm.startPrank(owner);
auctionFactory = new AuctionFactory(address(fjordPoints));
vm.stopPrank();
deal(address(auctionToken), owner, totalTokens);
}
function testAuctionOwner() public {
bytes32 salt = "1";
vm.startPrank(owner);
auctionToken.approve(address(auctionFactory), totalTokens);
address auction =
auctionFactory.createAuction(address(auctionToken), biddingTime, totalTokens, salt);
vm.stopPrank();
vm.assertEq(FjordAuction(auction).owner(), address(auctionFactory));
assert(FjordAuction(auction).owner() != owner);
}
}

Also, the FjordAuctionFactory.sol file was modified for this test so I could recover the auctionAddress after creating the new FjordAuction contract:

FjordAuctionFactory.sol
function createAuction(
address auctionToken,
uint256 biddingTime,
uint256 totalTokens,
bytes32 salt
- ) external onlyOwner {
+ ) external onlyOwner returns (address) {
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);
+ return auctionAddress;
}

Tools Used

Manual Review

Recommendations

During the FjordAuction construction, add another address for the correct owner:

FjordAuction.sol
constructor(
+ address _owner,
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;
+ owner = _owner;
auctionEndTime = block.timestamp.add(_biddingTime);
totalTokens = _totalTokens;
}
FjordAuctionFactory.sol
function createAuction(
address auctionToken,
uint256 biddingTime,
uint256 totalTokens,
bytes32 salt
) external onlyOwner {
address auctionAddress = address(
- new FjordAuction{ salt: salt }(fjordPoints, auctionToken, biddingTime, totalTokens)
+ new FjordAuction{ salt: salt }(owner, 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);
}
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.