sherlock-audit / 2024-08-flayer-judging

2 stars 0 forks source link

0xBhumii - Frontrunning Vulnerability in Token `Reservation` Process #739

Open sherlock-admin4 opened 1 month ago

sherlock-admin4 commented 1 month ago

0xBhumii

Medium

Frontrunning Vulnerability in Token Reservation Process

Summary

The reserve function in the Listings contract is susceptible to frontrunning attacks, where an attacker can observe and preemptively execute transactions to reserve tokens before the original user. This vulnerability can lead to unauthorized reservations and disrupt the intended reservation process.

Vulnerability Detail

Frontrunning occurs when an attacker monitors pending transactions in the blockchain's public mempool and submits their own transaction with a higher gas fee to be processed first. In the context of the reserve function, this allows an attacker to become the reserver of a token by front-running legitimate users' transactions.

Impact

Unauthorized Reservations: Attackers can reserve tokens intended for other users, potentially blocking legitimate access. User Frustration: Legitimate users may experience failed transactions and increased costs as they attempt to reserve tokens. Market Manipulation: Frontrunners can exploit the reservation process for personal gain, disrupting market dynamics.

Code Snippet

function reserve(address _collection, uint _tokenId, uint _collateral) public nonReentrant lockerNotPaused {
        // Read the existing listing in a single read
        Listing memory oldListing = _listings[_collection][_tokenId];

        // Ensure the caller is not the owner of the listing
        if (oldListing.owner == msg.sender) revert CallerIsAlreadyOwner();

        // Ensure that the existing listing is available
        (bool isAvailable, uint listingPrice) = getListingPrice(_collection, _tokenId);
        if (!isAvailable) revert ListingNotAvailable();

        // Find the underlying {CollectionToken} attached to our collection
        ICollectionToken collectionToken = locker.collectionToken(_collection);

        // Check if the listing is a floor item and process additional logic if there
        // was an owner (meaning it was not floor, so liquid or dutch).
        if (oldListing.owner != address(0)) {
            // We can process a tax refund for the existing listing if it isn't a liquidation
            if (!_isLiquidation[_collection][_tokenId]) {
                (uint _fees,) = _resolveListingTax(oldListing, _collection, true);
                if (_fees != 0) {
                    emit ListingFeeCaptured(_collection, _tokenId, _fees);
                }
            }

            // If the floor multiple of the original listings is different, then this needs
            // to be paid to the original owner of the listing.
            uint listingFloorPrice = 1 ether * 10 ** collectionToken.denomination();
            if (listingPrice > listingFloorPrice) {
                unchecked {
                    collectionToken.transferFrom(msg.sender, oldListing.owner, listingPrice - listingFloorPrice);
                }
            }

            // Reduce the amount of listings
            unchecked { listingCount[_collection] -= 1; }
        }

        // Burn the tokens that the user provided as collateral, as we will have it minted
        // from {ProtectedListings}.
        collectionToken.burnFrom(msg.sender, _collateral * 10 ** collectionToken.denomination());

        // We can now pull in the tokens from the Locker
        locker.withdrawToken(_collection, _tokenId, address(this));
        IERC721(_collection).approve(address(protectedListings), _tokenId);

        // Create a protected listing, taking only the tokens
        uint[] memory tokenIds = new uint[](1);
        tokenIds[0] = _tokenId;
        IProtectedListings.CreateListing[] memory createProtectedListing = new IProtectedListings.CreateListing[](1);
        createProtectedListing[0] = IProtectedListings.CreateListing({
            collection: _collection,
            tokenIds: tokenIds,
            listing: IProtectedListings.ProtectedListing({
                owner: payable(address(this)),
                tokenTaken: uint96(1 ether - _collateral),
                checkpoint: 0 // Set in the `createListings` call
            })
        });

        // Create our listing, receiving the ERC20 into this contract
        protectedListings.createListings(createProtectedListing);

        // We should now have received the non-collateral assets, which we will burn in
        // addition to the amount that the user sent us.
        collectionToken.burn((1 ether - _collateral) * 10 ** collectionToken.denomination());

        // We can now transfer ownership of the listing to the user reserving it
        protectedListings.transferOwnership(_collection, _tokenId, payable(msg.sender));
    }

https://github.com/sherlock-audit/2024-08-flayer/blob/main/flayer/src/contracts/Listings.sol#L690

Tool used

Manual Review

Recommendation

Commit-Reveal Scheme: Implement a two-step process where users first commit to reserving a token and later reveal their commitment, reducing the predictability of transactions.

Randomized Delays: Introduce a randomized delay or time window for reservations to make it harder for attackers to predict transaction timing.

Gas Price Management: Encourage users to set competitive gas prices and monitor network conditions to reduce the likelihood of being frontrun.