Code under review: 2024-08-fjord (662 nSLOC)
Contest Page: fjord-contest
Placement: #99
The official submission on Codehawks' website
If the total bids in the auction for a token are zero, the tokens are transferred to the AuctionFactory
. However, since the FjordAuctionFactory.sol:AuctionFactory
lacks any methods to transfer those funds, the tokens become permanently locked there.
In FjordAuction.sol:auctionEnd
at L192-L195, if FjordAuction.sol:totalBids
is zero, the tokens are transferred to the owner, which is FjordAuctionFactory.sol:AuctionFactory
. Since the FjordAuctionFactory.sol:AuctionFactory
contract lacks any methods to utilize or transfer those tokens, they become permanently stuck in the contract.
Impact
: High
Likelihood
: Medium
The tokens became stuck, making them unusable by anyone.
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity =0.8.21;
import {Test} from "forge-std/Test.sol";
import {FjordAuction} from "../../src/FjordAuction.sol";
import {FjordPoints} from "../../src/FjordPoints.sol";
import {FjordToken} from "../../src/FjordToken.sol";
import {AuctionFactory} from "../../src/FjordAuctionFactory.sol";
contract AuditTest is Test {
AuctionFactory public factory;
FjordPoints public points;
function setUp() public {
FjordPoints pointsContract = new FjordPoints();
factory = new AuctionFactory(address(pointsContract));
}
function test_tokenStuckInFactory() public {
FjordToken sampleAuctionToken = new FjordToken();
//approving factory contract to use tokens
sampleAuctionToken.approve(address(factory), 1000 ether);
//creating a new auction contract
address auctionAddress = factory.createAuction(
address(sampleAuctionToken),
block.timestamp + 1000,
1000 ether,
bytes32(uint(123))
);
vm.warp(block.timestamp + 1001);
FjordAuction auction = FjordAuction(auctionAddress);
//auction ended
auction.auctionEnd();
// balance of factory contract will be 1000 tokens which got stuck there forever
require(sampleAuctionToken.balanceOf(address(factory)) == 1000 ether);
}
}
AuctionFactory.sol:createAuction
returns the newly created auction contract address
/**
* @notice Creates a new auction contract using create2.
* @param biddingTime The duration of the auction in seconds.
* @param totalTokens The total number of tokens to be auctioned.
* @param salt A unique salt for create2 to generate a deterministic address.
*/
function createAuction(
address auctionToken,
uint256 biddingTime,
uint256 totalTokens,
bytes32 salt
) 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;
}
Pass the owner's address as a parameter to the FjordAuction.sol
constructor instead of using msg.sender
. This new owner should be an EOA (Externally Owned Account).
/**
* @dev Sets the token contract address and auction duration.
* @param _fjordPoints The address of the FjordPoints token contract.
* @param _biddingTime The duration of the auction in seconds.
* @param _totalTokens The total number of tokens to be auctioned.
*/
constructor(
address _fjordPoints,
address _auctionToken,
uint256 _biddingTime,
uint256 _totalTokens,
+ address _owner
) {
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;
}