Skip to content

Latest commit

 

History

History
147 lines (116 loc) · 4.8 KB

2024-08-fjord.md

File metadata and controls

147 lines (116 loc) · 4.8 KB

Fjord Token Staking

Contest Summary

Code under review: 2024-08-fjord (662 nSLOC)

Contest Page: fjord-contest

Placement: #99

Permanent loss of tokens if FjordAuction.sol:totalBids is 0 in Auction

The official submission on Codehawks' website

Summary

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.

Vulnerability Details

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

Impact: High

Likelihood: Medium

The tokens became stuck, making them unusable by anyone.

Proof-of-concept

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

Slight change for PoC

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

Recommendations

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