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: 92/116
Findings: 1
Award: $11.20
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 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
11.2034 USDC - $11.20
The Guard.sol
contract is enabled on Safe's and uses the _checkTransaction
function to ensure that transactions that the Safe executes do not transfer the asset out of the Safe.
The checkTransaction
function achieves this by isolating the function selector and checking that it is not a disallowed function selector. For instance: safeTransferFrom
, transferFrom
, approve
, enableModule
, etc.
The list does not, however, check for calls to burn
the token, neither does it check if it is a permit
. The sponsor has noted the following:
The Guard contract can only protect against the transfer of tokens that faithfully implement the ERC721/ERC1155 spec
But this does not acknowledge the fact that an ERC721/ERC1155 implementation can still be an honest implementation and have extra functionality. In particular, the burn
function is a common addition to many ERC721 contracts, usually granted through inheriting ERC721Burnable
.
For example, the following projects all have a burn
function, and Safe's protected by Guard.sol
that hold these NFTs will be vulnerable to loss of assets via a malicious renter:
These are three that are in the top 10 projects on Opensea at the time of writing.
We can see in the Guard.sol
file that certain function selectors are imported to be tested against:
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, 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";
From the _checkTransaction
function we see that there is no check for burn
, burnFrom
or permit
.
A malicious renter who is renting the asset can still execute burn
(common), burnFrom
(rare) or permit
(popularized by Uni v3), which will lead to loss of the asset.
The below test can be placed in the CheckTransaction.t.sol
test file. It should be run with forge test --match-test test_PoC -vvvv
function test_PoC() public { bytes4 burn_selector = 0x42966c68; // 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); // Build up the `transferFrom(address from, address to, uint256 tokenId)` calldata bytes memory burnCalldata = abi.encodeWithSelector( burn_selector, 69 ); // Expect revert because of an unauthorized function selector _checkTransactionRevertUnauthorizedSelector( address(alice.safe), address(erc721s[0]), burn_selector, burnCalldata ); }
The console output is:
Encountered 1 failing test in test/unit/Guard/CheckTransaction.t.sol:Guard_CheckTransaction_Unit_Test [FAIL. Reason: call did not revert as expected] test_PoC() (gas: 96093)
This shows that the checkTransaction
would not protect against calls to burn
the asset.
Manual review
Although not a catch-all, adding checks for burn
, burnFrom
and permit
functions (which are common in smart contracts) should prevent this in most cases.
Selectors:
burn
: 0x42966c68
burnFrom
: 0x1fe41211
permit
: 0xabae8f0d
In the Guard.sol
file:
} else if (selector == e1155_safe_transfer_from_selector) { // Load the token ID from calldata. uint256 tokenId = uint256( _loadValueFromCalldata(data, e1155_safe_transfer_from_token_id_offset) ); // Check if the selector is allowed. _revertSelectorOnActiveRental(selector, from, to, tokenId); + } else if (selector == burn_selector) { + // Load the extension address from calldata. + address extension = address( + uint160( + uint256( + _loadValueFromCalldata(data, burn_selector_offset) + ) + ) + ); + + _revertSelectorOnActiveRental(selector, from, to, tokenId); + } else if (selector == burn_From_selector) { + // Load the extension address from calldata. + address extension = address( + uint160( + uint256( + _loadValueFromCalldata(data, burn_From_selector_offset) + ) + ) + ); + + _revertSelectorOnActiveRental(selector, from, to, tokenId); + } else if (selector == permit_selector) { + // Load the extension address from calldata. + address extension = address( + uint160( + uint256( + _loadValueFromCalldata(data, permit_selector_offset) + ) + ) + ); + + _revertSelectorOnActiveRental(selector, from, to, tokenId); + }
Please note that there may be other flavours of the permit
function that have different signatures.
Other
#0 - c4-pre-sort
2024-01-21T17:38:10Z
141345 marked the issue as primary issue
#1 - c4-pre-sort
2024-01-21T17:38:14Z
141345 marked the issue as sufficient quality report
#2 - 141345
2024-01-21T18:49:31Z
(https://github.com/code-423n4/2024-01-renft-findings/issues/587) has a detailed discussion about permit()
.
#3 - c4-sponsor
2024-01-24T21:24:08Z
Alec1017 (sponsor) acknowledged
#4 - Alec1017
2024-01-24T21:24:14Z
Non-standard ERC implementations were considered out of scope, but regardless we do plan on adding a whitelist for tokens interacting with the protocol
#5 - 0xean
2024-01-26T22:24:25Z
originally commented on https://github.com/code-423n4/2024-01-renft-findings/issues/587#issuecomment-1912630065
but same thing applies here, I don't think this can be called out of scope but do think M is more appropiate.
#6 - c4-judge
2024-01-26T22:24:35Z
0xean changed the severity to 2 (Med Risk)
#7 - c4-judge
2024-01-26T22:25:04Z
0xean marked the issue as satisfactory
#8 - c4-judge
2024-01-29T10:27:33Z
0xean marked the issue as selected for report
#9 - Alec1017
2024-01-29T19:57:12Z
Given the justification for #587, M severity seems fair
#10 - lokithe5th
2024-01-30T05:44:12Z
@0xean thank you for your judging efforts.
As the author of this submission I would like to highlight a mistake in my report: the Safe itself will not be calling the permit()
function, only signing a transaction that approves a permit
. Adding a check for the permit
selector will not be effective.
I draw attention to this fact because I do not want the sponsor to believe they have guarded against the permit
vulnerability by implementing the suggested fix, while they may still in fact be vulnerable to it.
However, the PoC and the suggested fix remain valid for burn
and burnFrom
functions.
Credit to issue #587 and @stalinMacias for pointing this nuance out.
#11 - stalinMacias
2024-01-30T06:16:42Z
Thanks @lokithe5th.
I believe this issue is completely valid in regards to the burn
functionality and is providing the right mitigation for it.