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: 15/116
Findings: 6
Award: $962.50
๐ Selected for report: 0
๐ 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/main/src/policies/Stop.sol#L194-L212
The protocol has admin-owned hooks, which can be used instead of _checkTransaction
from GnosisSafe to disable transferring and approving the rented assets. There can be cases where hooks can contain vulnerable code in their onTransaction
function and admins should disable them. Still, a malicious user can effectively block them from disabling by creating a rent to himself with a long rent period. As there will most likely be other rents using these hooks, admins will be forced to call updateHookStatus
for onStop
and all funds will be locked inside the PaymentEscrow
contract, with no way to be recovered.
function _removeHooks( Hook[] calldata hooks, Item[] calldata rentalItems, address rentalWallet ) internal { // Define hook target, item index, and item. address target; uint256 itemIndex; Item memory item; // 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)) { --> revert Errors.Shared_DisabledHook(target); //@audit will revert if hook onStop function is disabled }
Manual Review
Consider adding a blacklist to disable hooks only for certain renters, this will ensure that other users who are using the same hook will be able to stop their rents, without losing their funds.
Context
#0 - c4-pre-sort
2024-01-21T19:01:14Z
141345 marked the issue as sufficient quality report
#1 - 141345
2024-01-23T10:38:06Z
intentionally retain malicious hook, onStop
function will be affected to lock fund
#2 - c4-pre-sort
2024-01-23T13:28:52Z
141345 marked the issue as primary issue
#3 - c4-sponsor
2024-01-25T18:39:54Z
Alec1017 (sponsor) confirmed
#4 - c4-judge
2024-01-27T18:03:06Z
0xean marked the issue as satisfactory
#5 - c4-judge
2024-01-29T10:28:01Z
0xean marked the issue as selected for report
#6 - akshaysrivastav
2024-01-30T06:38:07Z
Hey @0xean thanks for judging this. I think this report needs a revisit.
onTransaction
hook. The sponsors have already shared their input on this hook https://github.com/code-423n4/2024-01-renft-findings/issues/396#issuecomment-1910470022.Can you please recheck the impact, validity and duplication status of this report. Thanks.
#7 - Slavchew
2024-01-30T08:03:20Z
Hi @akshaysrivastav,
Now leave it to @0xean to conclude if this is another path and a different issue or if it's a duplicate, but it's definitely valid.
#8 - 0xEVom
2024-01-30T11:38:54Z
Agree that this is just another way of framing #501.
onStop
hook and it is disabled, stopRent
will always revert.onStop
hook, the admin will refrain from disabling it because that would brick the rentals.Seems like two sides of the same coin to me.
#9 - c4-judge
2024-01-30T14:27:24Z
0xean marked the issue as duplicate of #501
#10 - 0xean
2024-01-30T14:27:26Z
agree, should be duped.
#11 - c4-judge
2024-02-01T15:55:34Z
0xean marked the issue as not selected for report
๐ 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
ERC721 and ERC1155 tokens that implement ERC721Burnable
can be rented for a short rent period and maliciously burned because burn
function is not disabled in Guard::checkTransaction
.
import { //@audit burn selector is missing shared_set_approval_for_all_selector, e721_approve_selector, e721_safe_transfer_from_1_selector, e721_safe_transfer_from_2_selector, e721_transfer_from_selector, e721_approve_token_id_offset, e721_safe_transfer_from_1_token_id_offset, e721_safe_transfer_from_2_token_id_offset, e721_transfer_from_token_id_offset, e1155_safe_transfer_from_selector, e1155_safe_batch_transfer_from_selector, e1155_safe_transfer_from_token_id_offset, e1155_safe_batch_transfer_from_token_id_offset, gnosis_safe_set_guard_selector, gnosis_safe_enable_module_selector, gnosis_safe_disable_module_selector, gnosis_safe_enable_module_offset, gnosis_safe_disable_module_offset } from "@src/libraries/RentalConstants.sol";
Burning will lead to loss of funds from lender as it will cost him the total price of the NFT + amount from the rent because Stop::stopRent()
will revert when trying to transfer back the token to the lender.
Renter will have to pay for the rent period, but there are ERC721
tokens that return the floor price of the NFT when burned.
Opensea allows NFT collections with a burn mechanism and most of the NFT users donโt have the appropriate knowledge given the fact that they are not technical users.
There are some NFT collections that support token burning:
https://opensea.io/learn/nft/what-is-nft-burning
As we can see there are NFTs with burn functionality and prices over $100k.
Guard::checkTransaction()
and burns the token.Here is a test that demonstrates step 4, due to the off-chain nature of previous steps:
test/unit/Guard/CheckTransaction.sol
bytes4 public e721_burn_selector = 0x42966c68;
forge test --match-test test_Success_CheckTransaction_ERC721_Burn
function test_Success_CheckTransaction_ERC721_Burn() public { // Create a rentalId array RentalAssetUpdate[] memory rentalAssets = new RentalAssetUpdate[](1); rentalAssets[0] = RentalAssetUpdate(RentalUtils.getItemPointer(address(alice.safe), address(erc721s[0]), 0), 1); // Mark the rental as actively rented in storage _markRentalsAsActive(rentalAssets); //@audit needed to mark the assets as rented which will trigger the checkTransaction execution bytes memory burnFromCalldata = abi.encodeWithSelector(bytes4(keccak256(bytes("burn(uint256)"))), 0); //@audit burn is not handled in checkTransaction allowing renters to burn tokens // Expect revert because of an unauthorized function selector _checkTransactionRevertUnauthorizedSelector( address(alice.safe), address(erc721s[0]), e721_burn_selector, burnFromCalldata ); }
Guard
contract and tx passes successfully.Manual Review
Add burn selector to the Guard
and disable this functionality too, there are only negatives of allowing such transactions.
import { shared_set_approval_for_all_selector, e721_approve_selector, e721_safe_transfer_from_1_selector, e721_safe_transfer_from_2_selector, e721_transfer_from_selector, e721_approve_token_id_offset, e721_safe_transfer_from_1_token_id_offset, e721_safe_transfer_from_2_token_id_offset, e721_transfer_from_token_id_offset, + e721_burn_selector, + e1155_burn_selector, + e1155_burn_batch_selector, e1155_safe_transfer_from_selector, e1155_safe_batch_transfer_from_selector, e1155_safe_transfer_from_token_id_offset, e1155_safe_batch_transfer_from_token_id_offset, gnosis_safe_set_guard_selector, gnosis_safe_enable_module_selector, gnosis_safe_disable_module_selector, gnosis_safe_enable_module_offset, gnosis_safe_disable_module_offset } from "@src/libraries/RentalConstants.sol";
+ // bytes4(keccak256("burn(uint256)")); + bytes4 constant e721_burn_selector = 0x42966c68; + uint256 constant e721_burn_token_id_offset = 0x24; + // bytes4(keccak256("burn(address,uint256,uint256)")); + bytes4 constant e1155_burn_selector = 0xf5298aca; + uint256 constant e1155_burn_token_id_offset = 0x24; + // bytes4(keccak256("burnBatch(address,uint256[],uint256[])")); + bytes4 constant e1155_burn_batch_selector = 0x6b20c454;
Guard.sol::_checkTransaction() add following code:
else if (selector == e721_burn_selector) { // Load the token ID from calldata. uint256 tokenId = uint256( _loadValueFromCalldata(data, e721_burn_token_id_offset) ); // Check if the selector is allowed. _revertSelectorOnActiveRental(selector, from, to, tokenId); } else if (selector == e1155_burn_selector) { // Load the token ID from calldata. uint256 tokenId = uint256( _loadValueFromCalldata(data, e1155_burn_token_id_offset) ); // Check if the selector is allowed. _revertSelectorOnActiveRental(selector, from, to, tokenId); } else { if (selector == e1155_burn_batch_selector) { revert Errors.GuardPolicy_UnauthorizedSelector(e1155_burn_batch_selector); } }
ERC721
#0 - c4-pre-sort
2024-01-21T17:39:37Z
141345 marked the issue as duplicate of #323
#1 - c4-judge
2024-01-28T20:06:40Z
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
OrderMetadata struct has nested an array of the Hook struct. This Hook struct is missing from the OrderMeta typehash, which makes the OrderMetadata not compliant with EIP712, and with that, the RentPayload is also not compliant because it has OrderMetadata in it.
The OrderMetadata typehash is composed in _deriveRentalTypehashes()
as you see, the Hook typestring is missing.
Also, you can see the RentalOrder struct, which has nested structs but composes the typehash correctly.
https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Signer.sol#L406
struct OrderMetadata { // Type of order being created. OrderType orderType; // Duration of the rental in seconds. uint256 rentDuration; // Hooks that will act as middleware for the items in the order. Hook[] hooks; // Any extra data to be emitted upon order fulfillment. bytes emittedExtraData; }
function _deriveRentalTypehashes() internal pure returns ( bytes32 itemTypeHash, bytes32 hookTypeHash, bytes32 rentalOrderTypeHash, bytes32 orderFulfillmentTypeHash, bytes32 orderMetadataTypeHash, bytes32 rentPayloadTypeHash ) { // 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)" ); // Derive the Item type hash using the corresponding type string. itemTypeHash = keccak256(itemTypeString); // Derive the Hook type hash using the corresponding type string. hookTypeHash = keccak256(hookTypeString); // Derive the RentalOrder type hash using the corresponding type string. rentalOrderTypeHash = keccak256( abi.encode(rentalOrderTypeString, hookTypeString, itemTypeString) ); { // 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 ) ); // Derive the OrderFulfillment type hash using the corresponding type string. orderFulfillmentTypeHash = keccak256(orderFulfillmentTypeString); // Derive the OrderMetadata type hash using the corresponding type string. // @audit hookTypeString is missing orderMetadataTypeHash = keccak256(orderMetadataTypeString); } }
If the struct type references other struct types (and these in turn reference even more struct types), then the set of referenced struct types is collected, sorted by name and appended to the encoding. An example encoding isย
Transaction(Person from,Person to,Asset tx)Asset(address token,uint256 amount)Person(address wallet,string name)
. - from EIP 712 (link)
Also read this Stack Overflow page - link
Manual Review
Make sure to include the typestring for Hook
when generating the typehash for OrderMetadata
.
function _deriveRentalTypehashes() internal pure returns ( bytes32 itemTypeHash, bytes32 hookTypeHash, bytes32 rentalOrderTypeHash, bytes32 orderFulfillmentTypeHash, bytes32 orderMetadataTypeHash, bytes32 rentPayloadTypeHash ) { // 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)" ); // Derive the Item type hash using the corresponding type string. itemTypeHash = keccak256(itemTypeString); // Derive the Hook type hash using the corresponding type string. hookTypeHash = keccak256(hookTypeString); // Derive the RentalOrder type hash using the corresponding type string. rentalOrderTypeHash = keccak256( abi.encode(rentalOrderTypeString, hookTypeString, itemTypeString) ); { // 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 ) ); // Derive the OrderFulfillment type hash using the corresponding type string. orderFulfillmentTypeHash = keccak256(orderFulfillmentTypeString); // Derive the OrderMetadata type hash using the corresponding type string. // @audit hookTypeString is missing - orderMetadataTypeHash = keccak256(orderMetadataTypeString); + orderMetadataTypeHash = keccak256(abi.encode(orderMetadataTypeString, hookTypeString)); } }
en/de-code
#0 - c4-pre-sort
2024-01-21T17:51:15Z
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:06:04Z
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
The encoding of OrderMetadata encodedData
is inaccurately performed, leading to issues in verifying EIP712 signed messages.
Signer::_deriveOrderMetadataHash()
is used in Create::_isValidOrderMetadata()
and Signer::_deriveOrderMetadataHash()
to check if the hash of the OrderMetadata
struct is valid. However, there's a mistake in the encoding process because it doesn't hash all the members of OrderMetadata
.
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. // @audit why not pass 'orderType' and 'emittedExtraData' @> return keccak256( abi.encode( _ORDER_METADATA_TYPEHASH, metadata.rentDuration, keccak256(abi.encodePacked(hookHashes)) ) ); }
As you can see the encodedData
includes only OrderMetadata.rentDuration
and OrderMetadata.hooks
, while OrderMetadata.orderType
and OrderMetadata.emittedExtraData
are not included.
According to EIP712, all struct members should be hashed in the order that they are defined.
The encoding of a struct instance isย
enc(valueโ) โ enc(valueโ) โ โฆ โ enc(valueโ)
, i.e. the concatenation of the encoded member values in the order that they appear in the type. Each encoded member value is exactly 32-byte long.
Read Moreย here
All of this will result in that only wrong hashed OrderMetadata though Signer::_deriveOrderMetadataHash() to pass when Seaport call validateOrder(). If the Lender pass correctly composed hash in Seaport::OrderComponents::zoneHash
by himself and do not use Create::getOrderMetadataHash()
for it, any rental will revert because in Create::_isValidOrderMetadata()
.
This situation leads to an issue where only an incorrectly hashed OrderMetadata
can pass through Signer::_deriveOrderMetadataHash()
during the Seaport validateOrder()
. Even if the Lender provides the correct hash in Seaport::OrderComponents::zoneHash
, any rental will still revert in Create::_isValidOrderMetadata()
.
In the tests, when the lender provides the zoneHash
, it relies on the incorrect _deriveOrderMetadataHash()
. As a result, all these tests will pass, even though the underlying issue exists.
For accurate testing, use the correct OrderMetadata hash. This hash is generated in Remix using the fixed function outlined in the Recommendations section.
Hash: 0x04d9e28e69367406d7fdf7aaeb155428b0555405dbdf6521bfc87f6d3d19867c
Insert it into the test/fixture/engine/OrderCreator.sol
file at line 280 exactly as it is shown.
.withZoneHash(0x04d9e28e69367406d7fdf7aaeb155428b0555405dbdf6521bfc87f6d3d19867c)
Run the basic test in Rent::test_Success_Rent_BaseOrder_ERC721
. The test will revert CreatePolicy_InvalidOrderMetadataHash
.
Run with: forge test --match-test "test_Success_Rent_BaseOrder_ERC721" -vvvv
Note: The
Reason: log != expected log
message is a result of someexpectEmit()
calls, but it doesn't affect the test outcome.
Manual Review
Add orderType
and emittedExtraData
when create the hashStruct
.
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. // @audit why not pass 'orderType' and 'emittedExtraData' return keccak256( abi.encode( _ORDER_METADATA_TYPEHASH, + metadata.orderType, metadata.rentDuration, keccak256(abi.encodePacked(hookHashes)), + metadata.emittedExtraData ) ); }
en/de-code
#0 - c4-pre-sort
2024-01-21T17:50:02Z
141345 marked the issue as duplicate of #239
#1 - c4-judge
2024-01-28T20:18:48Z
0xean changed the severity to 2 (Med Risk)
#2 - c4-judge
2024-01-28T21:04:38Z
0xean marked the issue as satisfactory
๐ Selected for report: said
Also found by: SBSecurity
779.7358 USDC - $779.74
Judge has assessed an item in Issue #604 as 2 risk. The relevant finding follows:
L3
#0 - c4-judge
2024-01-30T19:21:03Z
0xean marked the issue as duplicate of #220
#1 - c4-judge
2024-01-30T19:21:07Z
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
135.0382 USDC - $135.04
Count | Title |
---|---|
[L-01] | CryptoPunk NFTs will be stolen |
[L-02] | Kernel deployment can be frontrunned |
[L-03] | ERC721Pausable tokens can block reclaiming |
[L-04] | Some NFTs will return the floor price when burned |
[L-05] | No validation if Order.orderType and OrderMetadata.orderType are equal |
Total Low Risk Issues | 5 |
---|
Count | Title |
---|---|
[NC-01] | Typos |
[NC-02] | CheckTransaction can be improved |
[NC-03] | setActiveStatus can implement check for status beforehand |
Total Non-Critical Issues | 3 |
---|
Issue Description:
CryptoPunks collection was created before EIP712 was introduced and has an additional function used to offer punk for sale to certain addresses:
offerPunkForSaleToAddress
.
function offerPunkForSaleToAddress(uint punkIndex, uint minSalePriceInWei, address toAddress) { if (!allPunksAssigned) throw; if (punkIndexToAddress[punkIndex] != msg.sender) throw; if (punkIndex >= 10000) throw; punksOfferedForSale[punkIndex] = Offer(true, punkIndex, msg.sender, minSalePriceInWei, toAddress); PunkOffered(punkIndex, minSalePriceInWei, toAddress); }
CryptoPunk NFT renter can call freely this function because Guard is not handling it and pass 1 wei as a price.
Then after rental period is terminated he will still have approval to buy it without any interruptions.
Recommendation:
There is no universal mitigation for all these non-standard NFT implementations but care must be taken when dealing with such tokens and especially CryptoPunks as they are the foundation of the whole NFT ecosystem.
Issue Description:
Kernel relies on the deployer to specify addresses of the Admin
and Executor
.
//@audit frontrun constructor(address _executor, address _admin) { executor = _executor; admin = _admin; }
There is also functionality to migrate kernel to a new implementation.
function executeAction(Actions action_, address target_) external onlyExecutor { ...More code } else if (action_ == Actions.MigrateKernel) { ensureContract(target_); _migrateKernel(Kernel(target_)); } ...More code emit Events.ActionExecuted(action_, target_); }
That way of setting roles which are critical for the protocol opens frontrun possibility by a person who will set himself as an executor
since this is the person with most privileges across the reNFT
contract.
Recommendation:
Consider adding Ownable2Step
to the Kernel
and assign the owner from there, after that have a separate onlyOwner
function to set and change the executor
role.
Issue Description:
Since ERC721Pausable is an extension to the EIP712 standard, there will be many tokens that are going to implement it.
One possible use case of renting an NFTs will be to show proof of an ownership to an specific game or organisation, for example Bored Ape or Axie Infinity game token.
If pausable token is being renter there is nothing to stop the renter from pausing the transfers of these NFTs.
He will be able to continue to use it, while the rent will be permanent and there will be no way to recover the rented items from the Stop
module.
Recommendation:
Consider adding pause
/unpause
selectors to the Guard::checkTransaction
, that will successfully mitigate the problem.
Issue Description:
There are certain NFTs that will return the floor price to the owner when burned. One example is the [Burnables](https://opensea.io/collection/burnables-nft?embed%5B0%5D=test&embed%5B1%5D=test(.(%22()%27.%2C%2C). We can verify this statement from the description of the collection:
The first-ever refundable NFT. The owner of the token can burn their NFT at any time and have the original mint price sent to their wallet.
Users who have such a token and are non-technical can also rent their NFTs. Then renter who spotted it can rent it for short period and burn it to receive the mint price of the NFT
.
This will result in a direct profit for renter, and big loss for the lender because rent payment wonโt be distributed as well.
Recommendation:
Add burn function selector which will remove all the malicious use cases for such tokens.
Order.orderType
and OrderMetadata.orderType
are equalIssue Description:
The OrderMetadata
hash, generated in _deriveOrderMetadataHash()
, is missing two crucial members: orderType
and emittedExtraData
. When fulfilling an order, Seaport calls Create::validateOrder()
, performing necessary logic on zones, conversion, and checks. The problem arises because the Lender sets Order.orderType
initially, but the Renter can later provide a different value in OrderMetadata
. As the hash doesn't consider orderType
, _isValidOrderMetadata()
passes, causing items to be converted based on the Renter's OrderMetadata.orderType
, leading to potential inconsistency. For example, if the Lender sets PAY
as the Order.orderType
, the Renter can pass BASE
in OrderMetadata
, resulting in inconsistency.
As all PAY
orders are provided with PAYEE
by the reNFT backend and executed collectively, the second validateOrder()
call in _processBaseOrderConsideration()
reverts, because it will try to convert ERC721, but the function only work with ERC20. However, this scenario has no impact whatsoever, except for the pre-existing inconsistency.
Recommendation:
To avoid the inconsistency, consider checking whether Order.orderType
equals OrderMetadata.orderType
initially in validateOrder()
. This step can help skip unnecessary computations or align all logic based on Order.orderType
rather than relying on the fulfiller's OrderMetadata.orderType
.
Issue Description:
Various typos are presented in the codebase that should be fixed to improve the overall quality of the code and make the understanding easier:
as a a kernel โ as a kernel
- @dev Instantiate this contract as a a kernel adapter. When using a proxy, the kernel address + @dev Instantiate this contract as a kernel adapter. When using a proxy, the kernel address
as a a kernel โ as a kernel
- @dev Instantiate this contract as a a kernel adapter. When using a proxy, the kernel address + @dev Instantiate this contract as a kernel adapter. When using a proxy, the kernel address
seaport โ Seaport
- @param considerations Array of seaport consideration items. + @param considerations Array of Seaport consideration items.
CheckTransaction
can be improvedIssue Description:
CheckTransaction
is used to prevent transfers and approvals of rented assets originated from safe wallet which has enabled this guard.
Special case are the batch
transactions: safeTransferBatch
, safeTransferFromBatch
. They are totally forbidden from the Guard
even if they donโt contain rented assets, due to the complexity that they add - needing to extract the tokenId offset for every token passed in the ids array:
} 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 ); } }
This checks can be simplified by placing all the batch
transactions into the else if
:
else if ( selector == shared_set_approval_for_all_selector || selector == e1155_safe_batch_transfer_from_selector || selector == gnosis_safe_set_guard_selector ) { revert Errors.GuardPolicy_UnauthorizedSelector(selector); }
This will make the whole function shorter and easier to spot that this types of transactions are not supported for the entire safe wallet.
setActiveStatus
can implement check for status beforehandIssue Description:
setActiveStatus
function can check for the status of the policy before setting it, that will improve the flow:
function setActiveStatus(bool activate_) external onlyKernel { + require(activate_ != isActive, "Cannot re-set same status"); isActive = activate_; }
#0 - 141345
2024-01-22T13:51:50Z
604 SBSecurity l r nc 2 1 2
L 1 d dup of https://github.com/code-423n4/2024-01-renft-findings/issues/111 L 2 b L 3 d dup of https://github.com/code-423n4/2024-01-renft-findings/issues/220 L 4 l L 5 l L 6 n L 7 r L 8 n
#1 - c4-judge
2024-01-27T20:28:59Z
0xean marked the issue as grade-a
#2 - Slavchew
2024-01-30T08:36:49Z
Hi @0xean,
L1 and L3 are forgotten to be upgraded.
๐ 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 focused on lending and collateral-free rentals of ERC721 and ERC1155 tokens in integration with OpenSeaโs engine - Seaport and GnosisSafe to prevent theft of rented assets. The protocol allows collateral-free in-house renting, lending, and reward sharing, focusing on scholarship automation.
The key contracts of reNFT protocol for this audit are:
Stop
policy) are routed and checked in the checkTransaction
function.Kernel
- core contract used as a registry, manages permissions, upgrades, activates, and deactivates policies and modules.Create2Deploy
- contract used for a deterministic salt-based deployment with added cross-chain safety.PaymentEscrow
- internal ERC20
accounting proxy contract, also calculates involved partiesโ payments.Storage
- module proxy contract used as storage - keeping, and maintaining information about hooks, rentals, and safes. Additions and removals are routed to functions in this contract.Admin
- contract responsible for fee management, whitelisting delegates and extensions, and upgrading modules.Create
- contract serving as a main entry point for renters after the order is being fulfilled, used to convert Order and Consideration structs to reNFT native and save it into memory allocated mapping.Factory
- contract used as deployment agent for Gnosis safe wallets and setting predefined wallet module (Stop
) and guard (Guard.sol
) to prevent malicious transactions from being executed.Guard
- contract inheriting from Gnosis Safeโs Guard
interface, used to verify all the transactions originated from a wallet that involves rented assets.Stop
- contract used to reclaim rental assets and release the funds from the escrow contract.Accumulator
- contract used to handle the manipulation of dynamically allocated arrays of data structures directly in memory.Reclaimer
- contract used to transfer back rental assets to the proper recipient when the period has expired or the lender terminated the rent.Signer
- contract used to create type hashes and signature verification when creating rentals.The approach that reNFT
team has taken is unique, they are configuring permissions to policies based on functions that need to be called.
There are standard trusted roles assigned to real people:
**ADMIN** - role used to grant and revoke other roles. **SEAPORT** - singleton role granted to Seaport, which will make it possible to call **validateOrder** function in **Create** policy. **CREATE_SIGNER** - privileged EOA which will live on the ReNFT backend, owned by ReNFT. **ADMIN_ADMIN** - admin of the protocol, can skim funds, and manage critical params through **Admin** policy **GUARD_ADMIN** - role given to EOA, can update hook status and path. **EXECUTOR** - role used only for action execution in the **Kernel**
Stage | Action | Details | Information |
---|---|---|---|
1 | Compile and Run Test | Installation | Easy setup and commands provided for testing and deploying |
2 | Documentation review | Docs | Provides full flow for creating, fulfilling, and stopping rentals, as well as detailed info about whitelists and hooks. |
3 | Contest details | Audit Details | Thorough details for the contracts and the idea of the protocol were provided. Known issues and possible attack scenarios are helpful. |
4 | Diagramming | Excalidraw | Drawing diagrams through the entire process of codebase evaluation. |
5 | Test Suits | Tests | In this section, the scope and content of the tests of the project are analyzed. |
6 | Manual Code Review | Scope | Reviewed all the contracts in scope |
7 | Special focus on Areas of Concern | Areas of Concern | Observing the known issues and bot report |
All possible Actions and Flows in reNFT:
Rental process cannot be finalized without a valid safe gnosis wallet deployed by the renter.
This can be done through Factory
policy as it contains all the needed configurations for a safe deployment and transaction validation. Pre deployed Gnosis contracts, such as FallbackHandler
, SafeProxyFactory
and SafeSingleton
, are being used. Additionally, Guard
policy is set as guard, that is used to prevent malicious transactions with rental assets involved, such as transfers and approvals, Stop
policy is set as wallet module as it is used to reclaim the rented assets without routing the transaction through the Guardโs
checkTransaction
. TokenCallbackHandler
is added as a wallet fallback handler, to be able to handle rental assets as this adds callback functionality.
When a user wants to rent an NFT he interacts only with the backend of the protocol by choosing the token that he want to rent and the rent period. Then fulfillAdvancedOrder
in Seaport is called which does various validations and actions, one of them to transfer the token to the recipient specified in the ZoneParameters
which is passed to the reNFTโs Create
policy through the validateOrder
function. This function is the entry point for the reNFT
rental creation as it does all the needed actions to ensure information is saved and can be tracked properly in the system:
Check that the payload is not expired.
Validates if the intended fulfiller by the lender is the same as the actual fulfiller.
Recovers the RentPayload
signer, who is a reNFT person who is granted the CREATE_SIGNER
role.
Valid safe wallet is required (should be deployed through Factory
policy).
SpentItem
and ReceivedItem
, aka offer and consideration items from a fulfillerโs perspective are converted to protocol native struct: Item
. Then rental assets (ERC721, ERC1155) which are hashed with the help of Accumulator
(package used for in memory structure, holding unknown size of elements) added as rentals to the Storage
module. (This is valid only for Base and Pay order types. You can read more about different order types here).
ERC20 items are used to increase the internal balance of the PaymentEscrow
module.
PaymentEscrow
module is passed as a conduit recipient and receives all the token payments when rent is initiated.
If renter has specified hooks, mostly used for Pay order types, their onStart
function is being executed (execution can be disabled by an admin).
Event is emitted and order is saved off-chain.
Lastly, valid order magic value is returned in order Seaport to consider the execution successful.
Note about why seaport was forked:
reNFT
usesvalidateOrder
with modifiedZoneParameters
struct that containstotalExecutions
used to validate the recipient for both ERC721 and ERC20 token which has to be Gnosis Safe wallet of the renter andPaymentEscrow
module respectively. This information is missing in the original OpenSea code.
First we need to understand how gnosis safes generally works - their architecture is modular, which allows owners to specify various add-ons on top of a wallet.
Some of them are guards and modules:
Guard
- this is a special EIP165 compliant contract that is executed for all normal transactions, initiated from a wallet and does various checks and validations according to the needs of the owners.Module
- contract that adds additional logic to an existing wallet - in reNFT
Stop
policy is configured as a wallet module. Note that modules can execute transactions without validating them through the Guard
which is crucial for the seamless rental reclaiming.Transaction is initiated from a wallet and if itโs not originated from a module, Guardโs
checkTransaction
is executed. This function is used to prevent renters from executing transactions that involve approvals or transfers of rented assets.
On top of that, if renter has specified hooks on rental initiation process and they are activated by an admin, their onExecution
function is called giving an additional extendability to the rental process.
Note: delegate call functionality offered by Gnosis is disabled by default, unless explicitly enabled by an admin for specific transaction recipients.
Process of stopping a rent can be initiated by everyone, but there are caveats - in case orderType is Base order it will only succeed if rent period has passed, not matter who the caller is. For Pay order if lender
initiated the reclaim action, rent period is not taken into account, for non-lender users the rule above applies.
Then for rental items (ERC721 and ERC1155) same dynamic memory structure is created as the one in Rental initiation is created. In case there are hooks specified their onStop
function is being executed (execution can be disabled by an admin).
Transaction from the Gnosis safe module (Stop
) is executed. This is done to prevent the Guard::checkTransaction
from being called (thatโs how modules in gnosis safe work) and assets are successfully returned to the renter.
PaymentEscrow
settles the payments in either full or pro rata, depending if the rent period is passed and the order type:
renter
because of rounding protection) based on the elapsed time.renter
due to the nature of this orderType.lender
.Rental references are removed from the Storage
module which prevents from claiming the tokens twice.
Lastly, event is emitted to notify the off-chain that the rental order has stopped.
Architecture of the protocol is simple and robust, there is no big complexity for the main tasks that will be performed - renting and executing transactions. There are other parts of the code that are requiring more attention such as upgrades and dynamically allocated data structs kept in memory.
Contracts are split by context which makes the understanding easier.
The idea of hooks is essential as it will extend significantly the use case of the protocol.
Create2Deployer
is gas efficient due to the amount of assembly used, would be great if same effect is applied to Factory
policy, because it will be used quite often.
Important notes regarding the codebase and its sustainability:
Guard
policy does not prevent important functions from being executed such as burn
, it is important to note that all the rent request will be approved by reNFT signer, but handling such function will definitely prevent from malicious actions.hashes
are constructed wrong, which will DoS the rentals.We consider the codebase of theย reNFT
ย simple and versatile, hooks greatly fit the main idea of the team but also does not limit the working capabilities of the protocol as not being mandatory and having the go-to checkTranscation
function giving the same or even bigger sense of security. Safe wallets deployment is utilised to automatically deploy one on behalf of renter. The rent stop process is not as good as the other parts of the code, as it heavily depends on the hooksโ (if order uses any) onStop
not being disabled, otherwise NFTs will be stuck in rental safes.
Tests have high abstraction which is a double-edged sword as they require more time to be completely understood, but due to Seaport integration this canโt be mitigated.
Codebase Quality Categories | Comments | Score (1-10) |
---|---|---|
Code Maintainability and Reliability | The codebase demonstrates moderate maintainability with well-structured functions and shared modularity across various contracts constructing all the calls in the system. Well-designed system for the main purpose and extendability for handling various NFT use cases by using Hooks. Packages are used to differentiate logic, lowering the code complexity and reducing the time needed to understand the reNFT codebase. | 8 |
Code Comments | All the contracts in scope have comments explaining the purpose and functionality of their functions, making it easier for developers and auditors to understand and maintain the code. Comments describe methods, variables, and the overall structure of the contract. Contracts have good amounts of comments that make understanding an easy process. | 9 |
Documentation | Mostly non-technical documentation is provided on the reNFT , but the team did a great job by providing an overview on the contest page explaining the idea, invariants, attack scenarios, and the main actions that various users will perform in the system. Seaport part was a bit confusing, reNFT team can push themselves further and create documentation about the functions used and what needed to change to be able to handle reNFT as a zone. | 8 |
Code Structure and Formatting | Most of the contracts are constructed from 1 main function which utilises all the others as this approach makes it easier to trace the flow and understand the system, default formatting is used which splits the arguments on a new line. There arenโt big and chunky code lines and in our opinion placing all the arguments on one line will result in better looking code. | 6 |
The audit scope of the contracts to be audited is 80% most of them cover the whole flow of the actions that are available in the system without testing in isolation.
However, development team did a great job by not creating mocks for most of the contracts but fixtures with the real logic of the contracts were used.
This approach deviates a little from the initial idea of unit testing - to test only small modules, but gives context what happens in the both protocols that are integrated in the system - GnosisSafe and Seaport.
Thing to consider about the tests is how the hashes in fixtures are constructed. Same function is used to hash both structs and this gives false sense of security and most of our reported vulnerabilities are from these parts of the code. Currently their representation is something like this:
_deriveOrderMetadataHash(arg) == _deriveOrderMetadataHash(arg)
, which will always return true.
reNFT
utilises contract based authorization which gives the policy contracts
(contracts where users interact with the system) certain function selectors from the module contracts
that can be called from these contracts. This approach lowers the centralization risk of the protocol.
There are other roles as well, such as EXECUTOR
and ADMIN
that will be given to EOAs and they poses serious centralisation risk, especially the EXECUTOR
.
If it turns out to be given to a malicious user, this can lead to catastrophic consequences as he can change the Kernel
, Policies
and Modules
implementation which will directly affect both lenders and renters.
Severity | Findings |
---|---|
High | 3 |
Medium | 2 |
Low/NC | 8 |
Findings were identified in the Signer
and Guard
contracts.
They cause significant impact because:
Signer
policy malfunction which leads to wrongly hashed structs.Burn
function selector is not handled and can result in 100%+ loss for lender and in some cases profit for renter who burned because NFT refunds the floor price.Learned about:
In general, the reNFT
project exhibits an standard and well-developed architecture we believe the team has done a good job regarding the code, but the identified risks need to be addressed, and measures should be implemented to protect the protocol from potential malicious use cases. Additionally, it is recommended to improve the documentation as there is a need of proper understanding of the underlying architecture interacting with the integrated protocols. It is also highly recommended that the team continues to invest in security measures such as mitigation reviews, audits, and bug bounty programs to maintain the security and reliability of the project.
55 hours
#0 - c4-judge
2024-01-27T20:23:48Z
0xean marked the issue as grade-b
#1 - Slavchew
2024-01-30T08:28:56Z
Hi, @0xean
Congrats on the quick judgment.
I want a revisit of this because to me, based on others that are marked as grade-a
, this is also grade-a
for sure.
Other analyses that are marked as grade-a
, but are worse than ours.
#2 - 0xean
2024-01-30T15:29:09Z
@141345 please comment on grading here.
#3 - 141345
2024-01-30T15:45:38Z
I want a revisit of this because to me, based on others that are marked as
grade-a
, this is alsograde-a
for sure.
- It covers the whole project structure.
- There are self-made diagrams of the main flows in the protocol.
- It gives appropriate recommendations for different parts of the codebase.
- Mention where the main issues are (do not explain them in it because this is an analysis, not an issue report).
Other analyses that are marked as
grade-a
, but are worse than ours.
- Analyses that only explain issues and do not cover the analysis points and structure.
- Analyses that explain file by file, without structuring anything, no flow path, images, etc.
my question is, do the following add any value to the sponser?
With regard to the analysis part, it is not as detailed as 380, 420, that's why I think this is a good analysis report, but not best
#4 - Slavchew
2024-01-30T16:03:06Z
I'm not saying it's the best, but it's certainly better than others that are grade-a
.
For example: #223, #386 - only provide auto-generated contract flow, deployment gas costs, etc.
Will take the note to give more security value to the sponsor rather than just explaining.
Thanks.
#5 - 141345
2024-01-31T13:21:52Z
I'm not saying it's the best, but it's certainly better than others that are
grade-a
.For example: #223, #386 - only provide auto-generated contract flow, deployment gas costs, etc.
Will take the note to give more security value to the sponsor rather than just explaining.
Thanks.
I'm not giving diagrams significant weights. Because nice graphs do not add value to sponsors in my opinion. 223 and 386 both give some suggestions and codebase/test feedback, which could be helpful to improve the protocol from different level.
This is analysis report, not summarize/drawing.