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