Lister is overpaying during the cancel of his listing on Listings::cancelListings().
Summary
Lister unfairly double pays the tax used while he is cancelling his listing through Listings::cancelListings().
Root Cause
When a user is creating his listing through Listings::createListings(), he is paying upfront the tax that is expected to be used for the whole duration of the listing. However, if he decides to cancel the listing after some time calling Listings::cancelListings(), he will find himself paying again the portion of the tax that has been used until this point. Let's see the Listings::cancelListings() :
function cancelListings(address _collection, uint[] memory _tokenIds, bool _payTaxWithEscrow) public lockerNotPaused {
uint fees;
uint refund;
for (uint i; i < _tokenIds.length; ++i) {
uint _tokenId = _tokenIds[i];
// Read the listing in a single read
Listing memory listing = _listings[_collection][_tokenId];
// Ensure the caller is the owner of the listing
if (listing.owner != msg.sender) revert CallerIsNotOwner(listing.owner);
// We cannot allow a dutch listing to be cancelled. This will also check that a liquid listing has not
// expired, as it will instantly change to a dutch listing type.
Enums.ListingType listingType = getListingType(listing);
if (listingType != Enums.ListingType.LIQUID) revert CannotCancelListingType();
// Find the amount of prepaid tax from current timestamp to prepaid timestamp
// and refund unused gas to the user.
(uint _fees, uint _refund) = _resolveListingTax(listing, _collection, false);
emit ListingFeeCaptured(_collection, _tokenId, _fees);
fees += _fees;
refund += _refund;
// Delete the listing objects
delete _listings[_collection][_tokenId];
// Transfer the listing ERC721 back to the user
locker.withdrawToken(_collection, _tokenId, msg.sender);
}
// cache
ICollectionToken collectionToken = locker.collectionToken(_collection);
// Burn the ERC20 token that would have been given to the user when it was initially created
@> uint requiredAmount = ((1 ether * _tokenIds.length) * 10 ** collectionToken.denomination()) - refund;
@> payTaxWithEscrow(address(collectionToken), requiredAmount, _payTaxWithEscrow);
collectionToken.burn(requiredAmount + refund);
// ...
}
As we can see, user is expected to return back the whole 1e18 - refund. It helps to remember that when he created the listing he "took" 1e18 - TAX where, now, TAX = refund + fees. So the user is expected to give back to the protocol 1e18 - refund while he got 1e18 - refund - fees. The difference of what he got at the start and what he is expected to return now :
So, now the user has to get out of his pocket and pay again for the fees while, technically, he has paid for them in the start by not ever taking them.
Furthermore, in this way, as we can see from this line, the protocol burns the whole 1e18 without considering the tax that got actually used and shouldn't be burned as it will be deposited to the UniswapV4Implementation :
collectionToken.burn(requiredAmount + refund);
Internal pre-conditions
User creates a listing from Listings::createListings().
External pre-conditions
User wants to cancel his listing by Listings:cancelListings().
Attack Path
User creates a listing from Listings::createListings() and takes back as collectionTokens -> 1e18 - prepaidTax .
Some time passes by.
User wants to cancel the listing by calling Listings::cancelListings() and he has to give back 1e18 - unusedTax. This mean that he has to give also the usedTax amount.
Impact
The impact of this serious vulnerability is that the user is forced to double pay the tax that has been used for the duration that his listing was up. He, firstly, paid for it by not taking it and now, when he cancels the listing, he has to pay it again out of his own pocket. This results to unfair loss of funds for whoever tries to cancel his listing.
PoC
No PoC needed.
Mitigation
To mitigate this vulnerability successfully, consider not requiring user to return the fee variable as well :
function cancelListings(address _collection, uint[] memory _tokenIds, bool _payTaxWithEscrow) public lockerNotPaused {
uint fees;
uint refund;
for (uint i; i < _tokenIds.length; ++i) {
// ...
}
// cache
ICollectionToken collectionToken = locker.collectionToken(_collection);
// Burn the ERC20 token that would have been given to the user when it was initially created
- uint requiredAmount = ((1 ether * _tokenIds.length) * 10 ** collectionToken.denomination()) - refund;
+ uint requiredAmount = ((1 ether * _tokenIds.length) * 10 ** collectionToken.denomination()) - refund - fees;
payTaxWithEscrow(address(collectionToken), requiredAmount, _payTaxWithEscrow);
collectionToken.burn(requiredAmount + refund);
// ...
}
zarkk01
High
Lister is overpaying during the cancel of his listing on
Listings::cancelListings()
.Summary
Lister unfairly double pays the
tax used
while he is cancelling hislisting
throughListings::cancelListings()
.Root Cause
When a user is creating his
listing
throughListings::createListings()
, he is paying upfront the tax that is expected to be used for the whole duration of thelisting
. However, if he decides to cancel the listing after some time callingListings::cancelListings()
, he will find himself paying again the portion of the tax that has been used until this point. Let's see theListings::cancelListings()
:Link to code
As we can see, user is expected to return back the whole
1e18 - refund
. It helps to remember that when he created the listing he "took"1e18 - TAX
where, now,TAX = refund + fees
. So the user is expected to give back to the protocol1e18 - refund
while he got1e18 - refund - fees
. The difference of what he got at the start and what he is expected to return now :So, now the user has to get out of his pocket and pay again for the
fees
while, technically, he has paid for them in the start by not ever taking them.Furthermore, in this way, as we can see from this line, the protocol burns the whole
1e18
without considering thetax
that got actually used and shouldn't be burned as it will be deposited to theUniswapV4Implementation
:Internal pre-conditions
listing
fromListings::createListings()
.External pre-conditions
listing
byListings:cancelListings()
.Attack Path
listing
fromListings::createListings()
and takes back ascollectionTokens
->1e18 - prepaidTax
.listing
by callingListings::cancelListings()
and he has to give back1e18 - unusedTax
. This mean that he has to give also theusedTax
amount.Impact
The impact of this serious vulnerability is that the user is forced to double pay the tax that has been used for the duration that his
listing
was up. He, firstly, paid for it by not taking it and now, when he cancels thelisting
, he has to pay it again out of his own pocket. This results to unfair loss of funds for whoever tries to cancel hislisting
.PoC
No PoC needed.
Mitigation
To mitigate this vulnerability successfully, consider not requiring user to return the
fee
variable as well :