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: 17/116
Findings: 6
Award: $671.17
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: sin1st3r__
Also found by: 0xA5DF, J4X, JCN, Jorgect, KupiaSec, evmboi32, jasonxiale, kaden, said
67.1301 USDC - $67.13
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.
Consider the following scenario:
Either:
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
🌟 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
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.
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:
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
🌟 Selected for report: stackachu
Also found by: 0xA5DF, 0xDING99YA, 0xc695, CipherSleuths, EV_om, HSP, cccz, evmboi32, hals, hash, jasonxiale, juancito, kaden, lanrebayode77, rbserver
22.2973 USDC - $22.30
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.
Consider the following scenario:
onERC721Received()
function on the safe, so that any call to it would revertIn case that the attempt to transfer reverts - create a safe with the lender as the sole owner and transfer the tokens to there.
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)
131.5503 USDC - $131.55
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
🌟 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
175.5496 USDC - $175.55
Factory.deployRentalSafe()
can be front-run to revert safe creationConsider the following scenario:
Factory.deployRentalSafe()
SafeProxyFactory.createProxyWithNonce()
directly with the same parametersThe 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.
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.
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.
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.
Consider the following scenario:
Mitigation - either make a separate allowlist for disabling, or allow disabling all modules but the Stop module
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.
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.
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.
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).
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)
Accumulator._insert()
might result in memory collision in future codeThis 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.
// Update the free memory pointer so that memory is safe // once we stop doing dynamic memory array inserts mstore(0x40, add(newItemPosition, 0x40))
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.
// Require that the `hook` address is a contract. if (hook.code.length == 0) revert Errors.StorageModule_NotContract(hook);
#0 - 141345
2024-01-22T13:55:08Z
330 0xA5DF l r nc 4 2 0
L 1 d dup of https://github.com/code-423n4/2024-01-renft-findings/issues/443 L 2 l L 3 d dup of https://github.com/code-423n4/2024-01-renft-findings/issues/111 L 4 d dup of https://github.com/code-423n4/2024-01-renft-findings/issues/396 L 5 d dup of https://github.com/code-423n4/2024-01-renft-findings/issues/238 L 6 d dup of https://github.com/code-423n4/2024-01-renft-findings/issues/409 L 7 l L 8 l L 9 l L 10 r L 11 r
#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
🌟 Selected for report: fouzantanveer
Also found by: 0xA5DF, 0xepley, SAQ, SBSecurity, albahaca, catellatech, clara, hals, hunter_w3b, kaveyjoe, ladboy233, pavankv, peanuts, tala7985, yongskiws
269.8643 USDC - $269.86
Trusted roles:
Name | Description | Risk |
---|---|---|
SEAPORT | The address of Seaport | As 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_SIGNER | An EOA that signs the orders | Doesn’t seem to hold much power on its own |
ADMIN_ADMIN | Holds the power to upgrade contracts, set the fee and take protocol fees | Can 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_ADMIN | Holds the power to add or remove hooks from the Guard | Adding a malicious hook can help renters to steal the rented tokens, since the hook checks the tx rather than the regular tx check. |
Kernel admin | Holds 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 executor | Holds 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)
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.
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.
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.
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.
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:
Description | Risk |
---|---|
Addresses whitelisted for a delegate call from the safe | During a delegate call we can execute anything, effectively bypass the guard. |
Enabled modules | Module txs aren’t checked by the guard, meaning a bug in a module can allow us to bypass the guard. |
On transaction hooks | Whan 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 hooks | Can prevent stopping a rental, would effectively lock the rented assets and payment. |
Start hooks | Can prevent starting rentals |
Note that bypassing the guard means you can easily steal the rented items, or even remove the guard completely.
Name | Integration | Notes |
---|---|---|
OpenZeppelin | ERC20/721/1155 interfaces | Just interfaces |
OpenZeppelin ECDSA | Recovering singer address at Signer.sol | |
GnosisSafe | Guard implementation | Requires a good understanding of how the safe and guard works. |
Checking the owner of a safe | Seems rather simple | |
Deploying a safe | ||
Seaport | _validateOrder() implementatoin | There 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. |
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.
Those don't contain any logic, so the chance of them having bugs is really low.
validateOrder()
functionvalidateOrder()
should ensure:
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.
We can try to bypass the guard by approving the tokens before the rental begins, however this doesn't seem to work:
/// When a Transfer event emits, this also indicates that the approved address for that NFT (if any) is reset to none.
approveForAll()
isn't allowed at all timesTechnically 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.
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.
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.
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.
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.
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.
Safes created by the protocol can be used to store other tokens. However the safe would still be restricted by the guard:
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.
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.
25 hours
#0 - c4-judge
2024-01-27T20:26:49Z
0xean marked the issue as grade-a