In the unlockProtectedListing() function, the interest that was supposed to be distributed to LP holders was instead burned.
Summary
In the unlockProtectedListing() function, the interest that was supposed to be distributed to LP holders was instead burned.
Vulnerability Detail
function unlockProtectedListing(address _collection, uint _tokenId, bool _withdraw) public lockerNotPaused {
// Ensure this is a protected listing
ProtectedListing memory listing = _protectedListings[_collection][_tokenId];
// Ensure the caller owns the listing
if (listing.owner != msg.sender) revert CallerIsNotOwner(listing.owner);
// Ensure that the protected listing has run out of collateral
int collateral = getProtectedListingHealth(_collection, _tokenId);
if (collateral < 0) revert InsufficientCollateral();
// cache
ICollectionToken collectionToken = locker.collectionToken(_collection);
uint denomination = collectionToken.denomination();
uint96 tokenTaken = _protectedListings[_collection][_tokenId].tokenTaken;
// Repay the loaned amount, plus a fee from lock duration
@>> uint fee = unlockPrice(_collection, _tokenId) * 10 ** denomination;
@>> collectionToken.burnFrom(msg.sender, fee);
// We need to burn the amount that was paid into the Listings contract
@>> collectionToken.burn((1 ether - tokenTaken) * 10 ** denomination);
// Remove our listing type
unchecked { --listingCount[_collection]; }
// Delete the listing objects
delete _protectedListings[_collection][_tokenId];
// Transfer the listing ERC721 back to the user
if (_withdraw) {
locker.withdrawToken(_collection, _tokenId, msg.sender);
emit ListingAssetWithdraw(_collection, _tokenId);
} else {
canWithdrawAsset[_collection][_tokenId] = msg.sender;
}
// Update our checkpoint to reflect that listings have been removed
_createCheckpoint(_collection);
// Emit an event
emit ListingUnlocked(_collection, _tokenId, fee);
}从unlockProtectedListing()调用的unlockPrice()可以知道,feeb包含了本金和利息。
function unlockPrice(address _collection, uint _tokenId) public view returns (uint unlockPrice_) {
// Get the information relating to the protected listing
ProtectedListing memory listing = _protectedListings[_collection][_tokenId];
// Calculate the final amount using the compounded factors and principle amount
@>> unlockPrice_ = locker.taxCalculator().compound({
_principle: listing.tokenTaken,
_initialCheckpoint: collectionCheckpoints[_collection][listing.checkpoint],
_currentCheckpoint: _currentCheckpoint(_collection)
});
}Therefore, after burning the fee, the interest paid by the user was also burned. This portion of the interest should have been distributed to the LP holders. Evidence for this can be found in the liquidateProtectedListing() function, where the interest generated by the ProtectedListing NFT is distributed to the LP holders.
function liquidateProtectedListing(address _collection, uint _tokenId) public lockerNotPaused listingExists(_collection, _tokenId) {
//-------skip-----------
// Send the remaining tokens to {Locker} implementation as fees
uint remainingCollateral = (1 ether - listing.tokenTaken - KEEPER_REWARD) * 10 ** denomination;
if (remainingCollateral > 0) {
IBaseImplementation implementation = locker.implementation();
collectionToken.approve(address(implementation), remainingCollateral);
@>> implementation.depositFees(_collection, 0, remainingCollateral);
}
//-------skip-----------
}After the interest is burned, it causes deflation in the total amount of collectionToken, which leads to serious problems:
- The total number of collectionTokens no longer matches the number of NFTs (it becomes less than the number of NFTs in the Locker contract), making it impossible to redeem some NFTs.
- The utilizationRate() calculation results in a utilization rate greater than 100%, leading to an excessively high interestRate_, which in turn makes it impossible for users to create listings on ProtectedListings.
/**
* Determines the usage rate of a listing type.
*
* @param _collection The collection to calculate the utilization rate of
*
* @return listingsOfType_ The number of listings that match the type passed
* @return utilizationRate_ The utilization rate percentage of the listing type (80% = 0.8 ether)
*/
function utilizationRate(address _collection) public view virtual returns (uint listingsOfType_, uint utilizationRate_) {
// Get the count of active listings of the specified listing type
listingsOfType_ = listingCount[_collection];
// If we have listings of this type then we need to calculate the percentage, otherwise
// we will just return a zero percent value.
if (listingsOfType_ != 0) {
ICollectionToken collectionToken = locker.collectionToken(_collection);
// If we have no totalSupply, then we have a zero percent utilization
uint totalSupply = collectionToken.totalSupply();
if (totalSupply != 0) {
utilizationRate_ = (listingsOfType_ * 1e36 * 10 ** collectionToken.denomination()) / totalSupply;
}
}
}Impact
LP holders suffer losses, and users may be unable to use ProtectedListings normally.
Code Snippet
Tool used
Manual Review
Recommendation
Distribute the interest to the LP holders.