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

Due to the incorrect owner assets may get locked

Summary

In the FjordAuction constructor, contract owner is incorrectly set to the msg.sender which is the instance of the FjordAuctionFactory. When auction ends without any bids assets are returned to the owner. However, FjordAuctionFactory does not have capability to manage these assets and they will remain locked.

Vulnerability Details

FjordAuctionFactory#L59

FjordAuction.sol#L134

In the FjordAuctionFactory::createAuction function, the constructor from the FjordAuction contract is called:

function createAuction(
address auctionToken,
uint256 biddingTime,
uint256 totalTokens,
bytes32 salt
) external onlyOwner {
@> 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);
}

The mentioned constructor looks like this:

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

As we can see, the owner of the auction is set to msg.sender. In the case where the FjordAuctionFactory contract calls the constructor of the FjordAuction, msg.sender will be the FjordAuctionFactory contract, not the caller of the FjordAuctionFactory::createAuction function.

PoC

Make a new test file auctionFactory.t.sol in test/unit/:

// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity =0.8.21;
import "forge-std/Test.sol";
import "src/FjordAuctionFactory.sol";
import { ERC20BurnableMock } from "../mocks/ERC20BurnableMock.sol";
contract TestAuction is Test {
using SafeMath for uint256;
AuctionFactory public factory;
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.prank(owner);
factory= new AuctionFactory(address(fjordPoints));
deal(address(auctionToken), owner, totalTokens);
}
function testAuctionOwner() public {
bytes32 salt=0x7465737400000000000000000000000000000000000000000000000000000000;//corresponding to the ASCII string "test" :)
vm.startPrank(owner);
auctionToken.approve(address(factory), totalTokens); //We need to allow the factory to use transferFrom with our address
factory.createAuction(
address(auctionToken),
biddingTime,
totalTokens,
salt
);
vm.stopPrank();
//auction address from emmited event is ->auctionAddress: FjordAuction: [0xD04e7b550AbafB44d41b4B36db060f03aB3D4766]
FjordAuction auction = FjordAuction(0xD04e7b550AbafB44d41b4B36db060f03aB3D4766);
address auctionOwner = auction.owner();
assert(auctionOwner != owner);
assert(auctionOwner == address(factory));
console.log("Auction owner:", auctionOwner);
console.log("It should be: ", owner);
console.log("But it is: ", address(factory));
vm.warp(vm.getBlockTimestamp() + biddingTime + 1);
auction.auctionEnd();
assert(0 == auctionToken.balanceOf(owner));
assert(totalTokens == auctionToken.balanceOf(address(factory)));
}
}

Impact

The owner of the auction will not be the one who called the FjordAuctionFactory::createAuction function and sent the tokens for the auction, but rather the FjordAuctionFactory contract itself. When creating new auction, creator sends the tokens to the created contract:

function createAuction(address auctionToken, uint256 biddingTime, uint256 totalTokens,bytes32 salt
) external onlyOwner {
(...)
// Transfer the auction tokens from the msg.sender to the new auction contract
@> IERC20(auctionToken).transferFrom(msg.sender, auctionAddress, totalTokens);
(...)
}

In the case when the auction ends without any bids, the tokens will be sent to the the auction owner i.e. FjordAuctionFactory address, not to the token owner, and they will remain locked in the mentioned contract:

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

Tools Used

Manual code review / Foundry tests

Recommendations

Modify the constructor in the FjordAuction contract by adding an additional parameter that will represent the address of the auction owner:

constructor(
address _fjordPoints,
address _auctionToken,
uint256 _biddingTime,
uint256 _totalTokens,
+ address auctionOwner
) {
if (_fjordPoints == address(0)) {
revert InvalidFjordPointsAddress();
}
if (_auctionToken == address(0)) {
revert InvalidAuctionTokenAddress();
}
fjordPoints = ERC20Burnable(_fjordPoints);
auctionToken = IERC20(_auctionToken);
- owner = msg.sender;
+ owner = auctionOwner;
auctionEndTime = block.timestamp.add(_biddingTime);
totalTokens = _totalTokens;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months 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.