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: 85/116
Findings: 2
Award: $14.38
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
There are several instances in the codebase where the protocol implementation isn't eip712
compliant.
In this report, I will be tagging all of the observed quirks:
eip712
, bytes and string should be the keccack256 of its content.
As seen hereThe dynamic values bytes and string are encoded as a keccak256 hash of their contents.
So, the Signer::_deriveHookHash
function, defined here
function _deriveHookHash(Hook memory hook) internal view returns (bytes32) { // Derive and return the hook as specified by EIP-712. return keccak256( abi.encode(_HOOK_TYPEHASH, hook.target, hook.itemIndex, hook.extraData) ); }
Should be changed to:
function _deriveHookHash(Hook memory hook) internal view returns (bytes32) { // Derive and return the hook as specified by EIP-712. return keccak256( abi.encode(_HOOK_TYPEHASH, hook.target, hook.itemIndex, keccak256(hook.extraData)) //<@ ); }
_ORDER_METADATA_TYPEHASH
This:
function _deriveOrderMetadataHash( OrderMetadata memory metadata ) internal view returns (bytes32) { ######## return keccak256( abi.encode( _ORDER_METADATA_TYPEHASH, metadata.rentDuration, keccak256(abi.encodePacked(hookHashes)) ) ); }
should be changed to:
function _deriveOrderMetadataHash( OrderMetadata memory metadata ) internal view returns (bytes32) { ######## return keccak256( abi.encode( _ORDER_METADATA_TYPEHASH, metadata.orderType, metadata.rentDuration, keccak256(abi.encodePacked(hookHashes)), keccak256(metadata.emittedExtraData) ) ); }
orderMetadataTypeHash
is computed in Signer::_deriveRentalTypehashes function.This is the struct whose type hash is to be defined:
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; }
The EIP 712 guide on how to do this can be found here
orderMetadataTypeHash
is the keccack256 of orderMetadataTypeString
:
// Construct the OrderMetadata type string. bytes memory orderMetadataTypeString = abi.encodePacked( "OrderMetadata(uint8 orderType,uint256 rentDuration,Hook[] hooks,bytes emittedExtraData)" );
Referencing the eip, we can observe that, there is no definition for the Hook struct in the above string. The correct string should be:
bytes memory orderMetadataTypeString = abi.encodePacked( "OrderMetadata(uint8 orderType,uint256 rentDuration,Hook[] hooks,bytes emittedExtraData)Hook(address target,uint256 itemIndex,bytes extraData)" );
With that fixed, rentPayloadTypeHash, which also uses the orderMetadataTypeString, should almost be okay, but there is still one issue.
According to the eip, here:
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.
the current definition:
rentPayloadTypeHash = keccak256( abi.encodePacked( rentPayloadTypeString, orderMetadataTypeString, orderFulfillmentTypeString ) );
should be changed to:
rentPayloadTypeHash = keccak256( abi.encodePacked( rentPayloadTypeString, orderFulfillmentTypeString, orderMetadataTypeString ) );
this is because F comes before M when going down --> order
Manual Review & EIP712 specification
Please observe the referenced fixes in the report
Other
#0 - c4-pre-sort
2024-01-21T17:51:04Z
141345 marked the issue as duplicate of #239
#1 - c4-judge
2024-01-28T22:33:29Z
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
12.582 USDC - $12.58
There are currently some checks in Guard that ensure certain delicate actions aren't allowed to be performed by owners of a safe, but the current implementation doesn't block calls that change the safe fallback handler. This could lead to compatibility issues and some other possible issues.
As discussed with a sponsor, this isn't an intended behaviour.
I have added a coded POC to show how this can be changed.
Add the below test to /test/integration/StopRent.t.sol
, then import:
import {Enum} from "@safe-contracts/common/Enum.sol";
and then add this dummy contract:
contract DummyFallbackHandler {}
then run:
forge test -vvv --match-path test/integration/StopRent.t.sol --match-test test_ADDHANDLER
function test_ADDHANDLER() public { //this will later be done in the setUp function DummyFallbackHandler handler = new DummyFallbackHandler(); //we will be working with bob // speed up in time past the rental expiration // vm.warp(block.timestamp + 750); //get safe's current nonce uint256 nonce = alice.safe.nonce(); bytes memory _calldata = abi.encodeWithSelector( bytes4(keccak256("setFallbackHandler(address)")), handler ); // bytes memory _calldata = abi.encodeWithSelector(0x42966c68, 0); bytes32 dataHash = alice.safe.getTransactionHash( address(alice.safe), uint256(0), _calldata, Enum.Operation.Call, uint256(0), uint256(0), uint256(0), address(0), payable(0), nonce ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(alice.privateKey, dataHash); bytes memory signature = new bytes(65); assembly { mstore(add(signature, 32), r) mstore(add(signature, 64), s) mstore8(add(signature, 96), v) } alice.safe.execTransaction( address(alice.safe), //self call to the safe itself 0, _calldata, Enum.Operation.Call, //Call 0, 0, 0, address(0), payable(0), signature ); }
Here are the logs:
[â †] Compiling... [â ˜] Compiling 1 files with 0.8.22 [â Š] Solc 0.8.22 finished in 45.57sCompiler run successful! [â ’] Solc 0.8.22 finished in 45.57s Running 1 test for test/integration/StopRent.t.sol:TestStopRent [PASS] test_ADDHANDLER() (gas: 143368) Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 18.24ms Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Add a check in the Guard::_checkTransaction function to block such requests
#0 - 141345
2024-01-22T13:56:39Z
175 Tendency l r nc 0 0 0
L 1 d dup of https://github.com/code-423n4/2024-01-renft-findings/issues/593
#1 - 0xean
2024-01-26T19:53:00Z
while warden highlights the potential issue, they show no impact or how to exploit the issue.
#2 - c4-judge
2024-01-27T20:31:28Z
0xean marked the issue as grade-b