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: 82/116
Findings: 3
Award: $15.74
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 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#L210 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/policies/Create.sol#L464
Each order can contain an array of hooks, which should be whitelisted by the admin. After the function for finalizing order creation gets called (Create.validateOrder
) a check is made if the provided hooks are whitelisted to run onStart()
(at the start of the rent). However, no check is made if they are allowed to run hookOnStop
(when rent is ended). This check is made only when the rent is being stopped (Stop.stopRent
). This means rentals with invalid hookOnStop
hooks can be created. As a consequence stopping will not be possible, because the check will revert
Quoting the explanation on hooks from the docs:
When signing a rental order, the lender can decide to include an array of Hook structs along with it. These are bespoke restrictions or added functionality that can be applied to the rented token within the wallet.
The rental is finalized in the protocol when validateOrder
in Create.sol
get's called. Inside of it a validation is made on the provided hooks:
function _addHooks( Hook[] memory hooks, SpentItem[] memory offerItems, address rentalWallet ) internal { ....... // Loop through each hook in the payload. for (uint256 i = 0; i < hooks.length; ++i) { // Get the hook's target address. target = hooks[i].target; // @audit no check for `hookOnStop` is made // Check that the hook is reNFT-approved to execute on rental start. if (!STORE.hookOnStart(target)) { revert Errors.Shared_DisabledHook(target); } ........ } }
No validation is made if the hooks provided will also run when the rent is concluded.
This could be a problem because the stopRent
function inside Stop.sol
does a validation on the hooks and reverts if even one of them is not whitelisted:
function _removeHooks( Hook[] calldata hooks, Item[] calldata rentalItems, address rentalWallet ) internal { .... // Loop through each hook in the payload. for (uint256 i = 0; i < hooks.length; ++i) { // Get the hook address. target = hooks[i].target; // Check that the hook is reNFT-approved to execute on rental stop. if (!STORE.hookOnStop(target)) { <------------- This will revert revert Errors.Shared_DisabledHook(target); } ........ }
The result is that invalid rental orders are allowed to be created, which obviously could be a problem.
Imagine a scenario with a PAY
order (where the lender gives his assets and also token payment to the renter). And he wants to end the rent prematurely, because he needs the assets or probably the renter does not behave as expected.
Even in normal circumstances when the appropriate conditions are met, rentals should not be blocked from being stopped, since this affects the normal functioning of the protocol.
To further reinforce my arguments. I'm quoting the provided docs on hooks:
Only hooks which have been enabled by the admin will be valid when passed to the address target field
https://github.com/code-423n4/2024-01-renft/blob/main/docs/hooks.md
Manual review
Add the following check inside _addHooks
in Create.sol
:
if (!STORE.hookOnStop(target)) { revert Errors.Shared_DisabledHook(target); }
Invalid Validation
#0 - c4-pre-sort
2024-01-21T19:25:25Z
141345 marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-23T14:51:53Z
141345 marked the issue as duplicate of #501
#2 - c4-judge
2024-01-28T19:36:53Z
0xean marked the issue as satisfactory
🌟 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
While a rental is active, the Guard
contract should prevent any token operations such as (transfer, approve, etc.), so that lender assets could not be stolen. Problem is that Guard.sol
does not account for burnable & pausable tokens (which are quite common as well). This means lenders could lose such assets if the renter acts maliciously.
According to the docs:
The Guard Prevents transfers of ERC721 and ERC1155 tokens while a rental is active, as well as preventing token approvals and enabling of non-whitelisted gnosis safe modules.
The validation of the functions being called happens inside _checkTransaction
of Guard.sol
:
function _checkTransaction(address from, address to, bytes memory data) private view { bytes4 selector; // Load in the function selector. assembly { selector := mload(add(data, 0x20)) } //@audit burn && pause NOT checked if (selector == e721_safe_transfer_from_1_selector) { _revertSelectorOnActiveRental(...) } else if (selector == e721_safe_transfer_from_2_selector) { _revertSelectorOnActiveRental(...) } else if (selector == e721_transfer_from_selector) { _revertSelectorOnActiveRental(...) } ...... }
Problem is that no validation is made for the following methods:
burn
(ERC721)pause
(ERC721)burnBatch
(ERC1155)This means that a renter could call any of those methods on the assets in his safe and destroy the assets(if calling burn
| burnBatch
) or pause them and prevent the transfer back to the lender once rental period is over.
I would like to refer to the publicly known issues section on Dishonest ERC721/ERC1155 Implementations
, which states:
The Guard contract can only protect against the transfer of tokens that faithfully implement the ERC721/ERC1155 spec. A dishonest implementation that adds an additional function to transfer the token to another wallet cannot be prevented
The mentioned methods are NOT dishonest or some exotic implementation of ERC721/ERC1155. They are part of the official OpenZeppelin contracts for the two standards and are thus wildly used all the time.
Myself as a web3 dev have implemented such tokens 3 times already!
Manual review
One option is to add the selectors for those methods to the _checkTransaction
function.
Even better approach would be to invert the mechanism. Instead of checking for each method separately (the list can get quite long), a whitelist of allowed methods (primarily get methods) could be made. This way any other call would not be allowed, thus making the safe secure from dishonest standart implementations
Invalid Validation
#0 - c4-pre-sort
2024-01-21T17:39:34Z
141345 marked the issue as duplicate of #323
#1 - c4-judge
2024-01-28T20:06:38Z
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
2.3438 USDC - $2.34
https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Signer.sol#L151-L151 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Signer.sol#L373-L375 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Signer.sol#L384-L386 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Signer.sol#L232-L238 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/policies/Create.sol#L636
Being not EIP712 compliant can lead to issues with integrators and possibly DOS.
The implementation of the hook hash (here) is done incorrectly. hook.extraData
is of type bytes
which according to EIP712 it is referred to as a dynamic type
. Dynamic types must be first hashed with keccak256
to become one 32-byte word before being encoded and hashed together with the typeHash and the other values.
function _deriveHookHash(Hook memory hook) internal view returns (bytes32) { // Derive and return the hook as specified by EIP-712. return keccak256( - abi.encode(_HOOK_TYPEHASH, hook.target, hook.itemIndex, hook.extraData) + abi.encode(_HOOK_TYPEHASH, hook.target, hook.itemIndex, keccak256(hook.extraData)) ); }
Some TypeHashes are computed by using abi.encode
instead of abi.encodePacked
(here) which makes the typeHash value and the whole final hash different from the hash that correctly implementing EIP712 entities would get.
// Construct the Item type string. bytes memory itemTypeString = abi.encodePacked( "Item(uint8 itemType,uint8 settleTo,address token,uint256 amount,uint256 identifier)" ); // Construct the Hook type string. bytes memory hookTypeString = abi.encodePacked( "Hook(address target,uint256 itemIndex,bytes extraData)" ); // Construct the RentalOrder type string. bytes memory rentalOrderTypeString = abi.encodePacked( "RentalOrder(bytes32 seaportOrderHash,Item[] items,Hook[] hooks,uint8 orderType,address lender,address renter,address rentalWallet,uint256 startTimestamp,uint256 endTimestamp)" ); ... rentalOrderTypeHash = keccak256( abi.encode(rentalOrderTypeString, hookTypeString, itemTypeString) );
The problem with this is that abi.encode-ing strings results in bytes with arbitrary length. In such cases (like the one here) there is a high chance that the bytes will not represent an exact N words in length (X * 32 bytes length) and the data is padded to conform uniformly to 32-byte words. This padding results in an incorrect hash of the typeHash and it will make the digest hash invalid when compared with properly implemented hashes from widely used libraries such as ethers.
Place the following code in any of the tests and run forge test -—mt test_EIP712_encoding
function test_EIP712_encoding() public { // Copied from the reNFT codebase bytes memory itemTypeString = abi.encodePacked( "Item(uint8 itemType,uint8 settleTo,address token,uint256 amount,uint256 identifier)" ); bytes memory hookTypeString = abi.encodePacked( "Hook(address target,uint256 itemIndex,bytes extraData)" ); bytes memory rentalOrderTypeString = abi.encodePacked( "RentalOrder(bytes32 seaportOrderHash,Item[] items,Hook[] hooks,uint8 orderType,address lender,address renter,address rentalWallet,uint256 startTimestamp,uint256 endTimestamp)" ); // protocol implementation bytes32 rentalOrderTypeHash = keccak256( abi.encode(rentalOrderTypeString, hookTypeString, itemTypeString) // <----- ); // correct implementation bytes32 rentalOrderTypeHashCorrect = keccak256( abi.encodePacked(rentalOrderTypeString, hookTypeString, itemTypeString) // <----- ); // the correct typehash bytes32 correctTypeHash = keccak256( "RentalOrder(bytes32 seaportOrderHash,Item[] items,Hook[] hooks,uint8 orderType,address lender,address renter,address rentalWallet,uint256 startTimestamp,uint256 endTimestamp)Hook(address target,uint256 itemIndex,bytes extraData)Item(uint8 itemType,uint8 settleTo,address token,uint256 amount,uint256 identifier)" ); assertNotEq(rentalOrderTypeHash, rentalOrderTypeHashCorrect); assertNotEq(rentalOrderTypeHash, correctTypeHash); assertEq(rentalOrderTypeHashCorrect, correctTypeHash); }
This test shows that the rentalOrderTypeHashCorrect
is the correct typeHash.
rentalOrderTypeHash = keccak256( - abi.encode(rentalOrderTypeString, hookTypeString, itemTypeString) + abi.encodePacked(rentalOrderTypeString, hookTypeString, itemTypeString) );
_deriveOrderMetadataHash
constructs the hash incorrectly because _ORDER_METADATA_TYPEHASH includes uint8 orderType
and bytes emittedExtraData
(it can be seen here) but these values are not provided below the typeHash (it can be seen here).
function _deriveOrderMetadataHash( OrderMetadata memory metadata ) internal view returns (bytes32) { bytes32[] memory hookHashes = new bytes32[](metadata.hooks.length); for (uint256 i = 0; i < metadata.hooks.length; ++i) { hookHashes[i] = _deriveHookHash(metadata.hooks[i]); } return keccak256( abi.encode( _ORDER_METADATA_TYPEHASH,// OrderMetadata(uint8 orderType,uint256 rentDuration,Hook[] hooks,bytes emittedExtraData) // <---- misses uint8 orderType metadata.rentDuration, keccak256(abi.encodePacked(hookHashes)) // <---- misses bytes emittedExtraData ) ); }
This hash is important because it is compared to the zoneHash inside Create.sol:validateOrder → _rentFromZone → _isValidOrderMetadata
(link): So if the provided zoneHash from Seaport was generated correctly and it is not the same as the one generated by reNFT, the protocol will not be able to create any rentalOrders resulting in DOS.
In any case, this implementation is not according to EIP712 and either the fields must be included or the _ORDER_METADATA_TYPEHASH
must remove uint8 orderType
and bytes emittedExtraData
Manual review and Foundry for testing
Apply the described fixes for each example
Other
#0 - c4-pre-sort
2024-01-21T17:48:26Z
141345 marked the issue as primary issue
#1 - c4-pre-sort
2024-01-21T17:48:29Z
141345 marked the issue as sufficient quality report
#2 - c4-sponsor
2024-01-24T18:58:16Z
Alec1017 (sponsor) confirmed
#3 - c4-judge
2024-01-27T19:28:38Z
0xean marked the issue as satisfactory
#4 - c4-judge
2024-01-28T21:05:27Z
0xean removed the grade
#5 - c4-judge
2024-01-28T21:05:35Z
0xean marked the issue as satisfactory
#6 - c4-judge
2024-01-29T10:27:01Z
0xean marked the issue as selected for report