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: 39/116
Findings: 9
Award: $244.32
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: plasmablocks
Also found by: 0xabhay, BARW, hals, serial-coder
113.6855 USDC - $113.69
hookOnStop
hook acts as a middleware contract that execute arbitrary logic when the rental order is stopped, but if the hook is malfunction and can't be executed (where its failuer is catched in the catch block and the transaction is reverted); this will result in reverting the Stop._removeHooks
function that's called when the rental is stopped, which will result in permanently locking the rented assets in the renter SAFE wallet.
Another issue with this: if a lot of hookOnStop
hooks are assigned with rental order (as the renter can assign as much on-stop hooks as he can ), the Stop.stopRental
transaction might revert due to out-of-gas resulting in locking the rented assets in the renter wallet, and this can be prevented by limiting the number of on-stop hooks that can be executed with each rental order.
Stop._removeHooks function / L209-L212
// Call the hook with data about the rented item. try IHook(target).onStop( rentalWallet, item.token, item.identifier, item.amount, hooks[i].extraData ) {} catch Error(string memory revertReason) { // Revert with reason given. revert Errors.Shared_HookFailString(revertReason); } catch Panic(uint256 errorCode) { // Convert solidity panic code to string. string memory stringErrorCode = LibString.toString(errorCode); // Revert with panic code. revert Errors.Shared_HookFailString( string.concat("Hook reverted: Panic code ", stringErrorCode) ); } catch (bytes memory revertData) { // Fallback to an error that returns the byte data. revert Errors.Shared_HookFailBytes(revertData); }
Manual Review.
Update Create._removeHooks
function to ignore the hookOnStop
if its execution didn't succeed without reverting the transaction.
DoS
#0 - c4-pre-sort
2024-01-21T17:59:08Z
141345 marked the issue as duplicate of #501
#1 - c4-judge
2024-01-28T19:35:58Z
0xean marked the issue as satisfactory
#2 - c4-judge
2024-01-28T20:47:34Z
0xean changed the severity to 2 (Med Risk)
#3 - DevHals
2024-01-30T15:54:52Z
Hi @0xean,
this issue is marked as a duplicate of 501, where this issue mentions the DoS caused by OOG reverts which makes it more of #538 duplicate rather than 501, since issue 501 points to DoS caused from disabling a whitelisted onStop
hook and a missing check on the validity/whitelisting of onStop
hook during order filling.
I kindly ask you to take a second look and re-evaluate this issue,
Thanks!
#4 - DevHals
2024-01-30T19:14:03Z
Just to add that I have reported the DoS of stopping rentals caused by disabling a whitelisted onStop
hook in #346
and DoS of stopping rentals caused by not checking the onStop
hooks while filling a rental order in #345
and both were duplicated with 501,
#5 - c4-judge
2024-01-30T19:24:36Z
0xean marked the issue as not a duplicate
#6 - c4-judge
2024-01-30T19:24:48Z
0xean marked the issue as duplicate of #538
#7 - c4-judge
2024-01-30T19:24:55Z
0xean marked the issue as partial-50
🌟 Selected for report: AkshaySrivastav
Also found by: 0xA5DF, 0xAlix2, 0xDING99YA, 0xdice91, BARW, BI_security, EV_om, J4X, Jorgect, SBSecurity, ZdravkoHr, evmboi32, hals, haxatron, imare, juancito, kaden, marqymarq10, oakcobalt, rbserver, rokinot, rvierdiiev, said, serial-coder, trachev
4.7844 USDC - $4.78
https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/policies/Create.sol#L479-L483 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/policies/Stop.sol#L209-L212
When an order is fulfilled; the openSea invokes Create.validateOrder
to check if the created hash of the rental order parameters match the orderHash stored by the protocol when the order is created along with other processes.
One of the processes includes checking if the order has hooks that must be executed before fulfilling it via Create._addHooks
; where this function checks if the hook assigned to each order item is whitelisted to be executed on start :
if (!STORE.hookOnStart(target)) { revert Errors.Shared_DisabledHook(target); }
So if any of these hooks are not allowed to be executed on-start (or allowd to be executed on-stop); the transaction will revert resulting in not fulfilling the order, so no losses will be incurred.
But if all the items hooks are whitlisted to be executed on-start and there's no on-stop hooks; the order will be fulfilled, but this will result in locking the rented asset in the renter SAFE wallet; but how?
Stop._removeHooks
function:if (!STORE.hookOnStop(target)) { revert Errors.Shared_DisabledHook(target); }
hookOnStart
hooks when the order is first fulfilled (as explained above), and this will result in reverting the Stop.stopRent
function resulting in permanently locking the rented items in the renter SAFE wallet.Create._addHooks function / L479-L483
// Check that the hook is reNFT-approved to execute on rental start. if (!STORE.hookOnStart(target)) { revert Errors.Shared_DisabledHook(target); }
Stop._removeHooks function / L209-L212
// Check that the hook is reNFT-approved to execute on rental stop. if (!STORE.hookOnStop(target)) { revert Errors.Shared_DisabledHook(target); }
Manual Review.
Update Create._addHooks
function to accept the whitlisted hookOnStop
, and execute it when the rental is stopped.
Update Create._removeHooks
function to ignore the execution of hookOnStart
if the item is assigned one without reverting the transaction.
DoS
#0 - c4-pre-sort
2024-01-21T17:59:11Z
141345 marked the issue as duplicate of #501
#1 - c4-judge
2024-01-28T19:36:23Z
0xean marked the issue as satisfactory
#2 - c4-judge
2024-01-28T20:47:34Z
0xean changed the severity to 2 (Med Risk)
🌟 Selected for report: AkshaySrivastav
Also found by: 0xA5DF, 0xAlix2, 0xDING99YA, 0xdice91, BARW, BI_security, EV_om, J4X, Jorgect, SBSecurity, ZdravkoHr, evmboi32, hals, haxatron, imare, juancito, kaden, marqymarq10, oakcobalt, rbserver, rokinot, rvierdiiev, said, serial-coder, trachev
4.7844 USDC - $4.78
https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/policies/Stop.sol#L209-L212 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/policies/Guard.sol#L373-L378
The Guard
contract admin can blacklist/whitelist a hook via Guard.updateHookStatus
function; where these hooks act as a middleware contracts that execute arbitrary logic when the rental order is fulfilled and when it's stopped, and the selection of which hooks to execute in each phase is specified by the lender when he creates the rental order.
But there's a serious issue when the hooks are checked:
The hookOnStop
attached to each order item (if any is present) is not checked if whitelisted or not when the order is fulfilled and validated via Create.validateOrder
, so items might have invalid/non-whitelited hooks attached to them.
The Guard
contract admin can change the status of the hookOnStop
from being whitelisted to non-whitelisted at any time.
This will result in reverting the Stop._removeHooks
function that's called when the rental is stopped, which will result in permanently locking the rented assets in the renter SAFE wallet.
Stop._removeHooks function / L209-L212
// Check that the hook is reNFT-approved to execute on rental stop. if (!STORE.hookOnStop(target)) { revert Errors.Shared_DisabledHook(target); }
Guard.updateHookStatus function
function updateHookStatus( address hook, uint8 bitmap ) external onlyRole("GUARD_ADMIN") { STORE.updateHookStatus(hook, bitmap); }
Manual Review.
Update Create._removeHooks
function to ignore the execution of hookOnStop
if the item is assigned one without reverting the transaction.
DoS
#0 - c4-pre-sort
2024-01-21T18:07:57Z
141345 marked the issue as duplicate of #238
#1 - c4-pre-sort
2024-01-28T05:44:28Z
141345 marked the issue as not a duplicate
#2 - c4-pre-sort
2024-01-28T05:45:25Z
141345 marked the issue as duplicate of #501
#3 - c4-judge
2024-01-28T19:36:04Z
0xean marked the issue as satisfactory
#4 - c4-judge
2024-01-28T20:47:36Z
0xean changed the severity to 2 (Med Risk)
🌟 Selected for report: LokiThe5th
Also found by: 0xAlix2, BI_security, Coverage, EV_om, Giorgio, KupiaSec, Qkite, SBSecurity, anshujalan, evmboi32, hals, juancito, krikolkk, oakcobalt, rbserver, rokinot, roleengineer, said, sin1st3r__, trachev, yashar
8.618 USDC - $8.62
When a rental order is fulfilled, the rented assets (ERC721/ERC1155) tokens are transferred to the renter SAFE wallet that's created and whitelisted by the protocol, where it has a Guard
contract set to guard the transactions originated from the rental wallet where it prevents delegatecalls and calls that aim to transfer the assets from the wallet or approve them.
All these checks are done in Guard.checkTransaction
function by checking if the function selector is allowed or not, if allowed then the transaction can be executed, and if not; the transaction will revert.
But it was noticed that any malicious renter can burn the rented asset as there's no limitation on calling the burn function on the rented token, since this function selector isn't standard between token contracts and usually depends on the token contract implementation (as it might be burn(uint256 tokenId)
or burn(address from,uint256 tokenId)
,etc...).
This will result in burning the rented assets from the renter wallet, as there's no check after the transaction execution if the assets still exist in the wallet or not.
Guard._checkTransaction function
function _checkTransaction(address from, address to, bytes memory data) private view { bytes4 selector; // Load in the function selector. assembly { selector := mload(add(data, 0x20)) } if (selector == e721_safe_transfer_from_1_selector) { // Load the token ID from calldata. uint256 tokenId = uint256( _loadValueFromCalldata(data, e721_safe_transfer_from_1_token_id_offset) ); // Check if the selector is allowed. _revertSelectorOnActiveRental(selector, from, to, tokenId); } else if (selector == e721_safe_transfer_from_2_selector) { // Load the token ID from calldata. uint256 tokenId = uint256( _loadValueFromCalldata(data, e721_safe_transfer_from_2_token_id_offset) ); // Check if the selector is allowed. _revertSelectorOnActiveRental(selector, from, to, tokenId); } else if (selector == e721_transfer_from_selector) { // Load the token ID from calldata. uint256 tokenId = uint256( _loadValueFromCalldata(data, e721_transfer_from_token_id_offset) ); // Check if the selector is allowed. _revertSelectorOnActiveRental(selector, from, to, tokenId); } else if (selector == e721_approve_selector) { // Load the token ID from calldata. uint256 tokenId = uint256( _loadValueFromCalldata(data, e721_approve_token_id_offset) ); // Check if the selector is allowed. _revertSelectorOnActiveRental(selector, from, to, tokenId); } else if (selector == e1155_safe_transfer_from_selector) { // Load the token ID from calldata. uint256 tokenId = uint256( _loadValueFromCalldata(data, e1155_safe_transfer_from_token_id_offset) ); // Check if the selector is allowed. _revertSelectorOnActiveRental(selector, from, to, tokenId); } else if (selector == gnosis_safe_enable_module_selector) { // Load the extension address from calldata. address extension = address( uint160( uint256( _loadValueFromCalldata(data, gnosis_safe_enable_module_offset) ) ) ); // Check if the extension is whitelisted. _revertNonWhitelistedExtension(extension); } else if (selector == gnosis_safe_disable_module_selector) { // Load the extension address from calldata. address extension = address( uint160( uint256( _loadValueFromCalldata(data, gnosis_safe_disable_module_offset) ) ) ); // Check if the extension is whitelisted. _revertNonWhitelistedExtension(extension); } else { // Revert if the `setApprovalForAll` selector is specified. This selector is // shared between ERC721 and ERC1155 tokens. if (selector == shared_set_approval_for_all_selector) { revert Errors.GuardPolicy_UnauthorizedSelector( shared_set_approval_for_all_selector ); } // Revert if the `safeBatchTransferFrom` selector is specified. There's no // cheap way to check if individual items in the batch are rented out. // Each token ID would require a call to the storage contract to check // its rental status. if (selector == e1155_safe_batch_transfer_from_selector) { revert Errors.GuardPolicy_UnauthorizedSelector( e1155_safe_batch_transfer_from_selector ); } // Revert if the `setGuard` selector is specified. if (selector == gnosis_safe_set_guard_selector) { revert Errors.GuardPolicy_UnauthorizedSelector( gnosis_safe_set_guard_selector ); } } }
Manual Review.
Override checkAfterExecution
function in the SAFE wallet contract to check if the assets are still owned after a transaction is execution.
Context
#0 - c4-pre-sort
2024-01-21T17:39:20Z
141345 marked the issue as duplicate of #323
#1 - c4-judge
2024-01-28T20:06:26Z
0xean marked the issue as satisfactory
#2 - c4-judge
2024-01-28T20:48:45Z
0xean changed the severity to 2 (Med Risk)
🌟 Selected for report: BI_security
Also found by: 0xPsuedoPandit, 0xpiken, ABAIKUNANBAEV, Beepidibop, CipherSleuths, EV_om, Giorgio, Hajime, J4X, KingNFT, KupiaSec, NentoR, SBSecurity, SpicyMeatball, Tendency, Ward, ZdravkoHr, boringslav, deepplus, hals, hash, haxatron, jasonxiale, juancito, pkqs90, plasmablocks, ravikiranweb3, rokinot, rvierdiiev, trachev, zaevlad, zzebra83
1.8029 USDC - $1.80
https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Signer.sol#L383-L386 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Signer.sol#L405-L406 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Signer.sol#L218-L239
Signer._deriveOrderMetadataHash
function is used to derive the order metadata hash, where the derived value is going to be used in:
Deriving the rentalPayloadHash:
function _deriveRentPayloadHash( RentPayload memory payload ) internal view returns (bytes32) { // Derive and return the rent payload hash as specified by EIP-712. return keccak256( abi.encode( _RENT_PAYLOAD_TYPEHASH, _deriveOrderFulfillmentHash(payload.fulfillment), _deriveOrderMetadataHash(payload.metadata), payload.expiration, payload.intendedFulfiller ) ); }
Validating the order metadata by comparing the derived order metadata hash with the zoneHash
of the order metadata that was passed in when the Seaport order was signed:
// Check that the zone hash is equal to the derived hash of the metadata. if (_deriveOrderMetadataHash(metadata) != zoneHash) { revert Errors.CreatePolicy_InvalidOrderMetadataHash(); }
The _ORDER_METADATA_TYPEHASH
immutable variable is assigned when the Create
contract is deployed, and it's defined using _deriveRentalTypehashes()
function, as follows:
// Construct the OrderMetadata type string. bytes memory orderMetadataTypeString = abi.encodePacked( "OrderMetadata(uint8 orderType,uint256 rentDuration,Hook[] hooks,bytes emittedExtraData)" ); //some code... // Derive the OrderMetadata type hash using the corresponding type string. orderMetadataTypeHash = keccak256(orderMetadataTypeString);
As can be noticed: to create a hash for the order metadata, the orderType
, rentDuration
, hooks
and emittedExtraData
must be encoded to create it; but it was noticed that the emittedExtraData
and orderType
are not encoded when creating the hash:
keccak256( abi.encode( _ORDER_METADATA_TYPEHASH, metadata.rentDuration, keccak256(abi.encodePacked(hookHashes)) ) );
This hash is supposed to follow EIP712
specification, but as shown above; it violates the specs, which will result in integration issues with SeaPort as the created hash is invalid and doesn't comply with EIP712
.
Signer._deriveRentalTypehashes function/ L383-L386
// Construct the OrderMetadata type string. bytes memory orderMetadataTypeString = abi.encodePacked( "OrderMetadata(uint8 orderType,uint256 rentDuration,Hook[] hooks,bytes emittedExtraData)" );
Signer._deriveRentalTypehashes function/ L405-L406
// Derive the OrderMetadata type hash using the corresponding type string. orderMetadataTypeHash = keccak256(orderMetadataTypeString);
Signer._deriveOrderMetadataHash function
function _deriveOrderMetadataHash( OrderMetadata memory metadata ) internal view returns (bytes32) { // Create array for hooks. bytes32[] memory hookHashes = new bytes32[](metadata.hooks.length); // Iterate over each hook. for (uint256 i = 0; i < metadata.hooks.length; ++i) { // Hash the hook hookHashes[i] = _deriveHookHash(metadata.hooks[i]); } // Derive and return the metadata hash as specified by EIP-712. return keccak256( abi.encode( _ORDER_METADATA_TYPEHASH, metadata.rentDuration, keccak256(abi.encodePacked(hookHashes)) ) ); }
Manual Review.
Update _deriveOrderMetadataHash
to be EIP712
compliant by including metadata.orderType
and metadata.emittedExtraData
in order metadata hash calculation:
function _deriveOrderMetadataHash( OrderMetadata memory metadata ) internal view returns (bytes32) { // Create array for hooks. bytes32[] memory hookHashes = new bytes32[](metadata.hooks.length); // Iterate over each hook. for (uint256 i = 0; i < metadata.hooks.length; ++i) { // Hash the hook hookHashes[i] = _deriveHookHash(metadata.hooks[i]); } // Derive and return the metadata hash as specified by EIP-712. return keccak256( abi.encode( _ORDER_METADATA_TYPEHASH, + metadata.orderType metadata.rentDuration, keccak256(abi.encodePacked(hookHashes)), + metadata.emittedExtraData ) ); }
Context
#0 - c4-pre-sort
2024-01-21T17:50:44Z
141345 marked the issue as duplicate of #239
#1 - c4-judge
2024-01-28T21:05:14Z
0xean marked the issue as satisfactory
🌟 Selected for report: BI_security
Also found by: 0xPsuedoPandit, 0xpiken, ABAIKUNANBAEV, Beepidibop, CipherSleuths, EV_om, Giorgio, Hajime, J4X, KingNFT, KupiaSec, NentoR, SBSecurity, SpicyMeatball, Tendency, Ward, ZdravkoHr, boringslav, deepplus, hals, hash, haxatron, jasonxiale, juancito, pkqs90, plasmablocks, ravikiranweb3, rokinot, rvierdiiev, trachev, zaevlad, zzebra83
1.8029 USDC - $1.80
https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Signer.sol#L378-L400 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Signer.sol#L248-L262
Signer._deriveRentalOrderHash
function is used to derive the hashes of rental orders , where the derived hash is going to be used:
Storage
,The _RENTAL_ORDER_TYPEHASH
immutable variable is assigned when the Create
contract is deployed, and it's defined using _deriveRentalTypehashes()
function, as follows:
// Construct the OrderFulfillment type string. bytes memory orderFulfillmentTypeString = abi.encodePacked( "OrderFulfillment(address recipient)" ); // Construct the OrderMetadata type string. bytes memory orderMetadataTypeString = abi.encodePacked( "OrderMetadata(uint8 orderType,uint256 rentDuration,Hook[] hooks,bytes emittedExtraData)" ); // Construct the RentPayload type string. bytes memory rentPayloadTypeString = abi.encodePacked( "RentPayload(OrderFulfillment fulfillment,OrderMetadata metadata,uint256 expiration,address intendedFulfiller)" ); // Derive RentPayload type hash via combination of relevant type strings. rentPayloadTypeHash = keccak256( abi.encodePacked( rentPayloadTypeString, orderMetadataTypeString, orderFulfillmentTypeString ) );
As can be noticed, to create a hash for a rental order, three structs data must be encoded:
RentPayload(OrderFulfillment fulfillment,OrderMetadata metadata,uint256 expiration,address intendedFulfiller)
OrderMetadata(uint8 orderType,uint256 rentDuration,Hook[] hooks,bytes emittedExtraData)
OrderFulfillment(address recipient)
And _deriveRentPayloadHash
is used to create the hash as follows:
function _deriveRentPayloadHash( RentPayload memory payload ) internal view returns (bytes32) { // Derive and return the rent payload hash as specified by EIP-712. return keccak256( abi.encode( _RENT_PAYLOAD_TYPEHASH, _deriveOrderFulfillmentHash(payload.fulfillment), _deriveOrderMetadataHash(payload.metadata), payload.expiration, payload.intendedFulfiller ) ); }
Where _deriveOrderFulfillmentHash
function returns :
return keccak256( abi.encode(_ORDER_FULFILLMENT_TYPEHASH, fulfillment.recipient) );
and _deriveOrderMetadataHash
function returns the following; (where the encoded data doesn't comply with the OrderMetadata
struct as it's missing encoding the emittedExtraData
and orderType
, this is reported in a separate issue):
```javascript return keccak256( abi.encode( _ORDER_METADATA_TYPEHASH, metadata.rentDuration, keccak256(abi.encodePacked(hookHashes)) ) ); ```
and the payload.expiration
is encoded while it's not supposed to be included.
So as can be noticed: the arrangement of the encoded structs data and the number of the encoded data are wrong and don't comply with the _RENTAL_ORDER_TYPEHASH
definition.
While the created order hash is supposed to follow EIP712
specification, but as shown above; it violates the specs, which might result in hashes collision as the created hash is invalid and doesn't comply with EIP712
.
Signer._deriveRentalTypehashes function/L378-L400
// Construct the OrderFulfillment type string. bytes memory orderFulfillmentTypeString = abi.encodePacked( "OrderFulfillment(address recipient)" ); // Construct the OrderMetadata type string. bytes memory orderMetadataTypeString = abi.encodePacked( "OrderMetadata(uint8 orderType,uint256 rentDuration,Hook[] hooks,bytes emittedExtraData)" ); // Construct the RentPayload type string. bytes memory rentPayloadTypeString = abi.encodePacked( "RentPayload(OrderFulfillment fulfillment,OrderMetadata metadata,uint256 expiration,address intendedFulfiller)" ); // Derive RentPayload type hash via combination of relevant type strings. rentPayloadTypeHash = keccak256( abi.encodePacked( rentPayloadTypeString, orderMetadataTypeString, orderFulfillmentTypeString ) );
Signer._deriveRentPayloadHash function
function _deriveRentPayloadHash( RentPayload memory payload ) internal view returns (bytes32) { // Derive and return the rent payload hash as specified by EIP-712. return keccak256( abi.encode( _RENT_PAYLOAD_TYPEHASH, _deriveOrderFulfillmentHash(payload.fulfillment), _deriveOrderMetadataHash(payload.metadata), payload.expiration, payload.intendedFulfiller ) ); }
Manual Review.
Update _deriveRentPayloadHash
to be EIP712
compliant by including the missing data and in the right order.
Context
#0 - c4-pre-sort
2024-01-21T17:50:41Z
141345 marked the issue as duplicate of #239
#1 - c4-judge
2024-01-28T21:05:15Z
0xean marked the issue as satisfactory
45.3128 USDC - $45.31
Judge has assessed an item in Issue #361 as 2 risk. The relevant finding follows:
L2,L3
#0 - c4-judge
2024-01-28T21:27:10Z
0xean marked the issue as duplicate of #239
#1 - c4-judge
2024-01-28T21:27:17Z
0xean marked the issue as satisfactory
#2 - c4-judge
2024-02-04T10:25:08Z
0xean marked the issue as not a duplicate
#3 - c4-judge
2024-02-04T10:25:39Z
0xean marked the issue as duplicate of #162
🌟 Selected for report: stackachu
Also found by: 0xA5DF, 0xDING99YA, 0xc695, CipherSleuths, EV_om, HSP, cccz, evmboi32, hals, hash, jasonxiale, juancito, kaden, lanrebayode77, rbserver
22.2973 USDC - $22.30
https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Reclaimer.sol#L93-L99 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Reclaimer.sol#L32-L34 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Reclaimer.sol#L42-L50
The assets are returned back to the lender when the rental is over, and the inherited Reclaimer
contract uses safeTransferFrom
method to transfer back the assets to the lender.
But if the lender (the address who created the rental order) is a smart contract (wallet) that doesn't implement the onERC721Received
or onERC1155Received
(depends on whether the rented asset/assets is/are ERC721,ERC1155 or both); it will not be able to receive these assets as the transaction will revert resulting in permanently locking the assets in the renter SAFE wallet.
This is because the safeTransferFrom
checks if the receiver is a contract or an EOA, if it's a contract then it must have onERC721Received
/onERC1155Received
function implemented as per specs; otherwise the transaction will revert (ERC721._checkOnERC721Received function):
function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) private { if (to.code.length > 0) { try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) { if (retval != IERC721Receiver.onERC721Received.selector) { revert ERC721InvalidReceiver(to); } } catch (bytes memory reason) { if (reason.length == 0) { revert ERC721InvalidReceiver(to); } else { /// @solidity memory-safe-assembly assembly { revert(add(32, reason), mload(reason)) } } } } }
Reclaimer.reclaimRentalOrder function / L93-L99
// Check if the item is an ERC721. if (item.itemType == ItemType.ERC721) _transferERC721(item, rentalOrder.lender); // check if the item is an ERC1155. if (item.itemType == ItemType.ERC1155) _transferERC1155(item, rentalOrder.lender);
Reclaimer._transferERC721 function
function _transferERC721(Item memory item, address recipient) private { IERC721(item.token).safeTransferFrom(address(this), recipient, item.identifier); }
Reclaimer._transferERC1155 function
function _transferERC1155(Item memory item, address recipient) private { IERC1155(item.token).safeTransferFrom( address(this), recipient, item.identifier, item.amount, "" ); }
Manual Review.
Implement another mechanism that enables lenders from pulling their assets to a specific address they wish once the rental is over.
DoS
#0 - c4-pre-sort
2024-01-21T18:02:39Z
141345 marked the issue as duplicate of #65
#1 - c4-judge
2024-01-28T19:25:52Z
0xean marked the issue as satisfactory
#2 - c4-judge
2024-01-30T14:21:44Z
0xean changed the severity to 2 (Med 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
The protocol will allow the use of the same ERC20 tokens that are used by the OpenSea protocol for rental payments, and one of these tokens is USDC.
USDC has a blacklist, where certain addresses are prohibited from receiving/transferring any amount of this token, so if any of the parties (the lender in the case of BASE
order, or the renter in the case of PAY
order) is blocklisted by the USDC token; then the PaymentEscrow.settlePayment
will revert when it tries to transfer the fees for the entitled user.
This will result in permanently locking the rented assets in the renter SAFE wallet as there's no way to extract these assets back.
PaymentEscrow._safeTransfer function
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.
Implement a pulling mechanism instead of pushing mechanism when settling rental payments; this is done by recording the claimablefees for both the renter and the lender and let them claim these fees by themselves by adding another function in the PaymentEscrow
to enable them from withdrawing their entitled fees.
DoS
#0 - c4-pre-sort
2024-01-21T17:36:17Z
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:05Z
0xean marked the issue as satisfactory
🌟 Selected for report: 0xA5DF
Also found by: 0xmystery, 7ashraf, AkshaySrivastav, CipherSleuths, NentoR, SBSecurity, Tendency, ZanyBonzy, ZdravkoHr, dd0x7e8, hals, haxatron, invitedtea, jasonxiale, juancito, kaden, krikolkk, ladboy233, oakcobalt, peanuts, petro_1912, pkqs90, plasmablocks, ravikiranweb3, rbserver, rokinot, souilos
12.582 USDC - $12.58
ID | Title | Severity |
---|---|---|
L-01 | Lack of minimum signers & minimum threshold check | Low |
L-02 | Create.validateOrder function : zoneParams can be replayed | Low |
L-03 | Create.validateOrder function : renter signature can be replayed | Low |
L-04 | PaymentEscrow can be bricked by some payment tokens | Low |
In the current Factory.deployRentalSafe
function design: for risk mitigation, it is suggested to set a minimum of 2 for the threshold
and a minimum of 3 signers/owners.
By doing this; two different signers votes will be needed before a transaction is executed; so in case of a one signer account is compromised; the other two signers will be able to mitigate his actions/change signers, also; if a signer is down, the other two signers will be able to execute transactions out of the SAFE wallet.
Factory.deployRentalSafe function
function deployRentalSafe(
Manual Testing.
Ensure that the minimum owners number is >= 3, and minimum threshold is >= 2.
Create.validateOrder
function : zoneParams
can be replayed <a id="l-02" ></a>validateOrder
function is invoked by the SeaPort with the zoneParams
for the order to be fulfilled, where this argument holds the rental payload and the renter signature, but there's a possibility of replaying the same order again even if it has an expiry for the signature.
function validateOrder( ZoneParameters calldata zoneParams ) external override onlyRole("SEAPORT") returns (bytes4 validOrderMagicValue) {
Manual Testing.
Save the executed zoneParams
in a mapping, and check against it whenever validateOrder
is invoked.
Create.validateOrder
function : renter signature
can be replayed <a id="l-03" ></a>When validateOrder
function is invoked by the SeaPort, the order fulfiller signature is recovered and validated,but there's a possibility of replaying the same siganture again even if it has an expiry for the signature.
// Recover the signer from the payload. address signer = _recoverSignerFromPayload( _deriveRentPayloadHash(payload), signature );
Signer._recoverSignerFromPayload function
function _recoverSignerFromPayload( bytes32 payloadHash, bytes memory signature ) internal view returns (address) { // Derive original EIP-712 digest using domain separator and order hash. bytes32 digest = _DOMAIN_SEPARATOR.toTypedDataHash(payloadHash); // Recover the signer address of the signature. return digest.recover(signature); }
Manual Testing.
Save the recovered signature in a mapping, and check against it whenever validateOrder
is invoked.
PaymentEscrow
can be bricked by some payment tokens<a id="l-04" ></a>Some tokens as cUSDCv3
(Compound USDC) has a special case when the user determines the amount to be transferred to be equals to type(uint256).max
while his balance is less than this amount; in this case his balance will be transferred instead of reverting the transaction:
if (amount == type(uint256).max) { amount = balanceOf(src); }
How could this brick the PaymentEscrow
with cUSDCv3
token as the payment token and resulting in locking the rented assets?
BASE
rental order with cUSDCv3
token as it's payment token.type(uint256).max
, so the escrow will record this max amount as being deposited in it.Stop.stopRentals
; the transaction will revert as the balance of the escrow is way less than it has actually received (the escrow contract received the dust amount instead of receiving the type(uint256).max
, and the recorded balanceOf[token]
is set to type(uint256).max
).This will result in permanently locking the rented assets in the renter SAFE wallet.
Create._rentFromZone function/L599-L603
for (uint256 i = 0; i < items.length; ++i) { if (items[i].isERC20()) { ESCRW.increaseDeposit(items[i].token, items[i].amount); } }
PaymentEscrow._increaseDeposit function
function _increaseDeposit(address token, uint256 amount) internal { // Directly increase the synced balance. balanceOf[token] += amount; }
Manual Review.
Prevent using such tokens in the protocol.
#0 - 141345
2024-01-22T13:53:59Z
361 hals l r nc 0 0 0
L 1 d dup of https://github.com/code-423n4/2024-01-renft-findings/issues/191 L 2 d dup of https://github.com/code-423n4/2024-01-renft-findings/issues/239 L 3 d dup of https://github.com/code-423n4/2024-01-renft-findings/issues/239 L 4 d dup of https://github.com/code-423n4/2024-01-renft-findings/issues/153
#1 - c4-judge
2024-01-27T20:31:28Z
0xean marked the issue as grade-b
#2 - DevHals
2024-02-02T15:17:50Z
Hi @141345 , l-03 of this report is a duplicate of the newly separated issue #162, could you please have a look and upgrade it?
Thanks!
#3 - 141345
2024-02-04T09:29:31Z
Hi @141345 , l-03 of this report is a duplicate of the newly separated issue #162, could you please have a look and upgrade it?
Thanks!
@0xean this is a missed dup of re-grouped issue.
#4 - 0xean
2024-02-04T10:25:48Z
@141345 I believe batching all of the replay attacks is appropriate. therefore L2 and L3 should be dupes of 162, and the only change is the duped issue.
🌟 Selected for report: fouzantanveer
Also found by: 0xA5DF, 0xepley, SAQ, SBSecurity, albahaca, catellatech, clara, hals, hunter_w3b, kaveyjoe, ladboy233, pavankv, peanuts, tala7985, yongskiws
32.5158 USDC - $32.52
reNFT protocol is built on the idea of facilitating ERC721/ERC1155 tokens rental, where it's built on top of SeaPort protocol and Gnosis SAFE wallets where the rented items are going to be reseted in.
Th audit scope consists of 12 contracts with ~ 1905 SLoC.
Methodology adopted during the audit:
validateOrder
.The main invariant that the protocol is more concerned about is preserving the rented asset in the renter SAFE wallet and safely returning the assets back to the lender after the rental is over, but it was noticed that this invariant (where the rented tokens are at risk of being stuck in the renter SAFE wallet) can be broken in the following cases:
If -upon stopRental
function invocation- any of the on-stop hooks reverted or became unsupported.
Knowing the the ERC20 payment tokens used in the protocol are the same ERC20 tokens used in SeaPort, and one of these tokens is USDC where it has a blocklist, then if the order type is a BASE order and the lender that's going to receive the USDC payment is blocklisted, or if the order is PAY and any of the lender or renter is blocklisted (or became after the fullfillment of the order) by the USDC, then the NFT will be stuck
Knowing that USDC is an upgradeable contract that might charge fees in the future, which will result in partially stuck NFTs in the wallets (partially as anyone can send payments directly to the PaymentEscrow
contract to compensate for the deducted fees and rescue the stuck rented assets).
And one important thing is if the recipient is a multisig wallet (contract) that doesn't implement onERC721Received
or onERC1155Received
; then they can't receive their rented tokens via safeTransfer
and the assets will be stuck permanently in the renter SAFE wallet (this is a different issue from the one reported by the winning bot: where the SAFE wallet is unable to receive NFTs or ERC1155 tokens as the don't inherint the OZ ERC721Holder ERC1155Holder contracts).
The protocol uses EIP712
specifications to create hashes for orders and orders metadata, but it was noticed that these hashes are not compliant with the specifications.
In cases of hardforks, the protocol will be disabled (temporarily until upgrading the Create
contract) as it uses an immutable to save the EIP712 _DOMAIN_SEPARATOR
that encodes the chain.id
at the time of the deployment; so after a hardfork, signatures sent to verify SeaPort calls to the validateOrder
will not be validated and the fulfilling orders functionality will not be working anymore.
Starting and Fulfilling a Rental Order If Alice has an NFT that she would like to lend -> she will have to interact with the reNFT frontend to create a rentalOrder -> then the order is checked in the server-side and validated -> then this order is listed on SeaPort -> Now Bob wants to rent this NFT -> he will interact with reNFT with a signed order -> the reNFT server-side will interact with SeaPort to fulfill the order -> Then the rented assets will be moved to a SAFE wallet created and whitlisted for the renter
Stoppinga a Rental Order
If the time of the rental is over or if Alice decided to stop the rental (if it's a PAY order) -> stopRental
is called where the rented assets are returned back to Alice -> and the payment is paid for the intended parties (for Bob if it's a BASE order, and for Bob and Alice if it's a PAY order that hasn't ended yet).
note : to ease the following the flow, Bob acts as he interacts with SeaPort directly, while he is actually interacting with reNFT front-end, and the reNFT server-side will interact with SeaPort to fulfill the order.
The protocol adopts the default framework architectural model, where it separates contracts into internal modules (back-end) and external policies (user facing contracts/front-end) for flexible, modular protocol design, where modules manage specific data models independently, while policies handle inbound calls and route updates to Modules, acting as intermediaries.
This separation of data management (what) from business logic (why) enhances flexibility and immutability, simplifying protocol development and any future features addition.
All the contracts are managed by the Kernel contract, which is a registry that manages all the changes and upgrades of the protocol: adding/removing policies and modules.
In general, the codebase is robust and follows the security best practices and implementations, but it would be a safe play if a pulling mechanism is adopted instead of pushing mechanism when the rented assets are returned and when the payments are paid for the intended parties, as this would prevent any risks related to denial-of-service due to external un-expected behaviours.
Also following the EIP712 specifications when creating hashes would prevent any integration issues with SeaPort protocol.
Approximately 25 hours ; divided between reading the documentation, manually reviewing the codebase, and documenting my findings.
25 hours
#0 - c4-judge
2024-01-27T20:23:44Z
0xean marked the issue as grade-b