Platform: Code4rena
Start Date: 08/01/2024
Pot Size: $83,600 USDC
Total HM: 23
Participants: 116
Period: 10 days
Judge: 0xean
Total Solo HM: 1
Id: 317
League: ETH
Rank: 93/116
Findings: 2
Award: $10.69
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: JCN
Also found by: 0xDING99YA, 0xpiken, ABAIKUNANBAEV, AkshaySrivastav, Audinarey, Aymen0909, DanielArmstrong, J4X, Krace, KupiaSec, Qkite, Ward, evmboi32, fnanni, hash, juancito, kaden, krikolkk, ravikiranweb3, rbserver, rvierdiiev, serial-coder, trachev, zach, zzzitron
7.9739 USDC - $7.97
https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/libraries/RentalUtils.sol#L38-L44 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/modules/Storage.sol#L118-L128 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/policies/Guard.sol#L126-L136
The rentalId
is derived from the combination of recipient
, token
, and identifier
. While this approach is suitable for ERC721, where each token has a unique ID, it poses challenges for ERC1155. In ERC1155, tokens may share the same ID, leading to potential issues in the contract when different users create orders with identical rentalId
if there are same recipient
.
The rentalId
is computed within the getItemPointer
function using recipient
, token
, and identifier
. In the context of ERC1155, where tokens can have shared IDs, situations may arise where different users create orders with the same ERC1155 and the same ID. This scenario results in identical rentalId
values when the recipient
is also the same.
function getItemPointer( address recipient, address token, uint256 identifier ) internal pure returns (RentalId id) { id = RentalId.wrap(keccak256(abi.encodePacked(recipient, token, identifier))); }
The rentalId
serves as a means to track the quantity of lent tokens in the safe. When initiating a rental, the corresponding amount is added to the rentedAssets
, and when concluding a rental, the amount is deducted from the rentedAssets
.
function addRentals( bytes32 orderHash, RentalAssetUpdate[] memory rentalAssetUpdates ) external onlyByProxy permissioned { // Add the order to storage. orders[orderHash] = true; // Add the rented items to storage. for (uint256 i = 0; i < rentalAssetUpdates.length; ++i) { RentalAssetUpdate memory asset = rentalAssetUpdates[i]; // Update the order hash for that item. rentedAssets[asset.rentalId] += asset.amount; } }
function removeRentals( bytes32 orderHash, RentalAssetUpdate[] calldata rentalAssetUpdates ) external onlyByProxy permissioned { // The order must exist to be deleted. if (!orders[orderHash]) { revert Errors.StorageModule_OrderDoesNotExist(orderHash); } else { // Delete the order from storage. delete orders[orderHash]; } // Process each rental asset. for (uint256 i = 0; i < rentalAssetUpdates.length; ++i) { RentalAssetUpdate memory asset = rentalAssetUpdates[i]; // Reduce the amount of tokens for the particular rental ID. rentedAssets[asset.rentalId] -= asset.amount; } }
If the amount in the safe is not zero, attempting to transfer these tokens will result in failure. Consequently, if there are multiple orders with the same rentalId
, none of the orders can be terminated until all of them have expired, so we can remove them all via removeRentalsBatch
.
function isRentedOut( address recipient, address token, uint256 identifier ) external view returns (bool) { // calculate the rental ID RentalId rentalId = RentalUtils.getItemPointer(recipient, token, identifier); // Determine if there is a positive amount return rentedAssets[rentalId] != 0; }
function _revertSelectorOnActiveRental( bytes4 selector, address safe, address token, uint256 tokenId ) private view { // Check if the selector is allowed. if (STORE.isRentedOut(safe, token, tokenId)) { revert Errors.GuardPolicy_UnauthorizedSelector(selector); } }
Example:
removeRentalsBatch
. If Carol only removes Alice's order, there will still be tokens in rentedAssets[rentalId]
, leading to a failed transfer.Manual Review
It's recommended to use more info to generate the rentalId
, like the lender's address.
Context
#0 - c4-pre-sort
2024-01-21T17:55:34Z
141345 marked the issue as duplicate of #239
#1 - c4-judge
2024-01-28T20:50:03Z
0xean changed the severity to 2 (Med Risk)
#2 - c4-judge
2024-01-28T21:05:05Z
0xean marked the issue as satisfactory
#3 - c4-pre-sort
2024-02-02T09:28:39Z
141345 marked the issue as not a duplicate
#4 - c4-pre-sort
2024-02-02T09:29:21Z
141345 marked the issue as duplicate of #418
#5 - c4-judge
2024-02-02T11:28:57Z
0xean marked the issue as partial-50
#6 - c4-judge
2024-02-02T11:41:30Z
0xean changed the severity to 3 (High Risk)
🌟 Selected for report: stackachu
Also found by: 0xHelium, 0xabhay, 0xc695, 0xpiken, DeFiHackLabs, EV_om, HSP, J4X, Krace, KupiaSec, Qkite, ZanyBonzy, albertwh1te, cccz, evmboi32, hals, hash, holydevoti0n, krikolkk, ladboy233, lanrebayode77, marqymarq10, oakcobalt, peanuts, peter, rbserver, said, serial-coder, sin1st3r__
2.7205 USDC - $2.72
https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/modules/PaymentEscrow.sol#L215-L283 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/modules/PaymentEscrow.sol#L100-L118
In cases where a rental order involves ERC20 tokens with a blacklist or hook, such as USDT or ERC777, the lender faces the challenge of being unable to reclaim their NFTs. This is due to the renter's ability to consistently revert the _safeTransfer
during payment settlement.
When a lender wants to stop the order, he needs to invoke stopRent
and settle the payment in function _settlePayment
.
function _settlePayment( Item[] calldata items, OrderType orderType, address lender, address renter, uint256 start, uint256 end ) internal { // Calculate the time values. uint256 elapsedTime = block.timestamp - start; uint256 totalTime = end - start; // Determine whether the rental order has ended. bool isRentalOver = elapsedTime >= totalTime; // Loop through each item in the order. for (uint256 i = 0; i < items.length; ++i) { // Get the item. Item memory item = items[i]; // Check that the item is a payment. if (item.isERC20()) { // Set a placeholder payment amount which can be reduced in the // presence of a fee. uint256 paymentAmount = item.amount; // Take a fee on the payment amount if the fee is on. if (fee != 0) { // Calculate the new fee. uint256 paymentFee = _calculateFee(paymentAmount); // Adjust the payment amount by the fee. paymentAmount -= paymentFee; } // Effect: Decrease the token balance. Use the payment amount pre-fee // so that fees can be taken. _decreaseDeposit(item.token, item.amount); // If its a PAY order but the rental hasn't ended yet. if (orderType.isPayOrder() && !isRentalOver) { // Interaction: a PAY order which hasnt ended yet. Payout is pro-rata. _settlePaymentProRata( item.token, paymentAmount, lender, renter, elapsedTime, totalTime ); } // If its a PAY order and the rental is over, or, if its a BASE order. else if ( (orderType.isPayOrder() && isRentalOver) || orderType.isBaseOrder() ) { // Interaction: a pay order or base order which has ended. Payout is in full. _settlePaymentInFull( item.token, paymentAmount, item.settleTo, lender, renter ); } else { revert Errors.Shared_OrderTypeNotSupported(uint8(orderType)); } } } }
The _settlePayment
function facilitates the transfer of ERC20 tokens to both the lender and renter through either _settlePaymentProRata
or _settlePaymentInFull
. Both of these functions employ the _safeTransfer
function for token transfer.
However, with certain types of ERC20, the recipient has the ability to consistently revert the transfer, resulting in a false return. Examples include USDT with its blacklist or ERC777 with its hook. In any such scenario where the renter fails the transfer, the lender remains unable to reclaim their NFT, allowing the renter to persist in using the NFT.
function _safeTransfer(address token, address to, uint256 value) internal { // Call transfer() on the token. (bool success, bytes memory data) = token.call( abi.encodeWithSelector(IERC20.transfer.selector, to, value) ); // Because both reverting and returning false are allowed by the ERC20 standard // to indicate a failed transfer, we must handle both cases. // // If success is false, the ERC20 contract reverted. // // If success is true, we must check if return data was provided. If no return // data is provided, then no revert occurred. But, if return data is provided, // then it must be decoded into a bool which will indicate the success of the // transfer. if (!success || (data.length != 0 && !abi.decode(data, (bool)))) { revert Errors.PaymentEscrowModule_PaymentTransferFailed(token, to, value); } }
Manual Review
It's recommended to lock the ERC20 in ESCRW
and let the renter or lender claim ERC20 for themselves.
DoS
#0 - c4-pre-sort
2024-01-21T17:35:50Z
141345 marked the issue as duplicate of #64
#1 - c4-judge
2024-01-28T20:49:24Z
0xean changed the severity to 2 (Med Risk)
#2 - c4-judge
2024-01-28T21:01:13Z
0xean marked the issue as satisfactory