reNFT - 0xA5DF'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: 17/116

Findings: 6

Award: $671.17

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: sin1st3r__

Also found by: 0xA5DF, J4X, JCN, Jorgect, KupiaSec, evmboi32, jasonxiale, kaden, said

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-600

Awards

67.1301 USDC - $67.13

External Links

Lines of code

https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/policies/Create.sol#L647-L657

Vulnerability details

Impact

When rental is active no tokens of the type (same address and same token ID) of the rented tokens can be transferred. This means that an owner of a safe can use a rental to lock in non-rented tokens that exists in the safe.

Proof of Concept

Consider the following scenario:

  • A group of friends are using the safe for renting an ERC721 token
  • They also store another ERC1155 token in the safe
  • One of the owners isn’t happy with what the group intends to do with the ERC1155 tokens so they create an order of 1 wei for 1000 years ahead of the same ERC1155 token
  • Since the attacker is one of the owners the order goes through
  • Now all of the ERC1155 in the contract is stuck forever

Either:

  • Allow withdrawing tokens from safe as long as the remaining balance is greater than the amount of locked tokens
  • Require that the safe would initiate the order fulfillment, this way we can ensure the rent is with agreement of all owners

Assessed type

Invalid Validation

#0 - c4-pre-sort

2024-01-21T18:00:23Z

141345 marked the issue as duplicate of #600

#1 - c4-judge

2024-01-27T18:09:33Z

0xean changed the severity to 3 (High Risk)

#2 - c4-judge

2024-01-28T11:19:38Z

0xean changed the severity to 2 (Med Risk)

#3 - c4-judge

2024-01-28T11:21:39Z

0xean marked the issue as satisfactory

#4 - c4-judge

2024-01-28T11:21:49Z

0xean marked the issue as partial-75

#5 - c4-judge

2024-01-28T19:29:39Z

0xean marked the issue as satisfactory

Awards

4.7844 USDC - $4.78

Labels

bug
2 (Med Risk)
satisfactory
duplicate-501

External Links

Lines of code

https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/policies/Stop.sol#L210-L212

Vulnerability details

Impact

Rental stop reverts if one of the specified hooks is disabled. The hooks are specified during rental creation, so if the admins disable the hooks in the meanwhile the rental wouldn't be stopped. This would deprive the lender/renter from payment and the lender from getting back their rented tokens.

Another scenario where this is possible, is if a hook is enabled only for rental-start, in that case the rental can start but can't be stopped.

Proof of Concept

During rental stop the hooks are checked, if the hook is disabled the function would revert.

            if (!STORE.hookOnStop(target)) {
                revert Errors.Shared_DisabledHook(target);
            }

