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

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.

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.

// 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)
);
// 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 _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:

// SPDX-License-Identifier: AGPL-3.0-only
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 {
// deploy auctionToken
auctionToken = new MockERC20("ATOKEN","ATOKEN",18);
vm.startPrank(owner);
// deploy fjordPoints
fjordPoints = new FjordPoints();
// deploy AuctionFactory
auctionFactory = new AuctionFactory(address(fjordPoints));
vm.stopPrank();
// check owner
assertEq(owner,auctionFactory.owner());
assertEq(owner,fjordPoints.owner());
// Mint auctionToken for 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());
// We can clearly see that the owner of `auctionAddress` is `auctionFactory`
console.log("auctionFactory address:",address(auctionFactory));
// So if no user bids for the auction, the `auctionToken` in `auctionAddress` will be returned to `auctionFactory`
// The balance before the cache call
uint256 auctionFactory_auctionTokenBalance_before_auctionEnd = auctionToken.balanceOf(address(auctionFactory));
vm.warp(11);
// calls auctionEnd()
FjordAuction(auctionAddress).auctionEnd();
// The balance after the cache call
uint256 auctionFactory_auctionTokenBalance_after_auctionEnd = auctionToken.balanceOf(address(auctionFactory));
// Calculate the amount of `auctionToken` received by the `auctionFactory` contract
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);
}
// [PASS] test_AuctionFactory_createAuction() (gas: 835538)
// Logs:
// auctionAddress: 0x0750eCB7Cd628CBa26465eE2d81bfC784352bc37
// owner: 0x7c8999dC9a822c1f0Df42023113EDB4FDd543266
// The owner of auctionFactory: 0x7c8999dC9a822c1f0Df42023113EDB4FDd543266
// The owner of auctionAddress: 0xCeF98e10D1e80378A9A74Ce074132B66CDD5e88d
// auctionFactory address: 0xCeF98e10D1e80378A9A74Ce074132B66CDD5e88d
// The amount of auctionToken received by the auctionFactory contract: 10000000000000000000
// helper
function owner_createAuction() public returns (address auctionAddress) {
vm.startPrank(owner);
auctionToken.approve(address(auctionFactory),10e18);
// Capture log
vm.recordLogs();
auctionFactory.createAuction(address(auctionToken),10,10e18,""); // owner transfer 10e18 to auctionAddress
vm.stopPrank();
// Get the recorded log
Vm.Log[] memory logs = vm.getRecordedLogs();
// Traverse the log to find the AuctionCreated event
for (uint256 i = 0; i < logs.length; i++) {
// Signature hash of the event: keccak256("AuctionCreated(address)")
if (logs[i].topics[0] == keccak256("AuctionCreated(address)")) {
// Get the event parameter auctionAddress, which is usually logs[i].topics[1] or logs[i].data[0]
auctionAddress = address(uint160(uint256(logs[i].topics[1]))); // Event parameter address
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;
}
Updates

Lead Judging Commences

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