Summary
The AuctionFactory::createAuction() function is used to create the FjordAuction contract. If no bids are placed, the auctionToken will remain permanently locked within the AuctionFactory contract.
Vulnerability Details
The AuctionFactory::createAuction() function is responsible for creating the FjordAuction contract. The ownership of the newly created FjordAuction contract is assigned to the AuctionFactory contract itself. If no bids are placed during the auction, the funds are returned to the AuctionFactory contract when AuctionFactory::auctionEnd() is called. However, since the AuctionFactory contract lacks an implementation for transferring ERC20 tokens, the auctionToken becomes permanently locked within the AuctionFactory contract.
The following code snippet illustrates the issue. When the owner calls AuctionFactory::createAuction() to deploy the FjordAuction contract, the msg.sender in the FjordAuction::constructor() is the AuctionFactory contract, resulting in owner == 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);
}
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;
}
Since the AuctionFactory contract lacks an implementation for transferring ERC20 tokens, the auctionToken is permanently locked within the AuctionFactory contract.
2024-08-fjord$ forge inspect AuctionFactory methodIdentifiers
{
"createAuction(address,uint256,uint256,bytes32)": "56c0703a",
"fjordPoints()": "eb702257",
"owner()": "8da5cb5b",
"setOwner(address)": "13af4035"
}
Poc
Please place the PoC code into the /test directory and execute it:
pragma solidity =0.8.21;
import "../src/FjordStaking.sol";
import { FjordPoints } from "../src/FjordPoints.sol";
import { Test,Vm,console } from "forge-std/Test.sol";
import { MockERC20 } from "solmate/test/utils/mocks/MockERC20.sol";
import { FjordPointsMock } from "./mocks/FjordPointsMock.sol";
import { ISablierV2LockupLinear } from "lib/v2-core/src/interfaces/ISablierV2LockupLinear.sol";
import { ISablierV2Lockup } from "lib/v2-core/src/interfaces/ISablierV2Lockup.sol";
import { Broker, LockupLinear } from "lib/v2-core/src/types/DataTypes.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ud60x18 } from "@prb/math/src/UD60x18.sol";
import "lib/v2-core/src/libraries/Errors.sol";
import {AuctionFactory} from "../src/FjordAuctionFactory.sol";
import {FjordAuction} from "../src/FjordAuction.sol";
contract Poc is Test {
AuctionFactory public auctionFactory;
FjordPoints public fjordPoints;
MockERC20 public auctionToken;
address public owner = makeAddr("owner");
function setUp() public {
auctionToken = new MockERC20("ATOKEN","ATOKEN",18);
vm.startPrank(owner);
fjordPoints = new FjordPoints();
auctionFactory = new AuctionFactory(address(fjordPoints));
vm.stopPrank();
assertEq(owner,auctionFactory.owner());
assertEq(owner,fjordPoints.owner());
auctionToken.mint(owner,100e18);
assertEq(auctionToken.balanceOf(owner),100e18);
}
function test_AuctionFactory_createAuction() public {
address auctionAddress = owner_createAuction();
console.log("auctionAddress:",auctionAddress);
console.log("owner:",owner);
console.log("The owner of auctionFactory:",auctionFactory.owner());
console.log("The owner of auctionAddress:",FjordAuction(auctionAddress).owner());
console.log("auctionFactory address:",address(auctionFactory));
uint256 auctionFactory_auctionTokenBalance_before_auctionEnd = auctionToken.balanceOf(address(auctionFactory));
vm.warp(11);
FjordAuction(auctionAddress).auctionEnd();
uint256 auctionFactory_auctionTokenBalance_after_auctionEnd = auctionToken.balanceOf(address(auctionFactory));
uint amount = auctionFactory_auctionTokenBalance_after_auctionEnd - auctionFactory_auctionTokenBalance_before_auctionEnd;
console.log("The amount of auctionToken received by the auctionFactory contract:",amount);
assertEq(amount,10e18);
}
function owner_createAuction() public returns (address auctionAddress) {
vm.startPrank(owner);
auctionToken.approve(address(auctionFactory),10e18);
vm.recordLogs();
auctionFactory.createAuction(address(auctionToken),10,10e18,"");
vm.stopPrank();
Vm.Log[] memory logs = vm.getRecordedLogs();
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;
}
}
}
}
Code Snippet
https://github.com/Cyfrin/2024-08-fjord/blob/3a78c456b39c799f64c9c2510992584ada3516a0/src/FjordAuctionFactory.sol#L52-L66
https://github.com/Cyfrin/2024-08-fjord/blob/3a78c456b39c799f64c9c2510992584ada3516a0/src/FjordAuction.sol#L120-L137
Impact
If no bids are placed during the auction, the auctionToken will be permanently locked within the AuctionFactory contract due to the lack of an ERC20 transfer implementation.
Tools Used
Manual Review
Recommendations
Consider adding an _owner parameter in the FjordAuction::constructor() to allow the user to initialize the owner. Additionally, modify AuctionFactory::createAuction() to pass msg.sender as a parameter to ensure that the owner of the auction contract is the caller of the FjordAuction::constructor() function.
// AuctionFactory::createAuction()
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 }(msg.sender, 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);
}
// FjordAuction::constructor()
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;
}