User can avoid paying interest rate on their protected Listing by adjusting the position
Summary
Lack of interest charge in ProtectedListings.adjustPosition() allows a user to avoid paying interest rate. The user can reduce their tokenTaken value to smallest amount possible, then when he unlocks the protected listing, the interest rate charged would be calculated based on the reduced amount.
Root Cause
in ProtectedListings.adjustPosition() the _amount provided by the user can be directly deducted from the tokenTaken. As the tokenTaken is reduced when user wants to unlock their protected listing, he will call ProtectedListings.unlockProtectedListing(). Then the interest rate shall be calculated only based on the remaining tokenTaken. What the user repaid using adjustPosition() is on zero interest.
A user needs to have a created protected listing which is healthy (i.e. the protected listing did not yet run out of collateral)
External pre-conditions
No external pre-conditions are required.
Attack Path
User creates a protected listing with certain amounts of tokenTaken
After some time the user instead of calling ProtectedListings.unlockProtectedListing() is calling ProtectedListings.adjustPosition() and repays the debt up to maximum amount possible.
user calls ProtectedListings.unlockProtectedListing() and pays the interest on the minimum amount of debt that is left to unlock the listing.
Impact
There is no impact to the fee loss of the protocol as in ProtectedListings the fees paid by the user are burned. however by allowing users to create protected listings without sufficient fees, the protocol tokenomics might be negatively affected as it could lead to over use of the protected listings and increase the ERC20 supply.
PoC
PoC test:
function test_MyAbuseAdjustPosition() public {
address payable _owner1 = payable(address(0x01));
// Deploy our platform contracts
_deployPlatform();
// Define our `_poolKey` by creating a collection. This uses `erc721b`, as `erc721a`
// is explicitly created in a number of tests.
locker.createCollection(address(erc721a), 'Test Collection', 'TEST', 0);
// Initialise our collection
_initializeCollection(erc721a, SQRT_PRICE_1_2);
// Mint our token to the _owner and approve the {Listings} contract to use it
erc721a.mint(_owner1, TOKEN_ID);
deal(address(locker.collectionToken(address(erc721a))), _owner1, 2 ether);
uint balanceBefore = locker.collectionToken(address(erc721a)).balanceOf(_owner1);
// Create our listing for owner 1
vm.startPrank(_owner1);
erc721a.approve(address(protectedListings), TOKEN_ID);
IProtectedListings.ProtectedListing memory listing = IProtectedListings.ProtectedListing({
owner: _owner1,
tokenTaken: 0.4 ether,
checkpoint: 0
});
// Create our listing
_createProtectedListing({
_listing: IProtectedListings.CreateListing({
collection: address(erc721a),
tokenIds: _tokenIdToArray(TOKEN_ID),
listing: listing
})
});
vm.stopPrank();
// Warp forward half the time, so that we can test the required amount to be repaid in addition
vm.warp(block.timestamp + (VALID_LIQUID_DURATION / 2));
// unlock the listing:
vm.startPrank(_owner1);
locker.collectionToken(address(erc721a)).approve(address(protectedListings), 1 ether);
//protectedListings.adjustPosition(address(erc721a), TOKEN_ID, -(0.4 ether - 1));
protectedListings.unlockProtectedListing(address(erc721a), TOKEN_ID, true);
uint balanceAfter = locker.collectionToken(address(erc721a)).balanceOf(_owner1);
console.log("Paid: %d", balanceBefore - balanceAfter);
}
Run the PoC code as it is and you should get the total cost of the protected listing interest:
jecikpo
Medium
User can avoid paying interest rate on their protected Listing by adjusting the position
Summary
Lack of interest charge in
ProtectedListings.adjustPosition()
allows a user to avoid paying interest rate. The user can reduce theirtokenTaken
value to smallest amount possible, then when he unlocks the protected listing, the interest rate charged would be calculated based on the reduced amount.Root Cause
in
ProtectedListings.adjustPosition()
the_amount
provided by the user can be directly deducted from thetokenTaken
. As thetokenTaken
is reduced when user wants to unlock their protected listing, he will callProtectedListings.unlockProtectedListing()
. Then the interest rate shall be calculated only based on the remainingtokenTaken
. What the user repaid usingadjustPosition()
is on zero interest.The problematic function: https://github.com/sherlock-audit/2024-08-flayer/blob/main/flayer/src/contracts/ProtectedListings.sol#L366
Internal pre-conditions
External pre-conditions
No external pre-conditions are required.
Attack Path
tokenTaken
ProtectedListings.unlockProtectedListing()
is callingProtectedListings.adjustPosition()
and repays the debt up to maximum amount possible.ProtectedListings.unlockProtectedListing()
and pays the interest on the minimum amount of debt that is left to unlock the listing.Impact
There is no impact to the fee loss of the protocol as in
ProtectedListings
the fees paid by the user are burned. however by allowing users to create protected listings without sufficient fees, the protocol tokenomics might be negatively affected as it could lead to over use of the protected listings and increase the ERC20 supply.PoC
PoC test:
Run the PoC code as it is and you should get the total cost of the protected listing interest:
next uncomment the following line which repays the entire debt minus
1
:After running again, we see that the cost to the user was zero:
Mitigation
Implement interest rate charge at the
adjustPosition()
position function when users decreases their debt.