Hooks can be disabled by the admins at any time: code:

    /**
     * @notice Updates a hook with a bitmap that indicates its active functionality.
     *         A valid bitmap is any decimal value that is less than or equal
     *         to 7 (0x111).
     *
     * @param hook   Address of the hook contract.
     * @param bitmap Decimal value that defines the active functionality on the hook.
     */
    function updateHookStatus(
        address hook,
        uint8 bitmap
    ) external onlyByProxy permissioned {

Hooks list is shared between rental start and stop, so when a lender specifies hooks they'd be used for both start and stop. code

            // Generate the rental order.
            RentalOrder memory order = RentalOrder({
                seaportOrderHash: seaportPayload.orderHash,
                items: items,
                hooks: payload.metadata.hooks,
                orderType: payload.metadata.orderType,
                lender: seaportPayload.offerer,
                renter: payload.intendedFulfiller,
                rentalWallet: payload.fulfillment.recipient,
                startTimestamp: block.timestamp,
                endTimestamp: block.timestamp + payload.metadata.rentDuration
            });

Either:

  • Skip disabled hooks (don't call them but don't revert)
  • Skip the check and run the hooks anyways, because during creation they were allowed (don't keep a separate status for start and stop hooks in that case)

Assessed type

Invalid Validation

#0 - c4-pre-sort

2024-01-21T17:58:36Z

141345 marked the issue as duplicate of #501

#1 - c4-judge

2024-01-28T19:36:27Z

0xean marked the issue as satisfactory

Findings Information

Awards

22.2973 USDC - $22.30

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-65

External Links

Lines of code

https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/packages/Reclaimer.sol#L27-L50

Vulnerability details

Impact

The lender can revert the rental stop by reverting the onXReceived() function calls. This would deprive the renter from their payment (in case of a PAY order). Additional effect might be if the rental includes ERC1155 tokens and the renter holds some identical non-rented ERC1155 in the safe - preventing the rental stop would prevent the transfer of those non-rented tokens.

Proof of Concept

Consider the following scenario:

  • Bob creates a PAY order via a safe they own (i.e. the lender is actually a safe, controlled by Bob)
  • Alice fulfills the order
  • During the rental the value of the NFT has dropped to nearly zero, so Bob doesn't care about it anymore
  • Before the rental ends Bob disables the onERC721Received() function on the safe, so that any call to it would revert
  • Now any attempt to stop the rental would revert. Bob can deprive Alice from the rental payment as long as he'd like.

In case that the attempt to transfer reverts - create a safe with the lender as the sole owner and transfer the tokens to there.

Assessed type

Error

#0 - c4-pre-sort

2024-01-21T18:02:17Z

141345 marked the issue as duplicate of #65

#1 - c4-judge

2024-01-28T19:26:00Z

0xean marked the issue as satisfactory

#2 - c4-judge

2024-01-28T20:51:59Z

0xean changed the severity to 3 (High Risk)

#3 - c4-judge

2024-01-30T14:21:44Z

0xean changed the severity to 2 (Med Risk)

Findings Information

🌟 Selected for report: 0xAlix2

Also found by: 0xA5DF, AkshaySrivastav, J4X, ZdravkoHr, fnanni, oakcobalt

Labels

2 (Med Risk)
satisfactory
duplicate-43

Awards

131.5503 USDC - $131.55

External Links

Judge has assessed an item in Issue #330 as 2 risk. The relevant finding follows:

L5

#0 - c4-judge

2024-02-01T10:18:46Z

0xean marked the issue as duplicate of #43

#1 - c4-judge

2024-02-01T10:18:50Z

0xean marked the issue as satisfactory

Awards

175.5496 USDC - $175.55

Labels

bug
grade-a
high quality report
QA (Quality Assurance)
selected for report
sponsor confirmed
Q-13

External Links

L1 - Factory.deployRentalSafe() can be front-run to revert safe creation

Consider the following scenario:

  • A user sends out a tx to call Factory.deployRentalSafe()
  • The attacker sees that and front runs it, calling SafeProxyFactory.createProxyWithNonce() directly with the same parameters
  • The user's tx would revert because the safe already exists

The user can mitigate that by first creating a safe with a different set or order of owners, or a different threshold, and then creating the safe they wished to create (the attacker can still keep front-running those txs though).

A way to mitigate that (prevent front running) is to deploy a fork of SafeProxyFactory with access control - allowing only the protocol's Factory to create new safes.

L2 - initializer can be front run

Both modules are using a proxy with initializers, those initializers can be front run during deployment. As mitigation - ensure that initialization runs at the same tx with deployment.

L3 - Renter can steal CryptoKitties token using transfer()

CryptoKitties uses a non-standard ERC721 interface, this means that in case it's rented it can be stolen by the renter since the Guard can't stop it. The collection is pretty popular, and it can be traded on Seaport using Conduit. This might be classified as a known issue, but given that CryptoKitties is commonly - it's worth noting that.

L4 - On transaction hooks aren’t controlled by the lender

Contrary to the docs which state that the hooks are controlled by the lender - in reality all active on-transaction hooks run regardless of what the lender has specified in the order.

L5 - impossible to disable modules that has been removed from whitelist

Consider the following scenario:

  • Module has been whitelisted
  • Some safes add it
  • Module has been removed from whitelist
  • Safes can’t disable this module now

Mitigation - either make a separate allowlist for disabling, or allow disabling all modules but the Stop module

L6 - limit the rental to a reasonable time, to prevent creating for an unreasonable time by mistake

Currently it seems that the protocol allows rental for thousands of years, that doesn’t make much sense and if a user specifies that by mistake that might lock their tokens forever.

L7 - if stop module is whitelisted it can be disabled by the safe

If stop module is whitelisted by mistake - renters can just disable it and prevent rental stop. As mitigation, consider adding a check during whitelisting to prevent whitelisting it.

L8 - Can’t update the status of a self destructed hook

The protocol doesn't allow updating the status of hooks with no code. The issue is that if a hook is self destructed for any reason the admin can't update its status any more.

code

        if (hook.code.length == 0) revert Errors.StorageModule_NotContract(hook);

As mitigation - if the hook doesn't have any code allow only disabling it (setting the status to zero).

L9 - if stop policy is replaced it’d brick the rental stop for older safes

The stop policy can be replaced via the kernel, if the old stop policy is disabled it'd be impossible to stop the rentals - since only the old stop policy has access to the safe.

As mitigation - either keep the old stop policy active, or add some migration mechanism (allowing the old stop policy to add the new stop policy to the safe as a module. This has to be done before deploying the current stop policy)

R1 - Accumulator._insert() might result in memory collision in future code

This function expands the size of the rentalAssets memory variable, assuming there's no memory that's currently used afterwards. This is true under current code, but worth keeping an eye on it for future code changes or upgrades - if there's another memory variable after rentalAssets when this function is called it'd override it, causing severe issues.

Code

            // Update the free memory pointer so that memory is safe
            // once we stop doing dynamic memory array inserts
            mstore(0x40, add(newItemPosition, 0x40))

R2 - impossible to remove a hook from a specific contract

While it’s possible to disable a hook, that would disable it for all contracts that are using it. If we want to enable it on one target and remove it from another target that’s impossible, because we can’t set it back to the zero address (since the zero address doesn’t have any code). As mitigation, allow setting it to the zero address.

code

        // Require that the `hook` address is a contract.
        if (hook.code.length == 0) revert Errors.StorageModule_NotContract(hook);

#1 - c4-sponsor

2024-01-24T21:39:40Z

Alec1017 (sponsor) confirmed

#2 - Alec1017

2024-01-24T21:39:43Z

L1- Acknowledged L2- Acknowledged L3- Disputed, considered out of scope because it doesnt adhere to standard ERC721 spec L4- Disputed, this is expected behavior L5- Confirmed L6- Confirmed L7- Disputed, stop module will not be whitelisted L8- Acknowledged, hook implementations considered out of scope L9- Disputed, policies can only be enabled or disabled. in this instance, a second stop policy would be created for users to opt-in to

R1- Acknowleged R2- Disputed, this is intended behavior. To disable the hook, updateHookStatus would be used here instead. the bitmap would be set to 0. It does not matter what the path is set to if the status is 0

#3 - c4-judge

2024-01-27T20:28:55Z

0xean marked the issue as grade-a

#4 - c4-judge

2024-01-27T20:30:20Z

0xean marked the issue as selected for report

#5 - 0xean

2024-01-29T22:22:42Z

L3- OOS L7 - invalid R2 - invalid

others lgtm.

#6 - 0xA5DF

2024-01-30T08:25:18Z

L1 is dupe of #443 L5 is dupe of #43

#7 - 0xean

2024-02-01T10:19:05Z

#443 is QA, upgraded L5 / #43

Findings Information

Awards

269.8643 USDC - $269.86

Labels

analysis-advanced
grade-a
sufficient quality report
A-13

External Links

Centralization risks

Trusted roles:

NameDescriptionRisk
SEAPORTThe address of SeaportAs long as the address is the Seaport contract the only risk is if Seaport has any bugs. However, if the admin of this role assigns this role to malicious address the attacker can exploit this to create fake orders (even without the signer, you can re-validate an order to lock funds in the safe)
CREATE_SIGNERAn EOA that signs the ordersDoesn’t seem to hold much power on its own
ADMIN_ADMINHolds the power to upgrade contracts, set the fee and take protocol feesCan easily steal funds from the protocol, e.g. - upgrade the escrow to a malicious contract and steal all the balance there (as long as it wasn’t frozen), whitelist a malicious extension that would help renters steal the rented tokens
GUARD_ADMINHolds the power to add or remove hooks from the GuardAdding a malicious hook can help renters to steal the rented tokens, since the hook checks the tx rather than the regular tx check.
Kernel adminHolds the power to grant or revoke all roles (except for kernel admin and executor)Basically holds the power to all the roles. Therefore it bears the risk of all those roles combined.
Kernel executorHolds the power to replace the modules. This would update the modules in the policies as well.This role holds a very high power over the system, if the role holder becomes malicious they can easily steal funds from the protocol. For example - they can add a new policy that has access to all modules and through that policy steal funds from escrow and mess up the rentals (locking funds in the safe or allowing renters to steal the rented items)

Overall it seems that most roles here hold a very high amount of power over the system and compromising them would cause loss of funds for the protocol and the users. Therefore, all of them must be behind a timelock to allow users to withdraw their funds in case one of the roles gets compromised (with an exception of CREATE_SIGNER and escrow.skim(). The latter is protocol fees and won’t harm the users). On top of that, given that the tokens are locked in the contracts till the end of the rental a timelock on its own wouldn’t help for orders which are longer than the timelock. I’d suggest adding an escape feature, where with an agreement of both the lender and the renter the rent can end and the tokens can be extracted safely from the safe and escrow. There'd still be a risk of the admin collaborating with the renter or lender, but that's a much smaller risk. (an alternative might be to require the rentals to be shorter than the timelock delay, but that doesn’t seem very reasonable)

Limiting the fee level

On top of that, it'd make sense to set a max fee, to limit the amount of fee that the owner can set, esp. considering that under current situation the fee applies retroactively.

Pausing mechanism

Currently there's no pausing mechanism in the protocol. However, all orders have to be signed via the CREATE_SIGNER role, that makes it possible for the admins to pause the creation of new orders by not signing orders. Given that the intention of a pausing mechanism is to stop new activity (but allowing the completion of existing activity) this might be sufficient. The rest of the actions in the system (safe tx during rental, stopping rentals) are for rents that already begun and it wouldn't make sense to give the pauser the power to pause them.

It's worth mentioning though that given that the CERATE_SIGNER needs to constantly sign orders it would have to be a warm wallet, which put it at a higher risk of being compromised. Therefore it might be worth to add additional pause mechanism, or make the admin of the CERATE_SIGNER role an EOA or multisig without a timelock mechanism, this way the CERATE_SIGNER role can be revoked immediately if it has been compromised.

Default framework

Access control

Access control to the modules is managed by the kernel. I've verified that all external/public functions in the modules have the permissioned modifier which would ensure that it can only be accessed with a policy with the right permissions.

Dual upgrade mechanism

Besides the framework's feature to update a module we can also upgrade the current modules implementation via the Admin policy. This makes sense, since an upgrade via the kernel would mean changing to a different address which wouldn't preserve the storage of the previous address. However, it's worth keeping an eye on the fact that we have another way to upgrade the modules which might be at the hands of a different role.

Integration with contracts not in scope

Future code

The following types contracts are to be added to the protocol in the future, besides failing to do what they're supposed to do they bare some additional risks:

DescriptionRisk
Addresses whitelisted for a delegate call from the safeDuring a delegate call we can execute anything, effectively bypass the guard.
Enabled modulesModule txs aren’t checked by the guard, meaning a bug in a module can allow us to bypass the guard.
On transaction hooksWhan a hook is active the guard uses it to check the tx, a bug in a hook might allow bypassing the guard to execute any tx.
Stop hooksCan prevent stopping a rental, would effectively lock the rented assets and payment.
Start hooksCan prevent starting rentals

Note that bypassing the guard means you can easily steal the rented items, or even remove the guard completely.

External libraries

NameIntegrationNotes
OpenZeppelinERC20/721/1155 interfacesJust interfaces
OpenZeppelin ECDSARecovering singer address at Signer.sol
GnosisSafeGuard implementationRequires a good understanding of how the safe and guard works.
Checking the owner of a safeSeems rather simple
Deploying a safe
Seaport_validateOrder() implementatoinThere are many parameters sent from Seaport, which we process during validation. Having a wrong assumption about one of them might open the door for a bug.

Other contracts excluded from scope

Contracts with little code

The following contracts have been excluded from scope:

  • src/packages/Zone.sol
  • src/libraries/KernelUtils.sol
  • src/libraries/RentalUtils.sol
  • src/proxy/Proxiable.sol
  • src/proxy/Proxy.sol

Most of them contain rather simple logic so the chance of having bugs in them is pretty low, still it's worth auditing those since the chance of them having bugs isn't zero.

Interfaces, error, events and structs

Those don't contain any logic, so the chance of them having bugs is really low.

Critical parts of the protocol

Rental creation and order validation

  • Ensure only Seaport can call the validateOrder() function
  • validateOrder() should ensure:
    • Items are sent to the right addresses
    • The CREATE_SIGNER signed the rent payload
    • Rental is registered to storage

Guarding against rented token escape

The most difficult part here is preventing the escape of a rented token from a safe. There might be many different ways to transfer a token and it might be difficult to try to block them all.

Pre approving a token

We can try to bypass the guard by approving the tokens before the rental begins, however this doesn't seem to work:

  • As for ERC721 all specific approves are deleted during transfer
    • This is according to ERC721 specs:
      /// When a Transfer event emits, this also indicates that the approved address for that NFT (if any) is reset to none.
  • ERC1155 doesn't have a function to approve a specific token
  • approveForAll() isn't allowed at all times
Approving for a previous 'life' of the safe

Technically we can deploy a safe, approve tokens, destroy it and then create another safe. However it doesn't seem to work in this case, since in order to deploy it to the same address we have to deploy it with the same parameters, which means it'd be with the same guard that would stop approving and destroying the safe.

Running before the guard is set

The guard is set right when the safe is created, there's no wiggle for an attacker to run any function before it's set.

Weird ERC721/ERC1155 tokens

This is where the biggest risk lays, tokens often take their freedom in adding new features to their own implementation. Those cool 'features' might be used to bypass the guard. The sponsor listed this as a known issue, but it's worth noting that these might be tokens which are commonly used (e.g. crypto kitties), and the average users wouldn't expect them to cause issues when using them in the protocol.

  • Alternative functions to transfer tokens
    • E.g. another parameter to add some metadata / additional instructions
  • Alternative functions to approve
    • E.g. ERC1155 function to approve a specific token
  • Different behavior for existing functions
    • E.g. not erasing the approval of ERC721 on transfer

Rental stop

  • Ensuring the rental exists and has ended
    • Deleting the rental from storage / Guarding against double rental stop
  • Transferring payment from escrow to lender (or renter for PAY order)
  • Transferring ERC721/1155 tokens back to the lender

Reentrancy

onXReceived

This seems the obvious way to achieve reentrancy here. During rental start the tokens are transferred to the safe, the onXReceived functions in the fallback handler of the safe simply return and do nothing.

During rental stop the tokens are transferred back to the lender and this is where we can achieve reentrancy.

  • Attempting to reenter and stop the same order again would probably revert during the check in Storage
  • Attempting to reenter and stop another rental would probably not have any effect on current rental or vice versa
    • The 2 interaction that weren't done yet during the reentrancy are the settle payments and marking the rental as removed in storage. Both don't seem to be affected by another rental being stopped before they run (or them affecting the other order).
  • Attempting to create another order during that time
    • It wouldn't have the same order hash, as the hash is based on the rental start timestamp as well

Despite that, it might be good not to put a reentrancy guard, or at least to move the _reclaimRentedItems() interaction to the bottom of the function.

During safe tx

The safe's Guard is pretty stateless - all txs are either allowed or prohibited, therefore it doesn't seem that a reentrancy can cause any issues with current code. As for future code - hooks, extensions and delegate calls - it's worth taking that into account when auditing the code.

Effects of the protocol over other tokens in the safe

Safes created by the protocol can be used to store other tokens. However the safe would still be restricted by the guard:

  • Delegate calls are restricted to allowlist
  • Approve for all is not allowed
  • Modules are restricted to allowlist
  • Non-rented ERC1155 tokens can't be transferred out of the safe if there's a similar token (same address, same token ID) that's rented.
  • The protocol's modules can transfer tokens out of the safe without the owner's approval. It's important to communicate that to the user, esp. the first effect.

Order hash uniqueness

The orderHash used in the protocol is a hash of the RentalOrder struct which contains Seaport's orderHash. This means that as long as Seaport's order hash is unique then the protocol's order hash would be unique as well (I didn't dive deep enough into Seaport to check that, but it makes sense that this is the case). Still, it might be worth for additional security to verify during registration that the order hash wasn't registered yet.

Push0 opcode on L2s

Some L2s might not support the push0 opcode, given that the compiler version is greater than 0.8.20 then it might cause issues if compiled to the Shanghai evm. I noticed the foundry.toml targets the paris evm for that reason, so as long as it's compiled for that evm this shouldn't be an issue.

Time spent:

25 hours

#0 - c4-judge

2024-01-27T20:26:49Z

0xean marked the issue as grade-a

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