diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 85f5562af..000000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -artifacts -cache -coverage diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index f27641414..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,26 +0,0 @@ -module.exports = { - env: { - browser: false, - es2021: true, - mocha: true, - node: true, - }, - plugins: ["@typescript-eslint", "import"], - extends: [ - "standard", - "plugin:prettier/recommended", - "eslint:recommended", - "plugin:import/recommended", - "plugin:import/typescript", - ], - parser: "@typescript-eslint/parser", - parserOptions: { - ecmaVersion: 12, - }, - rules: { - "node/no-unsupported-features/es-syntax": [ - "error", - { ignores: ["modules"] }, - ], - }, -}; diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..3c4cb66c0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ + + +## Motivation + + + +## Solution + + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bfaa07572..2665adfb8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,10 @@ name: Consideration Test CI -on: [push] +on: [push, pull_request] + +concurrency: + group: ${{github.workflow}}-${{github.ref}} + cancel-in-progress: true jobs: build: @@ -12,9 +16,9 @@ jobs: node-version: [16.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: yarn install @@ -29,9 +33,9 @@ jobs: node-version: [16.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: yarn install @@ -46,12 +50,13 @@ jobs: node-version: [16.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: yarn install + - run: yarn build - run: yarn test reference-test: @@ -66,19 +71,21 @@ jobs: REFERENCE: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: yarn install + - run: yarn build + - run: yarn build:ref - run: yarn test:ref forge-lite: name: Run "Lite" Forge Tests (via_ir = false; fuzz_runs = 1000) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: submodules: recursive @@ -100,7 +107,7 @@ jobs: name: Run Forge Tests (via_ir = true; fuzz_runs = 5000) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: submodules: recursive @@ -115,7 +122,7 @@ jobs: - name: Precompile reference using 0.8.7 and via-ir=false run: FOUNDRY_PROFILE=reference forge build - - name: Precompile optimized using 0.8.13 and via-ir=true + - name: Precompile optimized using 0.8.14 and via-ir=true run: FOUNDRY_PROFILE=optimized forge build - name: Run tests @@ -130,12 +137,13 @@ jobs: node-version: [16.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: yarn install + - run: yarn build - run: yarn coverage - uses: VeryGoodOpenSource/very_good_coverage@v1 with: @@ -154,12 +162,14 @@ jobs: REFERENCE: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: yarn install + - run: yarn build + - run: yarn build:ref - run: yarn coverage:ref - uses: VeryGoodOpenSource/very_good_coverage@v1 with: diff --git a/.prettierignore b/.prettierignore index f268596e5..d35df378a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,4 +2,4 @@ node_modules artifacts cache coverage* -gasReporterOutput.json +gasReporterOutput.json \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 9594aa969..000000000 --- a/.prettierrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "overrides": [ - { - "files": "*.sol", - "options": { - "tabWidth": 4, - "printWidth": 80, - "bracketSpacing": true - } - } - ] -} diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 000000000..e80ee82e0 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,71 @@ +# Seaport Contributors + +Contributor | ENS +------------------------------ | ------------------------------ +0age | `0age.eth` +d1ll0n | `d1ll0n.eth` +transmissions11 | `t11s.eth` +Kartik | `slokh.eth` +LeFevre | `lefevre.eth` +Joseph Schiarizzi | `CupOJoseph.eth` +Aspyn Palatnick | `stuckinaboot.eth` +James Wenzel | `emo.eth` +Stephan Min | `stephanm.eth` +Ryan Ghods | `ralxz.eth` +0xPatissier | +pcaversaccio | +David Eiber | +hack3r-0m | `hack3r-0m.eth` +csanuragjain | +Diego Estevez | `antidiego.eth` +Chomtana | `chomtana.eth` +Saw-mon and Natalie | `sawmonandnatalie.eth` +0xBeans | `0xBeans.eth` +0x4non | `punkdev.eth` +Laurence E. Day | `norsefire.eth` +vectorized.eth | `vectorized.eth` +karmacoma | `karmacoma.eth` +horsefacts | `horsefacts.eth` +UncarvedBlock | `uncarvedblock.eth` +Zoraiz Mahmood | `zorz.eth` +William Poulin | `wpoulin.eth` +Rajiv Patel-O'Connor | `rajivpoc.eth` +tserg | `tserg.eth` +cygaar | `cygaar.eth` +Meta0xNull | `meta0xnull.eth` +sach1r0 | +gpersoon | `gpersoon.eth` +Matt Solomon | `msolomon.eth` +twojoy0 | +Weikang Song | `weikangs.eth` +zer0dot | `zer0dot.eth` +Mudit Gupta | `mudit.eth` +ori_dabush | +leonardoalt | `leoalt.eth` +cmichel | `cmichel.eth` +Daniel Gelfand | +PraneshASP | `pranesh.eth` +JasperAlexander | `jasperalexander.eth` +okkothejawa | +FlameHorizon | +vdrg | +Ellahi | `ellahi.eth` +zaz | `1zaz1.eth` +berndartmueller | `berndartmueller.eth` +dmfxyz | `dmfxyz.eth` +daltoncoder | `dontkillrobots.eth` +0xf4ce | `0xf4ce.eth` +phaze | `phaze.eth` +hrkrshnn | `hrkrshnn.eth` +axic | `axic.eth` +leastwood | `leastwood.eth` +0xsanson | `sanson.eth` +blockdev | `blockd3v.eth` +dmitriia | +bokeh-eth | +fiveoutofnine | `fiveoutofnine.eth` +asutorufos | +rfart(rfa) | +shuklaayush | `shuklaayush.eth` +Riley Holterhus | +big-tech-sux | diff --git a/README.md b/README.md index 2dbbf5c0f..611eca339 100644 --- a/README.md +++ b/README.md @@ -1,197 +1,158 @@ +![Seaport](img/Seaport-banner.png) + # Seaport -Seaport is a marketplace contract for safely and efficiently creating and fulfilling orders for ERC721 and ERC1155 items. Each order contains an arbitrary number of items that the offerer is willing to give (the "offer") along with an arbitrary number of items that must be received along with their respective receivers (the "consideration"). - -## Order - -Each order contains eleven key components: -- The `offerer` of the order supplies all offered items and must either fulfill the order personally (i.e. `msg.sender == offerer`) or approve the order via signature (either standard 65-byte EDCSA, 64-byte EIP-2098, or an EIP-1271 `isValidSignature` check) or by listing the order on-chain (i.e. calling `validate`). -- The `zone` of the order is an optional secondary account attached to the order with two additional privileges: - - The zone may cancel orders where it is named as the zone by calling `cancel`. (Note that offerers can also cancel their own orders, either individually or for all orders signed with their current nonce at once by calling `incrementNonce`). - - "Restricted" orders (as specified by the order type) must either be executed by the zone or the offerer, or must be approved as indicated by a call to an `isValidOrder` or `isValidOrderIncludingExtraData` view function on the zone. -- The `offer` contains an array of items that may be transferred from the offerer's account, where each item consists of the following components: - - The `itemType` designates the type of item, with valid types being Ether (or other native token for the given chain), ERC20, ERC721, ERC1155, ERC721 with "criteria" (explained below), and ERC1155 with criteria. - - The `token` designates the account of the item's token contract (with the null address used for Ether or other native tokens). - - The `identifierOrCriteria` represents either the ERC721 or ERC1155 token identifier or, in the case of a criteria-based item type, a merkle root composed of the valid set of token identifiers for the item. This value will be ignored for Ether and ERC20 item types, and can optionally be zero for criteria-based item types to allow for any identifier. - - The `startAmount` represents the amount of the item in question that will be required should the order be fulfilled at the moment the order becomes active. - - The `endAmount` represents the amount of the item in question that will be required should the order be fulfilled at the moment the order expires. If this value differs from the item's `startAmount`, the realized amount is calculated linearly based on the time elapsed since the order became active. -- The `consideration` contains an array of items that must be received in order to fulfill the order. It contains all of the same components as an offered item, and additionally includes a `recipient` that will receive each item. This array may be extended by the fulfiller on order fulfillment so as to support "tipping" (e.g. relayer or referral payments). -- The `orderType` designates one of four types for the order depending on two distinct preferences: - - `FULL` indicates that the order does not support partial fills, whereas `PARTIAL` enables filling some fraction of the order, with the important caveat that each item must be cleanly divisible by the supplied fraction (i.e. no remainder after division). - - `OPEN` indicates that the call to execute the order can be submitted by any account, whereas `RESTRICTED` requires that the order either be executed by the offerer or the zone of the order, or that a magic value indicating that the order is approved is returned upon calling an `isValidOrder` or `isValidOrderIncludingExtraData` view function on the zone. -- The `startTime` indicates the block timestamp at which the order becomes active. -- The `endTime` indicates the block timestamp at which the order expires. This value and the `startTime` are used in conjunction with the `startAmount` and `endAmount` of each item to derive their current amount. -- The `zoneHash` represents an arbitrary 32-byte value that will be supplied to the zone when fulfilling restricted orders that the zone can utilize when making a determination on whether to authorize the order. -- The `salt` represents an arbitrary source of entropy for the order. -- The `conduitKey` is a `bytes32` value that indicates what conduit, if any, should be utilized as a source for token approvals when performing transfers. By default (i.e. when `conduitKey` is set to the zero hash), the offerer will grant ERC20, ERC721, and ERC1155 token approvals to Seaport directly so that it can perform any transfers specified by the order during fulfillment. In contrast, an offerer that elects to utilize a conduit will grant token approvals to the conduit contract corresponding to the supplied conduit key, and Seaport will then instruct that conduit to transfer the respective tokens. -- The `nonce` indicates a value that must match the current nonce for the given offerer. - -## Order Fulfillment - -Orders are fulfilled via one of four methods: -- Calling one of two "standard" functions, `fulfillOrder` and `fulfillAdvancedOrder`, where a second implied order will be constructed with the caller as the offerer, the consideration of the fulfilled order as the offer, and the offer of the fulfilled order as the consideration (with "advanced" orders containing the fraction that should be filled alongside a set of "criteria resolvers" that designate an identifier and a corresponding inclusion proof for each criteria-based item on the fulfilled order). All offer items will be transferred from the offerer of the order to the fulfiller, then all consideration items will be transferred from the fulfiller to the named recipient. -- Calling the "basic" function, `fulfillBasicOrder` with one of six basic route types supplied (`ETH_TO_ERC721`, `ETH_TO_ERC1155`, `ERC20_TO_ERC721`, `ERC20_TO_ERC1155`, `ERC721_TO_ERC20`, and `ERC1155_TO_ERC20`) will derive the order to fulfill from a subset of components, assuming the order in question adheres to the following: - - The order only contains a single offer item and contains at least one consideration item. - - The order contains exactly one ERC721 or ERC1155 item and that item is not criteria-based. - - The offerer of the order is the recipient of the first consideration item. - - All other items have the same Ether (or other native tokens) or ERC20 item type and token. - - The order does not offer an item with Ether (or other native tokens) as its item type. - - The `startAmount` on each item must match that item's `endAmount` (i.e. items cannot have an ascending/descending amount). - - All "ignored" item fields (i.e. `token` and `identifierOrCriteria` on native items and `identifierOrCriteria` on ERC20 items) are set to the null address or zero. - - If the order has an ERC721 item, that item has an amount of `1`. - - If the order has multiple consideration items and all consideration items other than the first consideration item have the same item type as the offered item, the offered item amount is not less than the sum of all consideration item amounts excluding the first consideration item amount. -- Calling one of two "fulfill available" functions, `fulfillAvailableOrders` and `fulfillAvailableAdvancedOrders`, where a group of orders are supplied alongside a group of fulfillments specifying which offer items can be aggregated into distinct transfers and which consideration items can be accordingly aggregated, and where any orders that have been cancelled, have an invalid time, or have already been fully filled will be skipped without causing the rest of the available orders to revert. Additionally, any remaining orders will be skipped once `maximumFulfilled` available orders have been located. Similar to the standard fulfillment method, all offer items will be transferred from the respective offerer to the fulfiller, then all consideration items will be transferred from the fulfiller to the named recipient. -- Calling one of two "match" functions, `matchOrders` and `matchAdvancedOrders`, where a group of explicit orders are supplied alongside a group of fulfillments specifying which offer items to apply to which consideration items (and with the "advanced" case operating in a similar fashion to the standard method, but supporting partial fills via supplied `numerator` and `denominator` fractional values as well as an optional `extraData` argument that will be supplied as part of a call to the `isValidOrderIncludingExtraData` view function on the zone when fulfilling restricted order types). Note that orders fulfilled in this manner do not have an explicit fulfiller; instead, Seaport will simply ensure coincidence of wants across each order. - -While the standard method can technically be used for fulfilling any order, it suffers from key efficiency limitations in certain scenarios: -- It requires additional calldata compared to the basic method for simple "hot paths". -- It requires the fulfiller to approve each consideration item, even if the consideration item can be fulfilled using an offer item (as is commonly the case when fulfilling an order that offers ERC20 items for an ERC721 or ERC1155 item and also includes consideration items with the same ERC20 item type for paying fees). -- It can result in unnecessary transfers, whereas in the "match" case those transfers can be reduced to a more minimal set. - -### Balance & Approval Requirements - -When creating an offer, the following requirements should be checked to ensure that the order will be fulfillable: -- The offerer should have sufficient balance of all offered items. -- If the order does not indicate to use a conduit, the offerer should have sufficient approvals set for the Seaport contract for all offered ERC20, ERC721, and ERC1155 items. -- If the order _does_ indicate to use a conduit, the offerer should have sufficient approvals set for the respective conduit contract for all offered ERC20, ERC721 and ERC1155 items. - -When fulfilling a _basic_ order, the following requirements need to be checked to ensure that the order will be fulfillable: -- The above checks need to be performed to ensure that the offerer still has sufficient balance and approvals. -- The fulfiller should have sufficient balance of all consideration items _except for those with an item type that matches the order's offered item type_ — by way of example, if the fulfilled order offers an ERC20 item and requires an ERC721 item to the offerer and the same ERC20 item to another recipient, the fulfiller needs to own the ERC721 item but does not need to own the ERC20 item as it will be sourced from the offerer. -- If the fulfiller does not elect to utilize a conduit, they need to have sufficient approvals set for the Seaport contract for all ERC20, ERC721, and ERC1155 consideration items on the fulfilled order _except for ERC20 items with an item type that matches the order's offered item type_. -- If the fulfiller _does_ elect to utilize a conduit, they need to have sufficient approvals set for their respective conduit for all ERC20, ERC721, and ERC1155 consideration items on the fulfilled order _except for ERC20 items with an item type that matches the order's offered item type_. -- If the fulfilled order specifies Ether (or other native tokens) as consideration items, the fulfiller must be able to supply the sum total of those items as `msg.value`. - -When fulfilling a _standard_ order, the following requirements need to be checked to ensure that the order will be fulfillable: -- The above checks need to be performed to ensure that the offerer still has sufficient balance and approvals. -- The fulfiller should have sufficient balance of all consideration items _after receiving all offered items_ — by way of example, if the fulfilled order offers an ERC20 item and requires an ERC721 item to the offerer and the same ERC20 item to another recipient with an amount less than or equal to the offered amount, the fulfiller does not need to own the ERC20 item as it will first be received from the offerer. -- If the fulfiller does not elect to utilize a conduit, they need to have sufficient approvals set for the Seaport contract for all ERC20, ERC721, and ERC1155 consideration items on the fulfilled order. -- If the fulfiller _does_ elect to utilize a conduit, they need to have sufficient approvals set for their respective conduit for all ERC20, ERC721, and ERC1155 consideration items on the fulfilled order. -- If the fulfilled order specifies Ether (or other native tokens) as consideration items, the fulfiller must be able to supply the sum total of those items as `msg.value`. - -When fulfilling a set of _match_ orders, the following requirements need to be checked to ensure that the order will be fulfillable: -- Each account that sources the ERC20, ERC721, or ERC1155 item for an execution that will be performed as part of the fulfillment must have sufficient balance and approval on Seaport or the indicated conduit at the time the execution is triggered. Note that prior executions may supply the necessary balance for subsequent executions. -- The sum total of all executions involving Ether (or other native tokens) must be supplied as `msg.value`. Note that executions where the offerer and the recipient are the same account will be filtered out of the final execution set. - -### Partial Fills - -When constructing an order, the offerer may elect to enable partial fills by setting an appropriate order type. Then, orders that support partial fills can be fulfilled for some _fraction_ of the respective order, allowing subsequent fills to bypass signature verification. To summarize a few key points on partial fills: -- When creating orders that support partial fills or determining a fraction to fill on those orders, all items (both offer and consideration) on the order must be cleanly divisible by the supplied fraction (i.e. no remainder after division). -- If the desired fraction to fill would result in more than the full order amount being filled, that fraction will be reduced to the amount remaining to fill. This applies to both partial fill attempts as well as full fill attempts. If this behavior is not desired (i.e. the fill should be "all or none"), the fulfiller can either use a "basic" order method if available (which requires that the full order amount be filled), or use the "match" order method and explicitly provide an order that requires the full desired amount be received back. - - By way of example: if one fulfiller tries to fill 1/2 of an order but another fulfiller first fills 3/4 of the order, the original fulfiller will end up filling 1/4 of the order. -- If any of the items on a partially fillable order specify a different "startAmount" and "endAmount (e.g. they are ascending-amount or descending-amount items), the fraction will be applied to _both_ amounts prior to determining the current price. This ensures that cleanly divisible amounts can be chosen when constructing the order without a dependency on the time when the order is ultimately fulfilled. -- Partial fills can be combined with criteria-based items to enable constructing orders that offer or receive multiple items that would otherwise not be partially fillable (e.g. ERC721 items). - - By way of example: an offerer can create a partially fillable order to supply up to 10 ETH for up to 10 ERC721 items from a given collection; then, any fulfiller can fill a portion of that order until it has been fully filled (or cancelled). - -## Sequence of Events - -### Fulfill Order - -When fulfilling an order via `fulfillOrder` or `fulfillAdvancedOrder`: - 1. Hash order - - Derive hashes for offer items and consideration items - - Retrieve current nonce for the offerer - - Derive hash for order - 2. Perform initial validation - - Ensure current time is inside order range - - Ensure valid caller for the order type; if the order type is restricted and the caller is not the offerer or the zone, call the zone to determine whether the order is valid - 3. Retrieve and update order status - - Ensure order is not cancelled - - Ensure order is not fully filled - - If the order is _partially_ filled, reduce the supplied fill amount if necessary so that the order is not overfilled - - Verify the order signature if not already validated - - Determine fraction to fill based on preference + available amount - - Update order status (validated + fill fraction) - 4. Determine amount for each item - - Compare start amount and end amount - - if they are equal: apply fill fraction to either one, ensure it divides cleanly, and use that amount - - if not: apply fill fraction to both, ensuring they both divide cleanly, then find linear fit based on current time - 5. Apply criteria resolvers - - Ensure each criteria resolver refers to a criteria-based order item - - Ensure the supplied identifier for each item is valid via inclusion proof if the item has a non-zero criteria root - - Update each item type and identifier - - Ensure all remaining items are non-criteria-based - 6. Emit OrderFulfilled event - - Include updated items (i.e. after amount adjustment and criteria resolution) - 7. Transfer offer items from offerer to caller - - Use either conduit or Seaport directly to source approvals, depending on order type - 8. Transfer consideration items from caller to respective recipients - - Use either conduit or Seaport directly to source approvals, depending on the fulfiller's stated preference - -> Note: `fulfillBasicOrder` works in a similar fashion, with a few exceptions: it reconstructs the order from a subset of order elements, skips linear fit amount adjustment and criteria resolution, requires that the full order amount be fillable, and performs a more minimal set of transfers by default when the offer item shares the same type and token as additional consideration items. - -### Match Orders - -When matching a group of orders via `matchOrders` or `matchAdvancedOrders`, steps 1 through 6 are nearly identical but are performed for _each_ supplied order. From there, the implementation diverges from standard fulfillments: - - 7. Apply fulfillments - - Ensure each fulfillment refers to one or more offer items and one or more consideration items, all with the same type and token, and with the same approval source for each offer item and the same recipient for each consideration item - - Reduce the amount on each offer item and each consideration item to zero and track total reduced amounts for each - - Compare total amounts for each and add back the remaining amount to the first item on the appropriate side of the order - - Return a single execution for each fulfillment - 8. Scan each consideration item and ensure that none still have a nonzero amount remaining - 9. Perform transfers as part of each execution - - Use either conduit or Seaport directly to source approvals, depending on the original order type - - Ignore each execution where `to == from` or `amount == 0` *(NOTE: the current implementation does not perform this last optimization)* - -## Known Limitations and Workarounds - -- As all offer and consideration items are allocated against one another in memory, there are scenarios in which the actual received item amount will differ from the amount specified by the order — notably, this includes items with a fee-on-transfer mechanic. Orders that contain items of this nature (or, more broadly, items that have some post-fulfillment state that should be met) should leverage "restricted" order types and route the order fulfillment through a zone contract that performs the necessary checks after order fulfillment is completed. -- As all offer items are taken directly from the offerer and all consideration items are given directly to the named recipient, there are scenarios where those accounts can increase the gas cost of order fulfillment or block orders from being fulfilled outright depending on the item being transferred. If the item in question is Ether or a similar native token, a recipient can throw in the payable fallback or even spend excess gas from the submitter. Similar mechanics can be leveraged by both offerers and receives if the item in question is a token with a transfer hook (like ERC1155 and ERC777) or a non-standard token implementation. Potential remediations to this category of issue include wrapping Ether as WETH as a fallback if the initial transfer fails and allowing submitters to specify the amount of gas that should be allocated as part of a given fulfillment. Orders that support explicit fulfillments can also elect to leave problematic or unwanted offer items unspent as long as all consideration items are received in full. -- As fulfillments may be executed in whatever sequence the fulfiller specifies as long as the fulfillments are all executable, as restricted orders are validated via zones prior to execution, and as orders may be combined with other orders or have additional consideration items supplied, any items with modifiable state are at risk of having that state modified during execution if a payable Ether recipient or onReceived 1155 transfer hook is able to modify that state. By way of example, imagine an offerer offers WETH and requires some ERC721 item as consideration, where the ERC721 should have some additional property like not having been used to mint some other ERC721 item. Then, even if the offerer enforces that the ERC721 have that property via a restricted order that checks for the property, a malicious fulfiller could include a second order (or even just an additional consideration item) that uses the ERC721 item being sold to mint before it is transferred to the offerer. One category of remediation for this problem is to use restricted orders that do not implement `isValidOrder` and actually require that order fulfillment is routed through them so that they can perform post-fulfillment validation. Another interesting solution to this problem that retains order composability is to "fight fire with fire" and have the offerer include a "validator" ERC1155 consideration item on orders that require additional assurances; this would be a contract that contains the ERC1155 interface but is not actually an 1155 token, and instead leverages the `onReceived` hook as a means to validate that the expected invariants were upheld, reverting the "transfer" if the check fails (so in the case of the example above, this hook would ensure that the offerer was the owner of the ERC721 item in question and that it had not yet been used to mint the other ERC721). The key limitation to this mechanic is the amount of data that can be supplied in-band via this route; only three arguments ("from", "identifier", and "amount") are available to utilize. -- As all consideration items are supplied at the time of order creation, dynamic adjustment of recipients or amounts after creation (e.g. modifications to royalty payout info) is not supported. However, a zone can enforce that a given restricted order contains _new_ dynamically computed consideration items by deriving them and either supplying them manually or ensuring that they are present via `isValidZoneIncludingExtraData` since consideration items can be extended arbitrarily, with the important caveat that no more than the original offer item amounts can be spent. -- As all criteria-based items are tied to a particular token, there is no native way to construct orders where items specify cross-token criteria. Additionally, each potential identifier for a particular criteria-based item must have the same amount as any other identifier. -- As orders that contain items with ascending or descending amounts may not be filled as quickly as a fulfiller would like (e.g. transactions taking longer than expected to be included), there is a risk that fulfillment on those orders will supply a larger item amount, or receive back a smaller item amount, than they intended or expected. One way to prevent these outcomes is to utilize `matchOrders`, supplying a contrasting order for the fulfiller that explicitly specifies the maximum allowable offer items to be spent and consideration items to be received back. Special care should be taken when handling orders that contain both brief durations as well as items with ascending or descending amounts, as realized amounts may shift appreciably in a short window of time. -- As all items on orders supporting partial fills must be "cleanly divisible" when performing a partial fill, orders with multiple items should to be constructed with care. A straightforward heuristic is to start with a "unit" bundle (e.g. 1 NFT item A, 3 NFT item B, and 5 NFT item C for 2 ETH) then applying a multiple to that unit bundle (e.g. 7 of those units results in a partial order for 7 NFT item A, 21 NFT item B, and 35 NFT item C for 14 ETH). -- As Ether cannot be "taken" from an account, any order that contains Ether or other native tokens as an offer item (including "implied" mirror orders) must be supplied by the caller executing the order(s) as msg.value. This also explains why there are no `ERC721_TO_ERC20` and `ERC1155_TO_ERC20` basic order route types, as Ether cannot be taken from the offerer in these cases. One important takeaway from this mechanic is that, technically, anyone can supply Ether on behalf of a given offerer (whereas the offerer themselves must supply all other items). It also means that all Ether must be supplied at the time the order or group of orders is originally called (and the amount available to spend by offer items cannot be increased by an external source during execution as is the case for token balances). -- As extensions to the consideration array on fulfillment (i.e. "tipping") can be arbitrarily set by the caller, fulfillments where all matched orders have already been signed for or validated can be frontrun on submission, with the frontrunner modifying any tips. Therefore, it is important that orders fulfilled in this manner either leverage "restricted" order types with a zone that enforces appropriate allocation of consideration extensions, or that each offer item is fully spent and each consideration item is appropriately declared on order creation. -- As orders that have been verified (via a call to `validate`) or partially filled will skip signature validation on subsequent fulfillments, orders that utilize EIP-1271 for verifying orders may end up in an inconsistent state where the original signature is no longer valid but the order is still fulfillable. In these cases, the offerer must explicitly cancel the previously verified order in question if they no longer wish for the order to be fulfillable. -- As orders filled by the "fulfill available" method will only be skipped if those orders have been cancelled, fully filled, or are inactive, fulfillments may still be attempted on unfulfillable orders (examples include revoked approvals or insufficient balances). This scenario (as well as issues with order formatting) will result in the full batch failing. One remediation to this failure condition is to perform additional checks from an executing zone or wrapper contract when constructing the call and filtering orders based on those checks. -- As order parameters must be supplied upon cancellation, orders that were meant to remain private (e.g. were not published publically) will be made visible upon cancellation. While these orders would not be _fulfillable_ without a corresponding signature, cancellation of private orders without broadcasting intent currently requires the offerer (or the zone, if the order type is restricted and the zone supports it) to increment the nonce. -- As order fulfillment attempts may become public before being included in a block, there is a risk of those orders being front-run. This risk is magnified in cases where offered items contain ascending amounts or consideration items contain descending amounts, as there is added incentive to leave the order unfulfilled until another interested fulfiller attempts to fulfill the order in question. Remediation efforts include utilization of a private mempool (e.g. flashbots) and/or restricted orders where the respective zone enforces a commit-reveal scheme. +Seaport is a new marketplace protocol for safely and efficiently buying and selling NFTs. -## Usage +## Table of Contents + +- [Background](#background) +- [Deployments](#deployments) +- [Diagram](#diagram) +- [Install](#install) +- [Usage](#usage) +- [Audits](#audits) +- [Contributing](#contributing) +- [License](#license) + +## Background + +Seaport is a marketplace protocol for safely and efficiently buying and selling NFTs. Each listing contains an arbitrary number of items that the offerer is willing to give (the "offer") along with an arbitrary number of items that must be received along with their respective receivers (the "consideration"). + +See the [documentation](docs/SeaportDocumentation.md), the [interface](contracts/interfaces/SeaportInterface.sol), and the full [interface documentation](https://docs.opensea.io/v2.0/reference/seaport-overview) for more information on Seaport. + +## Deployments + +Seaport 1.1 deployment addresses: + +| Network | Address | +| ---------------- | ------------------------------------------ | +| Ethereum Mainnet | [0x00000000006c3852cbEf3e08E8dF289169EdE581](https://etherscan.io/address/0x00000000006c3852cbEf3e08E8dF289169EdE581#code) | +| Rinkeby | [0x00000000006c3852cbEf3e08E8dF289169EdE581](https://rinkeby.etherscan.io/address/0x00000000006c3852cbEf3e08E8dF289169EdE581#code) | + +Conduit Controller deployment addresses: + +| Network | Address | +| ---------------- | ------------------------------------------ | +| Ethereum Mainnet | [0x00000000F9490004C11Cef243f5400493c00Ad63](https://etherscan.io/address/0x00000000F9490004C11Cef243f5400493c00Ad63#code) | +| Rinkeby | [0x00000000F9490004C11Cef243f5400493c00Ad63](https://rinkeby.etherscan.io/address/0x00000000F9490004C11Cef243f5400493c00Ad63#code) | + +## Diagram + +```mermaid +graph TD + Offer & Consideration --> Order + zone & conduitKey --> Order + + subgraph Seaport[ ] + Order --> Fulfill & Match + Order --> Validate & Cancel + end + + Validate --> Verify + Cancel --> OrderStatus + + Fulfill & Match --> OrderCombiner --> OrderFulfiller + + OrderCombiner --> BasicOrderFulfiller --> OrderValidator + OrderCombiner --> FulfillmentApplier + + OrderFulfiller --> CriteriaResolution + OrderFulfiller --> AmountDeriver + OrderFulfiller --> OrderValidator + + OrderValidator --> ZoneInteraction + OrderValidator --> Executor --> TokenTransferrer + Executor --> Conduit --> TokenTransferrer + Executor --> Verify + + subgraph Verifiers[ ] + Verify --> Time & Signature & OrderStatus + end +``` + +For a more thorough flowchart see [Seaport diagram](./diagrams/Seaport.drawio.svg). + +## Install + +To install dependencies and compile contracts: -First, install dependencies and compile: ```bash +git clone https://github.com/ProjectOpenSea/seaport && cd seaport yarn install yarn build ``` -Next, run linters and tests: +## Usage + +To run hardhat tests written in javascript: + ```bash -yarn lint:check yarn test yarn coverage ``` -To profile gas usage (note that gas usage is mildly non-deterministic at the moment due to random inputs in tests): +> Note: artifacts and cache folders may occasionally need to be removed between standard and coverage test runs. + +To run hardhat tests against reference contracts: + +```bash +yarn test:ref +yarn coverage:ref +``` + +To profile gas usage: + ```bash yarn profile ``` ### Foundry Tests -First, install Foundry (assuming a Linux or macOS system): +Seaport also includes a suite of fuzzing tests written in solidity with Foundry. + +To install Foundry (assuming a Linux or macOS system): + ```bash curl -L https://foundry.paradigm.xyz | bash ``` This will download foundryup. To start Foundry, run: + ```bash foundryup ``` To install dependencies: + ``` forge install ``` -To run tests: +To precompile contracts: + +The optimized contracts are compiled using the IR pipeline, which can take a long time to compile. By default, the differential test suite deploys precompiled versions of both the optimized and reference contracts. Precompilation can be done by specifying specific Foundry profiles. + +```bash +FOUNDRY_PROFILE=optimized forge build +FOUNDRY_PROFILE=reference forge build +``` + +There are three Foundry profiles for running the test suites, which bypass the IR pipeline to speed up compilation. To run tests, run any of the following: + ```bash -forge test +FOUNDRY_PROFILE=test forge test # with 5000 fuzz runs +FOUNDRY_PROFILE=lite forge test # with 1000 fuzz runs +FOUNDRY_PROFILE=local-ffi forge test # compiles and deploys ReferenceConsideration normally, with 1000 fuzz runs ``` +You may wish to include a `.env` file that `export`s a specific profile when developing locally. + +**Note** that stack+debug traces will not be available for precompiled contracts. To facilitate local development, specifying `FOUNDRY_PROFILE=local-ffi` will compile and deploy the reference implementation normally, allowing for stack+debug traces. + +**Note** the `local-ffi` profile uses Forge's `ffi` flag. `ffi` can potentially be unsafe, as it allows Forge to execute arbitrary code. Use with caution, and always ensure you trust the code in this repository, especially when working on third-party forks. + + The following modifiers are also available: - Level 2 (-vv): Logs emitted during tests are also displayed. @@ -200,8 +161,47 @@ The following modifiers are also available: - Level 5 (-vvvvv): Stack traces and setup traces are always displayed. ```bash -forge test -vv +FOUNDRY_PROFILE=test forge test -vv ``` For more information on foundry testing and use, see [Foundry Book installation instructions](https://book.getfoundry.sh/getting-started/installation.html). +To run lint checks: + +```bash +yarn lint:check +``` + +Lint checks utilize prettier, prettier-plugin-solidity, and solhint. + +```javascript +"prettier": "^2.5.1", +"prettier-plugin-solidity": "^1.0.0-beta.19", +``` + +## Audits + +OpenSea engaged Trail of Bits to audit the security of Seaport. From April 18th to May 12th 2022, a team of Trail of Bits consultants conducted a security review of Seaport. The audit did not uncover significant flaws that could result in the compromise of a smart contract, loss of funds, or unexpected behavior in the target system. Their [full report is available here](https://github.com/trailofbits/publications/blob/master/reviews/SeaportProtocol.pdf). + +## Contributing + +Contributions to Seaport are welcome by anyone interested in writing more tests, improving readability, optimizing for gas efficiency, or extending the protocol via new zone contracts or other features. + +When making a pull request, ensure that: + +- All tests pass. +- Code coverage remains at 100% (coverage tests must currently be written in hardhat). +- All new code adheres to the style guide: + - All lint checks pass. + - Code is thoroughly commented with natspec where relevant. +- If making a change to the contracts: + - Gas snapshots are provided and demonstrate an improvement (or an acceptable deficit given other improvements). + - Reference contracts are modified correspondingly if relevant. + - New tests (ideally via foundry) are included for all new features or code paths. +- If making a modification to third-party dependencies, `yarn audit` passes. +- A descriptive summary of the PR has been provided. + +## License + +[MIT](LICENSE) Copyright 2022 Ozone Networks, Inc. + diff --git a/.solcover-reference.js b/config/.solcover-reference.js similarity index 100% rename from .solcover-reference.js rename to config/.solcover-reference.js diff --git a/.solcover.js b/config/.solcover.js similarity index 94% rename from .solcover.js rename to config/.solcover.js index c81ea4054..9cba82828 100644 --- a/.solcover.js +++ b/config/.solcover.js @@ -9,6 +9,7 @@ module.exports = { "interfaces/ConsiderationEventsAndErrors.sol", "interfaces/ConsiderationInterface.sol", "interfaces/EIP1271Interface.sol", + "interfaces/SeaportInterface.sol", "interfaces/ZoneInterface.sol", "lib/ConsiderationConstants.sol", "lib/ConsiderationEnums.sol", @@ -23,6 +24,7 @@ module.exports = { "reference/lib/ReferenceTokenTransferrer.sol", "test/EIP1271Wallet.sol", "test/ExcessReturnDataRecipient.sol", + "test/ERC1155BatchRecipient.sol", "test/Reenterer.sol", "test/TestERC1155.sol", "test/TestERC20.sol", diff --git a/.solhint.json b/config/.solhint.json similarity index 100% rename from .solhint.json rename to config/.solhint.json diff --git a/.solhintignore b/config/.solhintignore similarity index 100% rename from .solhintignore rename to config/.solhintignore diff --git a/contracts/Seaport.sol b/contracts/Seaport.sol index bc8ddb2a4..ad036f657 100644 --- a/contracts/Seaport.sol +++ b/contracts/Seaport.sol @@ -1,14 +1,78 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; -import { Consideration } from "./Consideration.sol"; +import { Consideration } from "./lib/Consideration.sol"; /** * @title Seaport - * @author 0age - * @custom:coauthor d1ll0n - * @custom:coauthor transmissions11 - * @custom:version 1 + * @custom:version 1.1 + * @author 0age (0age.eth) + * @custom:coauthor d1ll0n (d1ll0n.eth) + * @custom:coauthor transmissions11 (t11s.eth) + * @custom:contributor Kartik (slokh.eth) + * @custom:contributor LeFevre (lefevre.eth) + * @custom:contributor Joseph Schiarizzi (CupOJoseph.eth) + * @custom:contributor Aspyn Palatnick (stuckinaboot.eth) + * @custom:contributor James Wenzel (emo.eth) + * @custom:contributor Stephan Min (stephanm.eth) + * @custom:contributor Ryan Ghods (ralxz.eth) + * @custom:contributor hack3r-0m (hack3r-0m.eth) + * @custom:contributor Diego Estevez (antidiego.eth) + * @custom:contributor Chomtana (chomtana.eth) + * @custom:contributor Saw-mon and Natalie (sawmonandnatalie.eth) + * @custom:contributor 0xBeans (0xBeans.eth) + * @custom:contributor 0x4non (punkdev.eth) + * @custom:contributor Laurence E. Day (norsefire.eth) + * @custom:contributor vectorized.eth (vectorized.eth) + * @custom:contributor karmacoma (karmacoma.eth) + * @custom:contributor horsefacts (horsefacts.eth) + * @custom:contributor UncarvedBlock (uncarvedblock.eth) + * @custom:contributor Zoraiz Mahmood (zorz.eth) + * @custom:contributor William Poulin (wpoulin.eth) + * @custom:contributor Rajiv Patel-O'Connor (rajivpoc.eth) + * @custom:contributor tserg (tserg.eth) + * @custom:contributor cygaar (cygaar.eth) + * @custom:contributor Meta0xNull (meta0xnull.eth) + * @custom:contributor gpersoon (gpersoon.eth) + * @custom:contributor Matt Solomon (msolomon.eth) + * @custom:contributor Weikang Song (weikangs.eth) + * @custom:contributor zer0dot (zer0dot.eth) + * @custom:contributor Mudit Gupta (mudit.eth) + * @custom:contributor leonardoalt (leoalt.eth) + * @custom:contributor cmichel (cmichel.eth) + * @custom:contributor PraneshASP (pranesh.eth) + * @custom:contributor JasperAlexander (jasperalexander.eth) + * @custom:contributor Ellahi (ellahi.eth) + * @custom:contributor zaz (1zaz1.eth) + * @custom:contributor berndartmueller (berndartmueller.eth) + * @custom:contributor dmfxyz (dmfxyz.eth) + * @custom:contributor daltoncoder (dontkillrobots.eth) + * @custom:contributor 0xf4ce (0xf4ce.eth) + * @custom:contributor phaze (phaze.eth) + * @custom:contributor hrkrshnn (hrkrshnn.eth) + * @custom:contributor axic (axic.eth) + * @custom:contributor leastwood (leastwood.eth) + * @custom:contributor 0xsanson (sanson.eth) + * @custom:contributor blockdev (blockd3v.eth) + * @custom:contributor fiveoutofnine (fiveoutofnine.eth) + * @custom:contributor shuklaayush (shuklaayush.eth) + * @custom:contributor 0xPatissier + * @custom:contributor pcaversaccio + * @custom:contributor David Eiber + * @custom:contributor csanuragjain + * @custom:contributor sach1r0 + * @custom:contributor twojoy0 + * @custom:contributor ori_dabush + * @custom:contributor Daniel Gelfand + * @custom:contributor okkothejawa + * @custom:contributor FlameHorizon + * @custom:contributor vdrg + * @custom:contributor dmitriia + * @custom:contributor bokeh-eth + * @custom:contributor asutorufos + * @custom:contributor rfart(rfa) + * @custom:contributor Riley Holterhus + * @custom:contributor big-tech-sux * @notice Seaport is a generalized ETH/ERC20/ERC721/ERC1155 marketplace. It * minimizes external calls to the greatest extent possible and provides * lightweight methods for common routes as well as more flexible @@ -37,9 +101,9 @@ contract Seaport is Consideration { function _name() internal pure override returns (string memory) { // Return the name of the contract. assembly { - mstore(0, 0x20) - mstore(0x27, 0x07536561706f7274) - return(0, 0x60) + mstore(0x20, 0x20) + mstore(0x47, 0x07536561706f7274) + return(0x20, 0x60) } } diff --git a/contracts/conduit/Conduit.sol b/contracts/conduit/Conduit.sol index 0169c3472..411f77bc4 100644 --- a/contracts/conduit/Conduit.sol +++ b/contracts/conduit/Conduit.sol @@ -13,6 +13,8 @@ import { ConduitBatch1155Transfer } from "./lib/ConduitStructs.sol"; +import "./lib/ConduitConstants.sol"; + /** * @title Conduit * @author 0age @@ -32,6 +34,40 @@ contract Conduit is ConduitInterface, TokenTransferrer { // Track the status of each channel. mapping(address => bool) private _channels; + /** + * @notice Ensure that the caller is currently registered as an open channel + * on the conduit. + */ + modifier onlyOpenChannel() { + // Utilize assembly to access channel storage mapping directly. + assembly { + // Write the caller to scratch space. + mstore(ChannelKey_channel_ptr, caller()) + + // Write the storage slot for _channels to scratch space. + mstore(ChannelKey_slot_ptr, _channels.slot) + + // Derive the position in storage of _channels[msg.sender] + // and check if the stored value is zero. + if iszero( + sload(keccak256(ChannelKey_channel_ptr, ChannelKey_length)) + ) { + // The caller is not an open channel; revert with + // ChannelClosed(caller). First, set error signature in memory. + mstore(ChannelClosed_error_ptr, ChannelClosed_error_signature) + + // Next, set the caller as the argument. + mstore(ChannelClosed_channel_ptr, caller()) + + // Finally, revert, returning full custom error with argument. + revert(ChannelClosed_error_ptr, ChannelClosed_error_length) + } + } + + // Continue with function execution. + _; + } + /** * @notice In the constructor, set the deployer as the controller. */ @@ -42,7 +78,12 @@ contract Conduit is ConduitInterface, TokenTransferrer { /** * @notice Execute a sequence of ERC20/721/1155 transfers. Only a caller - * with an open channel can call this function. + * with an open channel can call this function. Note that channels + * are expected to implement reentrancy protection if desired, and + * that cross-channel reentrancy may be possible if the conduit has + * multiple open channels at once. Also note that channels are + * expected to implement checks against transferring any zero-amount + * items if that constraint is desired. * * @param transfers The ERC20/721/1155 transfers to perform. * @@ -52,23 +93,16 @@ contract Conduit is ConduitInterface, TokenTransferrer { function execute(ConduitTransfer[] calldata transfers) external override + onlyOpenChannel returns (bytes4 magicValue) { - // Ensure that the caller has an open channel. - if (!_channels[msg.sender]) { - revert ChannelClosed(); - } - // Retrieve the total number of transfers and place on the stack. uint256 totalStandardTransfers = transfers.length; // Iterate over each transfer. for (uint256 i = 0; i < totalStandardTransfers; ) { - // Retrieve the transfer in question. - ConduitTransfer calldata standardTransfer = transfers[i]; - - // Perform the transfer. - _transfer(standardTransfer); + // Retrieve the transfer in question and perform the transfer. + _transfer(transfers[i]); // Skip overflow check as for loop is indexed starting at zero. unchecked { @@ -81,23 +115,24 @@ contract Conduit is ConduitInterface, TokenTransferrer { } /** - * @notice Execute a sequence of batch 1155 transfers. Only a caller with an - * open channel can call this function. + * @notice Execute a sequence of batch 1155 item transfers. Only a caller + * with an open channel can call this function. Note that channels + * are expected to implement reentrancy protection if desired, and + * that cross-channel reentrancy may be possible if the conduit has + * multiple open channels at once. Also note that channels are + * expected to implement checks against transferring any zero-amount + * items if that constraint is desired. * - * @param batchTransfers The 1155 batch transfers to perform. + * @param batchTransfers The 1155 batch item transfers to perform. * - * @return magicValue A magic value indicating that the transfers were + * @return magicValue A magic value indicating that the item transfers were * performed successfully. */ function executeBatch1155( ConduitBatch1155Transfer[] calldata batchTransfers - ) external override returns (bytes4 magicValue) { - // Ensure that the caller has an open channel. - if (!_channels[msg.sender]) { - revert ChannelClosed(); - } - - // Perform 1155 batch transfers. + ) external override onlyOpenChannel returns (bytes4 magicValue) { + // Perform 1155 batch transfers. Note that memory should be considered + // entirely corrupted from this point forward. _performERC1155BatchTransfers(batchTransfers); // Return a magic value indicating that the transfers were performed. @@ -105,34 +140,32 @@ contract Conduit is ConduitInterface, TokenTransferrer { } /** - * @notice Execute a sequence of transfers, both single and batch 1155. Only - * a caller with an open channel can call this function. + * @notice Execute a sequence of transfers, both single ERC20/721/1155 item + * transfers as well as batch 1155 item transfers. Only a caller + * with an open channel can call this function. Note that channels + * are expected to implement reentrancy protection if desired, and + * that cross-channel reentrancy may be possible if the conduit has + * multiple open channels at once. Also note that channels are + * expected to implement checks against transferring any zero-amount + * items if that constraint is desired. * - * @param standardTransfers The ERC20/721/1155 transfers to perform. - * @param batchTransfers The 1155 batch transfers to perform. + * @param standardTransfers The ERC20/721/1155 item transfers to perform. + * @param batchTransfers The 1155 batch item transfers to perform. * - * @return magicValue A magic value indicating that the transfers were + * @return magicValue A magic value indicating that the item transfers were * performed successfully. */ function executeWithBatch1155( ConduitTransfer[] calldata standardTransfers, ConduitBatch1155Transfer[] calldata batchTransfers - ) external override returns (bytes4 magicValue) { - // Ensure that the caller has an open channel. - if (!_channels[msg.sender]) { - revert ChannelClosed(); - } - + ) external override onlyOpenChannel returns (bytes4 magicValue) { // Retrieve the total number of transfers and place on the stack. uint256 totalStandardTransfers = standardTransfers.length; // Iterate over each standard transfer. for (uint256 i = 0; i < totalStandardTransfers; ) { - // Retrieve the transfer in question. - ConduitTransfer calldata standardTransfer = standardTransfers[i]; - - // Perform the transfer. - _transfer(standardTransfer); + // Retrieve the transfer in question and perform the transfer. + _transfer(standardTransfers[i]); // Skip overflow check as for loop is indexed starting at zero. unchecked { @@ -140,7 +173,9 @@ contract Conduit is ConduitInterface, TokenTransferrer { } } - // Perform 1155 batch transfers. + // Perform 1155 batch transfers. Note that memory should be considered + // entirely corrupted from this point forward aside from the free memory + // pointer having the default value. _performERC1155BatchTransfers(batchTransfers); // Return a magic value indicating that the transfers were performed. @@ -159,6 +194,11 @@ contract Conduit is ConduitInterface, TokenTransferrer { revert InvalidController(); } + // Ensure that the channel does not already have the indicated status. + if (_channels[channel] == isOpen) { + revert ChannelStatusAlreadySet(channel, isOpen); + } + // Update the status of the channel. _channels[channel] = isOpen; @@ -167,14 +207,19 @@ contract Conduit is ConduitInterface, TokenTransferrer { } /** - * @dev Internal function to transfer a given ERC20/721/1155 item. + * @dev Internal function to transfer a given ERC20/721/1155 item. Note that + * channels are expected to implement checks against transferring any + * zero-amount items if that constraint is desired. * * @param item The ERC20/721/1155 item to transfer. */ function _transfer(ConduitTransfer calldata item) internal { - // If the item type indicates Ether or a native token... + // Determine the transfer method based on the respective item type. if (item.itemType == ConduitItemType.ERC20) { - // Transfer ERC20 token. + // Transfer ERC20 token. Note that item.identifier is ignored and + // therefore ERC20 transfer items are potentially malleable — this + // check should be performed by the calling channel if a constraint + // on item malleability is desired. _performERC20Transfer(item.token, item.from, item.to, item.amount); } else if (item.itemType == ConduitItemType.ERC721) { // Ensure that exactly one 721 item is being transferred. diff --git a/contracts/conduit/ConduitController.sol b/contracts/conduit/ConduitController.sol index 70db94c9f..d3e0b711a 100644 --- a/contracts/conduit/ConduitController.sol +++ b/contracts/conduit/ConduitController.sol @@ -59,6 +59,11 @@ contract ConduitController is ConduitControllerInterface { override returns (address conduit) { + // Ensure that an initial owner has been supplied. + if (initialOwner == address(0)) { + revert InvalidInitialOwner(); + } + // If the first 20 bytes of the conduit key do not match the caller... if (address(uint160(bytes20(conduitKey))) != msg.sender) { // Revert with an error indicating that the creator is invalid. @@ -90,11 +95,14 @@ contract ConduitController is ConduitControllerInterface { // Deploy the conduit via CREATE2 using the conduit key as the salt. new Conduit{ salt: conduitKey }(); + // Initialize storage variable referencing conduit properties. + ConduitProperties storage conduitProperties = _conduits[conduit]; + // Set the supplied initial owner as the owner of the conduit. - _conduits[conduit].owner = initialOwner; + conduitProperties.owner = initialOwner; // Set conduit key used to deploy the conduit to enable reverse lookup. - _conduits[conduit].key = conduitKey; + conduitProperties.key = conduitKey; // Emit an event indicating that the conduit has been deployed. emit NewConduit(conduit, conduitKey); @@ -149,7 +157,13 @@ contract ConduitController is ConduitControllerInterface { } else if (!isOpen && channelPreviouslyOpen) { // Set a previously open channel as closed via "swap & pop" method. // Decrement located index to get the index of the closed channel. - uint256 removedChannelIndex = channelIndexPlusOne - 1; + uint256 removedChannelIndex; + + // Skip underflow check as channelPreviouslyOpen being true ensures + // that channelIndexPlusOne is nonzero. + unchecked { + removedChannelIndex = channelIndexPlusOne - 1; + } // Use length of channels array to determine index of last channel. uint256 finalChannelIndex = conduitProperties.channels.length - 1; @@ -185,6 +199,7 @@ contract ConduitController is ConduitControllerInterface { * Only the owner of the conduit in question may call this function. * * @param conduit The conduit for which to initiate ownership transfer. + * @param newPotentialOwner The new potential owner of the conduit. */ function transferOwnership(address conduit, address newPotentialOwner) external @@ -198,8 +213,13 @@ contract ConduitController is ConduitControllerInterface { revert NewPotentialOwnerIsZeroAddress(conduit); } + // Ensure the new potential owner is not already set. + if (newPotentialOwner == _conduits[conduit].potentialOwner) { + revert NewPotentialOwnerAlreadySet(conduit, newPotentialOwner); + } + // Emit an event indicating that the potential owner has been updated. - emit PotentialOwnerUpdated(conduit, newPotentialOwner); + emit PotentialOwnerUpdated(newPotentialOwner); // Set the new potential owner as the potential owner of the conduit. _conduits[conduit].potentialOwner = newPotentialOwner; @@ -215,11 +235,16 @@ contract ConduitController is ConduitControllerInterface { // Ensure the caller is the current owner of the conduit in question. _assertCallerIsConduitOwner(conduit); + // Ensure that ownership transfer is currently possible. + if (_conduits[conduit].potentialOwner == address(0)) { + revert NoPotentialOwnerCurrentlySet(conduit); + } + // Emit an event indicating that the potential owner has been cleared. - emit PotentialOwnerUpdated(conduit, address(0)); + emit PotentialOwnerUpdated(address(0)); // Clear the current new potential owner from the conduit. - delete _conduits[conduit].potentialOwner; + _conduits[conduit].potentialOwner = address(0); } /** @@ -240,10 +265,10 @@ contract ConduitController is ConduitControllerInterface { } // Emit an event indicating that the potential owner has been cleared. - emit PotentialOwnerUpdated(conduit, address(0)); + emit PotentialOwnerUpdated(address(0)); // Clear the current new potential owner from the conduit. - delete _conduits[conduit].potentialOwner; + _conduits[conduit].potentialOwner = address(0); // Emit an event indicating conduit ownership has been transferred. emit OwnershipTransferred( @@ -472,12 +497,12 @@ contract ConduitController is ConduitControllerInterface { } /** - * @dev Internal view function to revert if the caller is not the owner of a + * @dev Private view function to revert if the caller is not the owner of a * given conduit. * * @param conduit The conduit for which to assert ownership. */ - function _assertCallerIsConduitOwner(address conduit) internal view { + function _assertCallerIsConduitOwner(address conduit) private view { // Ensure that the conduit in question exists. _assertConduitExists(conduit); @@ -489,11 +514,11 @@ contract ConduitController is ConduitControllerInterface { } /** - * @dev Internal view function to revert if a given conduit does not exist. + * @dev Private view function to revert if a given conduit does not exist. * * @param conduit The conduit for which to assert existence. */ - function _assertConduitExists(address conduit) internal view { + function _assertConduitExists(address conduit) private view { // Attempt to retrieve a conduit key for the conduit in question. if (_conduits[conduit].key == bytes32(0)) { // Revert if no conduit key was located. diff --git a/contracts/conduit/lib/ConduitConstants.sol b/contracts/conduit/lib/ConduitConstants.sol new file mode 100644 index 000000000..2979289c4 --- /dev/null +++ b/contracts/conduit/lib/ConduitConstants.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.7; + +// error ChannelClosed(address channel) +uint256 constant ChannelClosed_error_signature = ( + 0x93daadf200000000000000000000000000000000000000000000000000000000 +); +uint256 constant ChannelClosed_error_ptr = 0x00; +uint256 constant ChannelClosed_channel_ptr = 0x4; +uint256 constant ChannelClosed_error_length = 0x24; + +// For the mapping: +// mapping(address => bool) channels +// The position in storage for a particular account is: +// keccak256(abi.encode(account, channels.slot)) +uint256 constant ChannelKey_channel_ptr = 0x00; +uint256 constant ChannelKey_slot_ptr = 0x20; +uint256 constant ChannelKey_length = 0x40; diff --git a/contracts/helpers/TransferHelper.sol b/contracts/helpers/TransferHelper.sol new file mode 100644 index 000000000..593fd967b --- /dev/null +++ b/contracts/helpers/TransferHelper.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.7; + +import "./TransferHelperStructs.sol"; + +import { TokenTransferrer } from "../lib/TokenTransferrer.sol"; + +import { ConduitInterface } from "../interfaces/ConduitInterface.sol"; + +// prettier-ignore +import { + ConduitControllerInterface +} from "../interfaces/ConduitControllerInterface.sol"; + +import { Conduit } from "../conduit/Conduit.sol"; + +import { ConduitTransfer } from "../conduit/lib/ConduitStructs.sol"; + +// prettier-ignore +import { + TransferHelperInterface +} from "../interfaces/TransferHelperInterface.sol"; + +/** + * @title TransferHelper + * @author stuckinaboot, stephankmin + * @notice TransferHelper is a utility contract for transferring + * ERC20/ERC721/ERC1155 items in bulk to a specific recipient. + */ +contract TransferHelper is TransferHelperInterface, TokenTransferrer { + // Allow for interaction with the conduit controller. + ConduitControllerInterface internal immutable _CONDUIT_CONTROLLER; + + // Cache the conduit creation hash used by the conduit controller. + bytes32 internal immutable _CONDUIT_CREATION_CODE_HASH; + + /** + * @dev Set the supplied conduit controller and retrieve its + * conduit creation code hash. + * + * + * @param conduitController A contract that deploys conduits, or proxies + * that may optionally be used to transfer approved + * ERC20/721/1155 tokens. + */ + constructor(address conduitController) { + // Get the conduit creation code hash from the supplied conduit + // controller and set it as an immutable. + ConduitControllerInterface controller = ConduitControllerInterface( + conduitController + ); + (_CONDUIT_CREATION_CODE_HASH, ) = controller.getConduitCodeHashes(); + + // Set the supplied conduit controller as an immutable. + _CONDUIT_CONTROLLER = controller; + } + + /** + * @notice Transfer multiple items to a single recipient. + * + * @param items The items to transfer. + * @param recipient The address the items should be transferred to. + * @param conduitKey The key of the conduit through which the bulk transfer + * should occur. + * + * @return magicValue A value indicating that the transfers were successful. + */ + function bulkTransfer( + TransferHelperItem[] calldata items, + address recipient, + bytes32 conduitKey + ) external override returns (bytes4 magicValue) { + // Retrieve total number of transfers and place on stack. + uint256 totalTransfers = items.length; + + // If no conduitKey is given, use TokenTransferrer to perform transfers. + if (conduitKey == bytes32(0)) { + // Skip overflow checks: all for loops are indexed starting at zero. + unchecked { + // Iterate over each transfer. + for (uint256 i = 0; i < totalTransfers; ++i) { + // Retrieve the transfer in question. + TransferHelperItem calldata item = items[i]; + + // Perform a transfer based on the transfer's item type. + // Revert if item being transferred is a native token. + if (item.itemType == ConduitItemType.NATIVE) { + revert InvalidItemType(); + } else if (item.itemType == ConduitItemType.ERC20) { + _performERC20Transfer( + item.token, + msg.sender, + recipient, + item.amount + ); + } else if (item.itemType == ConduitItemType.ERC721) { + _performERC721Transfer( + item.token, + msg.sender, + recipient, + item.identifier + ); + } else { + _performERC1155Transfer( + item.token, + msg.sender, + recipient, + item.identifier, + item.amount + ); + } + } + } + } + // Otherwise, a conduitKey was provided. + else { + // Derive the conduit address from the deployer, conduit key + // and creation code hash. + address conduit = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + address(_CONDUIT_CONTROLLER), + conduitKey, + _CONDUIT_CREATION_CODE_HASH + ) + ) + ) + ) + ); + + // Declare a new array to populate with each token transfer. + ConduitTransfer[] memory conduitTransfers = new ConduitTransfer[]( + totalTransfers + ); + + // Skip overflow checks: all for loops are indexed starting at zero. + unchecked { + // Iterate over each transfer. + for (uint256 i = 0; i < totalTransfers; ++i) { + // Retrieve the transfer in question. + TransferHelperItem calldata item = items[i]; + + // Create a ConduitTransfer corresponding to each + // TransferHelperItem. + conduitTransfers[i] = ConduitTransfer( + item.itemType, + item.token, + msg.sender, + recipient, + item.identifier, + item.amount + ); + } + } + + // Call the conduit and execute bulk transfers. + ConduitInterface(conduit).execute(conduitTransfers); + } + + // Return a magic value indicating that the transfers were performed. + magicValue = this.bulkTransfer.selector; + } +} diff --git a/contracts/helpers/TransferHelperStructs.sol b/contracts/helpers/TransferHelperStructs.sol new file mode 100644 index 000000000..35aeec140 --- /dev/null +++ b/contracts/helpers/TransferHelperStructs.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.7; + +import { ConduitItemType } from "../conduit/lib/ConduitEnums.sol"; + +struct TransferHelperItem { + ConduitItemType itemType; + address token; + uint256 identifier; + uint256 amount; +} diff --git a/contracts/interfaces/ConduitControllerInterface.sol b/contracts/interfaces/ConduitControllerInterface.sol index 01ec9a797..83561ae54 100644 --- a/contracts/interfaces/ConduitControllerInterface.sol +++ b/contracts/interfaces/ConduitControllerInterface.sol @@ -46,22 +46,38 @@ interface ConduitControllerInterface { * @dev Emit an event whenever a conduit owner registers a new potential * owner for that conduit. * - * @param conduit The conduit for which ownership may now be - * transferred. * @param newPotentialOwner The new potential owner of the conduit. */ - event PotentialOwnerUpdated( - address indexed conduit, - address indexed newPotentialOwner - ); + event PotentialOwnerUpdated(address indexed newPotentialOwner); /** * @dev Revert with an error when attempting to create a new conduit using a - * conduit key where the last twenty bytes of the key do not match the + * conduit key where the first twenty bytes of the key do not match the * address of the caller. */ error InvalidCreator(); + /** + * @dev Revert with an error when attempting to create a new conduit when no + * initial owner address is supplied. + */ + error InvalidInitialOwner(); + + /** + * @dev Revert with an error when attempting to set a new potential owner + * that is already set. + */ + error NewPotentialOwnerAlreadySet( + address conduit, + address newPotentialOwner + ); + + /** + * @dev Revert with an error when attempting to cancel ownership transfer + * when no new potential owner is currently set. + */ + error NoPotentialOwnerCurrentlySet(address conduit); + /** * @dev Revert with an error when attempting to interact with a conduit that * does not yet exist. @@ -102,13 +118,13 @@ interface ConduitControllerInterface { /** * @notice Deploy a new conduit using a supplied conduit key and assigning - * an initial owner for the deployed conduit. Note that the last + * an initial owner for the deployed conduit. Note that the first * twenty bytes of the supplied conduit key must match the caller * and that a new conduit cannot be created if one has already been * deployed using the same conduit key. * * @param conduitKey The conduit key used to deploy the conduit. Note that - * the last twenty bytes of the conduit key must match + * the first twenty bytes of the conduit key must match * the caller of this contract. * @param initialOwner The initial owner to set for the new conduit. * @@ -143,6 +159,7 @@ interface ConduitControllerInterface { * Only the owner of the conduit in question may call this function. * * @param conduit The conduit for which to initiate ownership transfer. + * @param newPotentialOwner The new potential owner of the conduit. */ function transferOwnership(address conduit, address newPotentialOwner) external; diff --git a/contracts/interfaces/ConduitInterface.sol b/contracts/interfaces/ConduitInterface.sol index 2feeb4e01..d988fcc83 100644 --- a/contracts/interfaces/ConduitInterface.sol +++ b/contracts/interfaces/ConduitInterface.sol @@ -18,7 +18,13 @@ interface ConduitInterface { * @dev Revert with an error when attempting to execute transfers using a * caller that does not have an open channel. */ - error ChannelClosed(); + error ChannelClosed(address channel); + + /** + * @dev Revert with an error when attempting to update a channel to the + * current status of that channel. + */ + error ChannelStatusAlreadySet(address channel, bool isOpen); /** * @dev Revert with an error when attempting to execute a transfer for an @@ -32,20 +38,13 @@ interface ConduitInterface { */ error InvalidController(); - /** - * @dev Revert with an error when attempting to execute an 1155 batch - * transfer using calldata not produced by default ABI encoding or with - * different lengths for ids and amounts arrays. - */ - error Invalid1155BatchTransferEncoding(); - /** * @dev Emit an event whenever a channel is opened or closed. * * @param channel The channel that has been updated. * @param open A boolean indicating whether the conduit is open or not. */ - event ChannelUpdated(address channel, bool open); + event ChannelUpdated(address indexed channel, bool open); /** * @notice Execute a sequence of ERC20/721/1155 transfers. Only a caller diff --git a/contracts/interfaces/ConsiderationEventsAndErrors.sol b/contracts/interfaces/ConsiderationEventsAndErrors.sol index 852910f36..13682ecda 100644 --- a/contracts/interfaces/ConsiderationEventsAndErrors.sol +++ b/contracts/interfaces/ConsiderationEventsAndErrors.sol @@ -15,9 +15,11 @@ interface ConsiderationEventsAndErrors { * @param orderHash The hash of the fulfilled order. * @param offerer The offerer of the fulfilled order. * @param zone The zone of the fulfilled order. - * @param fulfiller The fulfiller of the order, or the null address if - * there is no specific fulfiller (i.e. the order is - * part of a group of orders). + * @param recipient The recipient of each spent item on the fulfilled + * order, or the null address if there is no specific + * fulfiller (i.e. the order is part of a group of + * orders). Defaults to the caller unless explicitly + * specified otherwise by the fulfiller. * @param offer The offer items spent as part of the order. * @param consideration The consideration items received as part of the * order along with the recipients of each item. @@ -26,7 +28,7 @@ interface ConsiderationEventsAndErrors { bytes32 orderHash, address indexed offerer, address indexed zone, - address fulfiller, + address recipient, SpentItem[] offer, ReceivedItem[] consideration ); @@ -60,12 +62,12 @@ interface ConsiderationEventsAndErrors { ); /** - * @dev Emit an event whenever a nonce for a given offerer is incremented. + * @dev Emit an event whenever a counter for a given offerer is incremented. * - * @param newNonce The new nonce for the offerer. + * @param newCounter The new counter for the offerer. * @param offerer The offerer in question. */ - event NonceIncremented(uint256 newNonce, address indexed offerer); + event CounterIncremented(uint256 newCounter, address indexed offerer); /** * @dev Revert with an error when attempting to fill an order that has @@ -179,4 +181,10 @@ interface ConsiderationEventsAndErrors { * available orders when none are fulfillable. */ error NoSpecifiedOrdersAvailable(); + + /** + * @dev Revert with an error when attempting to fulfill an order with an + * offer for ETH outside of matching orders. + */ + error InvalidNativeOfferItem(); } diff --git a/contracts/interfaces/ConsiderationInterface.sol b/contracts/interfaces/ConsiderationInterface.sol index fd10fa831..1c6d5c9fc 100644 --- a/contracts/interfaces/ConsiderationInterface.sol +++ b/contracts/interfaces/ConsiderationInterface.sol @@ -17,7 +17,7 @@ import { /** * @title ConsiderationInterface * @author 0age - * @custom:version 1 + * @custom:version 1.1 * @notice Consideration is a generalized ETH/ERC20/ERC721/ERC1155 marketplace. * It minimizes external calls to the greatest extent possible and * provides lightweight methods for common routes as well as more @@ -99,7 +99,7 @@ interface ConsiderationInterface { * contained in the merkle root held by the item * in question's criteria element. Note that an * empty criteria indicates that any - * (transferrable) token identifier on the token + * (transferable) token identifier on the token * in question is valid and that no associated * proof needs to be supplied. * @param fulfillerConduitKey A bytes32 value indicating what conduit, if @@ -107,6 +107,9 @@ interface ConsiderationInterface { * from. The zero hash signifies that no conduit * should be used, with direct approvals set on * Consideration. + * @param recipient The intended recipient for all received items, + * with `address(0)` indicating that the caller + * should receive the items. * * @return fulfilled A boolean indicating whether the order has been * successfully fulfilled. @@ -114,7 +117,8 @@ interface ConsiderationInterface { function fulfillAdvancedOrder( AdvancedOrder calldata advancedOrder, CriteriaResolver[] calldata criteriaResolvers, - bytes32 fulfillerConduitKey + bytes32 fulfillerConduitKey, + address recipient ) external payable returns (bool fulfilled); /** @@ -207,7 +211,7 @@ interface ConsiderationInterface { * is contained in the merkle root held by * the item in question's criteria element. * Note that an empty criteria indicates - * that any (transferrable) token + * that any (transferable) token * identifier on the token in question is * valid and that no associated proof needs * to be supplied. @@ -223,6 +227,9 @@ interface ConsiderationInterface { * approvals from. The zero hash signifies * that no conduit should be used, with * direct approvals set on this contract. + * @param recipient The intended recipient for all received + * items, with `address(0)` indicating that + * the caller should receive the items. * @param maximumFulfilled The maximum number of orders to fulfill. * * @return availableOrders An array of booleans indicating if each order @@ -238,6 +245,7 @@ interface ConsiderationInterface { FulfillmentComponent[][] calldata offerFulfillments, FulfillmentComponent[][] calldata considerationFulfillments, bytes32 fulfillerConduitKey, + address recipient, uint256 maximumFulfilled ) external @@ -285,7 +293,7 @@ interface ConsiderationInterface { * indicated by the order) to transfer any relevant * tokens on their behalf and each consideration * recipient must implement `onERC1155Received` in - * order toreceive ERC1155 tokens. Also note that + * order to receive ERC1155 tokens. Also note that * the offer and consideration components for each * order must have no remainder after multiplying * the respective amount with the supplied fraction @@ -296,7 +304,7 @@ interface ConsiderationInterface { * offer or consideration, a token identifier, and * a proof that the supplied token identifier is * contained in the order's merkle root. Note that - * an empty root indicates that any (transferrable) + * an empty root indicates that any (transferable) * token identifier is valid and that no associated * proof needs to be supplied. * @param fulfillments An array of elements allocating offer components @@ -350,12 +358,12 @@ interface ConsiderationInterface { /** * @notice Cancel all orders from a given offerer with a given zone in bulk - * by incrementing a nonce. Note that only the offerer may increment - * the nonce. + * by incrementing a counter. Note that only the offerer may + * increment the counter. * - * @return newNonce The new nonce. + * @return newCounter The new counter. */ - function incrementNonce() external returns (uint256 newNonce); + function incrementCounter() external returns (uint256 newCounter); /** * @notice Retrieve the order hash for a given order. @@ -397,13 +405,16 @@ interface ConsiderationInterface { ); /** - * @notice Retrieve the current nonce for a given offerer. + * @notice Retrieve the current counter for a given offerer. * * @param offerer The offerer in question. * - * @return nonce The current nonce. + * @return counter The current counter. */ - function getNonce(address offerer) external view returns (uint256 nonce); + function getCounter(address offerer) + external + view + returns (uint256 counter); /** * @notice Retrieve configuration information for this contract. diff --git a/contracts/interfaces/FulfillmentApplicationErrors.sol b/contracts/interfaces/FulfillmentApplicationErrors.sol index 7125c299c..776a0f6be 100644 --- a/contracts/interfaces/FulfillmentApplicationErrors.sol +++ b/contracts/interfaces/FulfillmentApplicationErrors.sol @@ -11,9 +11,9 @@ import { Side } from "../lib/ConsiderationEnums.sol"; */ interface FulfillmentApplicationErrors { /** - * @dev Revert with an error when a fulfillment is provided as part of an - * call to fulfill available orders that does not declare at least one - * component. + * @dev Revert with an error when a fulfillment is provided that does not + * declare at least one component as part of a call to fulfill + * available orders. */ error MissingFulfillmentComponentOnAggregation(Side side); diff --git a/contracts/interfaces/SeaportInterface.sol b/contracts/interfaces/SeaportInterface.sol new file mode 100644 index 000000000..6593f8658 --- /dev/null +++ b/contracts/interfaces/SeaportInterface.sol @@ -0,0 +1,440 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.7; + +// prettier-ignore +import { + BasicOrderParameters, + OrderComponents, + Fulfillment, + FulfillmentComponent, + Execution, + Order, + AdvancedOrder, + OrderStatus, + CriteriaResolver +} from "../lib/ConsiderationStructs.sol"; + +/** + * @title SeaportInterface + * @author 0age + * @custom:version 1.1 + * @notice Seaport is a generalized ETH/ERC20/ERC721/ERC1155 marketplace. It + * minimizes external calls to the greatest extent possible and provides + * lightweight methods for common routes as well as more flexible + * methods for composing advanced orders. + * + * @dev SeaportInterface contains all external function interfaces for Seaport. + */ +interface SeaportInterface { + /** + * @notice Fulfill an order offering an ERC721 token by supplying Ether (or + * the native token for the given chain) as consideration for the + * order. An arbitrary number of "additional recipients" may also be + * supplied which will each receive native tokens from the fulfiller + * as consideration. + * + * @param parameters Additional information on the fulfilled order. Note + * that the offerer must first approve this contract (or + * their preferred conduit if indicated by the order) for + * their offered ERC721 token to be transferred. + * + * @return fulfilled A boolean indicating whether the order has been + * successfully fulfilled. + */ + function fulfillBasicOrder(BasicOrderParameters calldata parameters) + external + payable + returns (bool fulfilled); + + /** + * @notice Fulfill an order with an arbitrary number of items for offer and + * consideration. Note that this function does not support + * criteria-based orders or partial filling of orders (though + * filling the remainder of a partially-filled order is supported). + * + * @param order The order to fulfill. Note that both the + * offerer and the fulfiller must first approve + * this contract (or the corresponding conduit if + * indicated) to transfer any relevant tokens on + * their behalf and that contracts must implement + * `onERC1155Received` to receive ERC1155 tokens + * as consideration. + * @param fulfillerConduitKey A bytes32 value indicating what conduit, if + * any, to source the fulfiller's token approvals + * from. The zero hash signifies that no conduit + * should be used, with direct approvals set on + * Seaport. + * + * @return fulfilled A boolean indicating whether the order has been + * successfully fulfilled. + */ + function fulfillOrder(Order calldata order, bytes32 fulfillerConduitKey) + external + payable + returns (bool fulfilled); + + /** + * @notice Fill an order, fully or partially, with an arbitrary number of + * items for offer and consideration alongside criteria resolvers + * containing specific token identifiers and associated proofs. + * + * @param advancedOrder The order to fulfill along with the fraction + * of the order to attempt to fill. Note that + * both the offerer and the fulfiller must first + * approve this contract (or their preferred + * conduit if indicated by the order) to transfer + * any relevant tokens on their behalf and that + * contracts must implement `onERC1155Received` + * to receive ERC1155 tokens as consideration. + * Also note that all offer and consideration + * components must have no remainder after + * multiplication of the respective amount with + * the supplied fraction for the partial fill to + * be considered valid. + * @param criteriaResolvers An array where each element contains a + * reference to a specific offer or + * consideration, a token identifier, and a proof + * that the supplied token identifier is + * contained in the merkle root held by the item + * in question's criteria element. Note that an + * empty criteria indicates that any + * (transferable) token identifier on the token + * in question is valid and that no associated + * proof needs to be supplied. + * @param fulfillerConduitKey A bytes32 value indicating what conduit, if + * any, to source the fulfiller's token approvals + * from. The zero hash signifies that no conduit + * should be used, with direct approvals set on + * Seaport. + * @param recipient The intended recipient for all received items, + * with `address(0)` indicating that the caller + * should receive the items. + * + * @return fulfilled A boolean indicating whether the order has been + * successfully fulfilled. + */ + function fulfillAdvancedOrder( + AdvancedOrder calldata advancedOrder, + CriteriaResolver[] calldata criteriaResolvers, + bytes32 fulfillerConduitKey, + address recipient + ) external payable returns (bool fulfilled); + + /** + * @notice Attempt to fill a group of orders, each with an arbitrary number + * of items for offer and consideration. Any order that is not + * currently active, has already been fully filled, or has been + * cancelled will be omitted. Remaining offer and consideration + * items will then be aggregated where possible as indicated by the + * supplied offer and consideration component arrays and aggregated + * items will be transferred to the fulfiller or to each intended + * recipient, respectively. Note that a failing item transfer or an + * issue with order formatting will cause the entire batch to fail. + * Note that this function does not support criteria-based orders or + * partial filling of orders (though filling the remainder of a + * partially-filled order is supported). + * + * @param orders The orders to fulfill. Note that both + * the offerer and the fulfiller must first + * approve this contract (or the + * corresponding conduit if indicated) to + * transfer any relevant tokens on their + * behalf and that contracts must implement + * `onERC1155Received` to receive ERC1155 + * tokens as consideration. + * @param offerFulfillments An array of FulfillmentComponent arrays + * indicating which offer items to attempt + * to aggregate when preparing executions. + * @param considerationFulfillments An array of FulfillmentComponent arrays + * indicating which consideration items to + * attempt to aggregate when preparing + * executions. + * @param fulfillerConduitKey A bytes32 value indicating what conduit, + * if any, to source the fulfiller's token + * approvals from. The zero hash signifies + * that no conduit should be used, with + * direct approvals set on this contract. + * @param maximumFulfilled The maximum number of orders to fulfill. + * + * @return availableOrders An array of booleans indicating if each order + * with an index corresponding to the index of the + * returned boolean was fulfillable or not. + * @return executions An array of elements indicating the sequence of + * transfers performed as part of matching the given + * orders. + */ + function fulfillAvailableOrders( + Order[] calldata orders, + FulfillmentComponent[][] calldata offerFulfillments, + FulfillmentComponent[][] calldata considerationFulfillments, + bytes32 fulfillerConduitKey, + uint256 maximumFulfilled + ) + external + payable + returns (bool[] memory availableOrders, Execution[] memory executions); + + /** + * @notice Attempt to fill a group of orders, fully or partially, with an + * arbitrary number of items for offer and consideration per order + * alongside criteria resolvers containing specific token + * identifiers and associated proofs. Any order that is not + * currently active, has already been fully filled, or has been + * cancelled will be omitted. Remaining offer and consideration + * items will then be aggregated where possible as indicated by the + * supplied offer and consideration component arrays and aggregated + * items will be transferred to the fulfiller or to each intended + * recipient, respectively. Note that a failing item transfer or an + * issue with order formatting will cause the entire batch to fail. + * + * @param advancedOrders The orders to fulfill along with the + * fraction of those orders to attempt to + * fill. Note that both the offerer and the + * fulfiller must first approve this + * contract (or their preferred conduit if + * indicated by the order) to transfer any + * relevant tokens on their behalf and that + * contracts must implement + * `onERC1155Received` to enable receipt of + * ERC1155 tokens as consideration. Also + * note that all offer and consideration + * components must have no remainder after + * multiplication of the respective amount + * with the supplied fraction for an + * order's partial fill amount to be + * considered valid. + * @param criteriaResolvers An array where each element contains a + * reference to a specific offer or + * consideration, a token identifier, and a + * proof that the supplied token identifier + * is contained in the merkle root held by + * the item in question's criteria element. + * Note that an empty criteria indicates + * that any (transferable) token + * identifier on the token in question is + * valid and that no associated proof needs + * to be supplied. + * @param offerFulfillments An array of FulfillmentComponent arrays + * indicating which offer items to attempt + * to aggregate when preparing executions. + * @param considerationFulfillments An array of FulfillmentComponent arrays + * indicating which consideration items to + * attempt to aggregate when preparing + * executions. + * @param fulfillerConduitKey A bytes32 value indicating what conduit, + * if any, to source the fulfiller's token + * approvals from. The zero hash signifies + * that no conduit should be used, with + * direct approvals set on this contract. + * @param recipient The intended recipient for all received + * items, with `address(0)` indicating that + * the caller should receive the items. + * @param maximumFulfilled The maximum number of orders to fulfill. + * + * @return availableOrders An array of booleans indicating if each order + * with an index corresponding to the index of the + * returned boolean was fulfillable or not. + * @return executions An array of elements indicating the sequence of + * transfers performed as part of matching the given + * orders. + */ + function fulfillAvailableAdvancedOrders( + AdvancedOrder[] calldata advancedOrders, + CriteriaResolver[] calldata criteriaResolvers, + FulfillmentComponent[][] calldata offerFulfillments, + FulfillmentComponent[][] calldata considerationFulfillments, + bytes32 fulfillerConduitKey, + address recipient, + uint256 maximumFulfilled + ) + external + payable + returns (bool[] memory availableOrders, Execution[] memory executions); + + /** + * @notice Match an arbitrary number of orders, each with an arbitrary + * number of items for offer and consideration along with as set of + * fulfillments allocating offer components to consideration + * components. Note that this function does not support + * criteria-based or partial filling of orders (though filling the + * remainder of a partially-filled order is supported). + * + * @param orders The orders to match. Note that both the offerer and + * fulfiller on each order must first approve this + * contract (or their conduit if indicated by the order) + * to transfer any relevant tokens on their behalf and + * each consideration recipient must implement + * `onERC1155Received` to enable ERC1155 token receipt. + * @param fulfillments An array of elements allocating offer components to + * consideration components. Note that each + * consideration component must be fully met for the + * match operation to be valid. + * + * @return executions An array of elements indicating the sequence of + * transfers performed as part of matching the given + * orders. + */ + function matchOrders( + Order[] calldata orders, + Fulfillment[] calldata fulfillments + ) external payable returns (Execution[] memory executions); + + /** + * @notice Match an arbitrary number of full or partial orders, each with an + * arbitrary number of items for offer and consideration, supplying + * criteria resolvers containing specific token identifiers and + * associated proofs as well as fulfillments allocating offer + * components to consideration components. + * + * @param orders The advanced orders to match. Note that both the + * offerer and fulfiller on each order must first + * approve this contract (or a preferred conduit if + * indicated by the order) to transfer any relevant + * tokens on their behalf and each consideration + * recipient must implement `onERC1155Received` in + * order to receive ERC1155 tokens. Also note that + * the offer and consideration components for each + * order must have no remainder after multiplying + * the respective amount with the supplied fraction + * in order for the group of partial fills to be + * considered valid. + * @param criteriaResolvers An array where each element contains a reference + * to a specific order as well as that order's + * offer or consideration, a token identifier, and + * a proof that the supplied token identifier is + * contained in the order's merkle root. Note that + * an empty root indicates that any (transferable) + * token identifier is valid and that no associated + * proof needs to be supplied. + * @param fulfillments An array of elements allocating offer components + * to consideration components. Note that each + * consideration component must be fully met in + * order for the match operation to be valid. + * + * @return executions An array of elements indicating the sequence of + * transfers performed as part of matching the given + * orders. + */ + function matchAdvancedOrders( + AdvancedOrder[] calldata orders, + CriteriaResolver[] calldata criteriaResolvers, + Fulfillment[] calldata fulfillments + ) external payable returns (Execution[] memory executions); + + /** + * @notice Cancel an arbitrary number of orders. Note that only the offerer + * or the zone of a given order may cancel it. Callers should ensure + * that the intended order was cancelled by calling `getOrderStatus` + * and confirming that `isCancelled` returns `true`. + * + * @param orders The orders to cancel. + * + * @return cancelled A boolean indicating whether the supplied orders have + * been successfully cancelled. + */ + function cancel(OrderComponents[] calldata orders) + external + returns (bool cancelled); + + /** + * @notice Validate an arbitrary number of orders, thereby registering their + * signatures as valid and allowing the fulfiller to skip signature + * verification on fulfillment. Note that validated orders may still + * be unfulfillable due to invalid item amounts or other factors; + * callers should determine whether validated orders are fulfillable + * by simulating the fulfillment call prior to execution. Also note + * that anyone can validate a signed order, but only the offerer can + * validate an order without supplying a signature. + * + * @param orders The orders to validate. + * + * @return validated A boolean indicating whether the supplied orders have + * been successfully validated. + */ + function validate(Order[] calldata orders) + external + returns (bool validated); + + /** + * @notice Cancel all orders from a given offerer with a given zone in bulk + * by incrementing a counter. Note that only the offerer may + * increment the counter. + * + * @return newCounter The new counter. + */ + function incrementCounter() external returns (uint256 newCounter); + + /** + * @notice Retrieve the order hash for a given order. + * + * @param order The components of the order. + * + * @return orderHash The order hash. + */ + function getOrderHash(OrderComponents calldata order) + external + view + returns (bytes32 orderHash); + + /** + * @notice Retrieve the status of a given order by hash, including whether + * the order has been cancelled or validated and the fraction of the + * order that has been filled. + * + * @param orderHash The order hash in question. + * + * @return isValidated A boolean indicating whether the order in question + * has been validated (i.e. previously approved or + * partially filled). + * @return isCancelled A boolean indicating whether the order in question + * has been cancelled. + * @return totalFilled The total portion of the order that has been filled + * (i.e. the "numerator"). + * @return totalSize The total size of the order that is either filled or + * unfilled (i.e. the "denominator"). + */ + function getOrderStatus(bytes32 orderHash) + external + view + returns ( + bool isValidated, + bool isCancelled, + uint256 totalFilled, + uint256 totalSize + ); + + /** + * @notice Retrieve the current counter for a given offerer. + * + * @param offerer The offerer in question. + * + * @return counter The current counter. + */ + function getCounter(address offerer) + external + view + returns (uint256 counter); + + /** + * @notice Retrieve configuration information for this contract. + * + * @return version The contract version. + * @return domainSeparator The domain separator for this contract. + * @return conduitController The conduit Controller set for this contract. + */ + function information() + external + view + returns ( + string memory version, + bytes32 domainSeparator, + address conduitController + ); + + /** + * @notice Retrieve the name of this contract. + * + * @return contractName The name of this contract. + */ + function name() external view returns (string memory contractName); +} diff --git a/contracts/interfaces/TokenTransferrerErrors.sol b/contracts/interfaces/TokenTransferrerErrors.sol index 1e88e3b10..21887650b 100644 --- a/contracts/interfaces/TokenTransferrerErrors.sol +++ b/contracts/interfaces/TokenTransferrerErrors.sol @@ -17,6 +17,16 @@ interface TokenTransferrerErrors { */ error MissingItemAmount(); + /** + * @dev Revert with an error when attempting to fulfill an order where an + * item has unused parameters. This includes both the token and the + * identifier parameters for native transfers as well as the identifier + * parameter for ERC20 transfers. Note that the conduit does not + * perform this check, leaving it up to the calling channel to enforce + * when desired. + */ + error UnusedItemParameters(); + /** * @dev Revert with an error when an ERC20, ERC721, or ERC1155 token * transfer reverts. @@ -75,4 +85,11 @@ interface TokenTransferrerErrors { * @param account The account that should contain code. */ error NoContract(address account); + + /** + * @dev Revert with an error when attempting to execute an 1155 batch + * transfer using calldata not produced by default ABI encoding or with + * different lengths for ids and amounts arrays. + */ + error Invalid1155BatchTransferEncoding(); } diff --git a/contracts/interfaces/TransferHelperInterface.sol b/contracts/interfaces/TransferHelperInterface.sol new file mode 100644 index 000000000..c579868fd --- /dev/null +++ b/contracts/interfaces/TransferHelperInterface.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.7; + +import { TransferHelperItem } from "../helpers/TransferHelperStructs.sol"; + +interface TransferHelperInterface { + /** + * @dev Revert with an error when attempting to execute transfers with a + * NATIVE itemType. + */ + error InvalidItemType(); + + /** + * @notice Transfer multiple items to a single recipient. + * + * @param items The items to transfer. + * @param recipient The address the items should be transferred to. + * @param conduitKey The key of the conduit performing the bulk transfer. + */ + function bulkTransfer( + TransferHelperItem[] calldata items, + address recipient, + bytes32 conduitKey + ) external returns (bytes4); +} diff --git a/contracts/interfaces/ZoneInteractionErrors.sol b/contracts/interfaces/ZoneInteractionErrors.sol index 71551aec5..f7b271c4c 100644 --- a/contracts/interfaces/ZoneInteractionErrors.sol +++ b/contracts/interfaces/ZoneInteractionErrors.sol @@ -10,7 +10,7 @@ interface ZoneInteractionErrors { /** * @dev Revert with an error when attempting to fill an order that specifies * a restricted submitter as its order type when not submitted by - * either the offerrer or the order's zone or approved as valid by the + * either the offerer or the order's zone or approved as valid by the * zone in question via a staticcall to `isValidOrder`. * * @param orderHash The order hash for the invalid restricted order. diff --git a/contracts/lib/AmountDeriver.sol b/contracts/lib/AmountDeriver.sol index 25dff2c89..f35b2cb73 100644 --- a/contracts/lib/AmountDeriver.sol +++ b/contracts/lib/AmountDeriver.sol @@ -1,73 +1,94 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; // prettier-ignore import { AmountDerivationErrors } from "../interfaces/AmountDerivationErrors.sol"; +import "./ConsiderationConstants.sol"; + /** * @title AmountDeriver * @author 0age - * @notice AmountDeriver contains pure functions related to deriving item - * amounts based on partial fill quantity and on linear extrapolation - * based on current time when the start amount and end amount differ. + * @notice AmountDeriver contains view and pure functions related to deriving + * item amounts based on partial fill quantity and on linear + * interpolation based on current time when the start amount and end + * amount differ. */ contract AmountDeriver is AmountDerivationErrors { /** - * @dev Internal pure function to derive the current amount of a given item + * @dev Internal view function to derive the current amount of a given item * based on the current price, the starting price, and the ending * price. If the start and end prices differ, the current price will be - * extrapolated on a linear basis. + * interpolated on a linear basis. Note that this function expects that + * the startTime parameter of orderParameters is not greater than the + * current block timestamp and that the endTime parameter is greater + * than the current block timestamp. If this condition is not upheld, + * duration / elapsed / remaining variables will underflow. * * @param startAmount The starting amount of the item. * @param endAmount The ending amount of the item. - * @param elapsed The time elapsed since the order's start time. - * @param remaining The time left until the order's end time. - * @param duration The total duration of the order. + * @param startTime The starting time of the order. + * @param endTime The end time of the order. * @param roundUp A boolean indicating whether the resultant amount * should be rounded up or down. * - * @return The current amount. + * @return amount The current amount. */ function _locateCurrentAmount( uint256 startAmount, uint256 endAmount, - uint256 elapsed, - uint256 remaining, - uint256 duration, + uint256 startTime, + uint256 endTime, bool roundUp - ) internal pure returns (uint256) { + ) internal view returns (uint256 amount) { // Only modify end amount if it doesn't already equal start amount. if (startAmount != endAmount) { - // Leave extra amount to add for rounding at zero (i.e. round down). - uint256 extraCeiling = 0; + // Declare variables to derive in the subsequent unchecked scope. + uint256 duration; + uint256 elapsed; + uint256 remaining; + + // Skip underflow checks as startTime <= block.timestamp < endTime. + unchecked { + // Derive the duration for the order and place it on the stack. + duration = endTime - startTime; - // If rounding up, set rounding factor to one less than denominator. - if (roundUp) { - // Skip underflow check: duration cannot be zero. - unchecked { - extraCeiling = duration - 1; - } + // Derive time elapsed since the order started & place on stack. + elapsed = block.timestamp - startTime; + + // Derive time remaining until order expires and place on stack. + remaining = duration - elapsed; } - // Aggregate new amounts weighted by time with rounding factor - // prettier-ignore - uint256 totalBeforeDivision = ( - (startAmount * remaining) + (endAmount * elapsed) + extraCeiling - ); + // Aggregate new amounts weighted by time with rounding factor. + uint256 totalBeforeDivision = ((startAmount * remaining) + + (endAmount * elapsed)); - // Division performed with no zero check as duration cannot be zero. - uint256 newAmount; + // Use assembly to combine operations and skip divide-by-zero check. assembly { - newAmount := div(totalBeforeDivision, duration) + // Multiply by iszero(iszero(totalBeforeDivision)) to ensure + // amount is set to zero if totalBeforeDivision is zero, + // as intermediate overflow can occur if it is zero. + amount := mul( + iszero(iszero(totalBeforeDivision)), + // Subtract 1 from the numerator and add 1 to the result if + // roundUp is true to get the proper rounding direction. + // Division is performed with no zero check as duration + // cannot be zero as long as startTime < endTime. + add( + div(sub(totalBeforeDivision, roundUp), duration), + roundUp + ) + ) } - // Return the current amount (expressed as endAmount internally). - return newAmount; + // Return the current amount. + return amount; } - // Return the original amount (now expressed as endAmount internally). + // Return the original amount as startAmount == endAmount. return endAmount; } @@ -98,16 +119,13 @@ contract AmountDeriver is AmountDerivationErrors { // Ensure fraction can be applied to the value with no remainder. Note // that the denominator cannot be zero. - bool exact; assembly { // Ensure new value contains no remainder via mulmod operator. // Credit to @hrkrshnn + @axic for proposing this optimal solution. - exact := iszero(mulmod(value, numerator, denominator)) - } - - // Ensure that division gave a final result with no remainder. - if (!exact) { - revert InexactFraction(); + if mulmod(value, numerator, denominator) { + mstore(0, InexactFraction_error_signature) + revert(0, InexactFraction_error_len) + } } // Multiply the numerator by the value and ensure no overflow occurs. @@ -121,7 +139,7 @@ contract AmountDeriver is AmountDerivationErrors { } /** - * @dev Internal pure function to apply a fraction to a consideration + * @dev Internal view function to apply a fraction to a consideration * or offer item. * * @param startAmount The starting amount of the item. @@ -129,9 +147,10 @@ contract AmountDeriver is AmountDerivationErrors { * @param numerator A value indicating the portion of the order that * should be filled. * @param denominator A value indicating the total size of the order. - * @param elapsed The time elapsed since the order's start time. - * @param remaining The time left until the order's end time. - * @param duration The total duration of the order. + * @param startTime The starting time of the order. + * @param endTime The end time of the order. + * @param roundUp A boolean indicating whether the resultant + * amount should be rounded up or down. * * @return amount The received item to transfer with the final amount. */ @@ -140,23 +159,21 @@ contract AmountDeriver is AmountDerivationErrors { uint256 endAmount, uint256 numerator, uint256 denominator, - uint256 elapsed, - uint256 remaining, - uint256 duration, + uint256 startTime, + uint256 endTime, bool roundUp - ) internal pure returns (uint256 amount) { + ) internal view returns (uint256 amount) { // If start amount equals end amount, apply fraction to end amount. if (startAmount == endAmount) { // Apply fraction to end amount. amount = _getFraction(numerator, denominator, endAmount); } else { - // Otherwise, apply fraction to both and extrapolate final amount. + // Otherwise, apply fraction to both and interpolated final amount. amount = _locateCurrentAmount( _getFraction(numerator, denominator, startAmount), _getFraction(numerator, denominator, endAmount), - elapsed, - remaining, - duration, + startTime, + endTime, roundUp ); } diff --git a/contracts/lib/Assertions.sol b/contracts/lib/Assertions.sol index ad6a19447..ec10a11fe 100644 --- a/contracts/lib/Assertions.sol +++ b/contracts/lib/Assertions.sol @@ -1,13 +1,16 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { OrderParameters } from "./ConsiderationStructs.sol"; import { GettersAndDerivers } from "./GettersAndDerivers.sol"; -import { TokenTransferrerErrors } from "../interfaces/TokenTransferrerErrors.sol"; +// prettier-ignore +import { + TokenTransferrerErrors +} from "../interfaces/TokenTransferrerErrors.sol"; -import { NonceManager } from "./NonceManager.sol"; +import { CounterManager } from "./CounterManager.sol"; import "./ConsiderationConstants.sol"; @@ -19,7 +22,7 @@ import "./ConsiderationConstants.sol"; */ contract Assertions is GettersAndDerivers, - NonceManager, + CounterManager, TokenTransferrerErrors { /** @@ -35,17 +38,17 @@ contract Assertions is {} /** - * @dev Internal view function to to ensure that the supplied consideration + * @dev Internal view function to ensure that the supplied consideration * array length on a given set of order parameters is not less than the * original consideration array length for that order and to retrieve - * the current nonce for a given order's offerer and zone and use it to - * derive the order hash. + * the current counter for a given order's offerer and zone and use it + * to derive the order hash. * * @param orderParameters The parameters of the order to hash. * * @return The hash. */ - function _assertConsiderationLengthAndGetNoncedOrderHash( + function _assertConsiderationLengthAndGetOrderHash( OrderParameters memory orderParameters ) internal view returns (bytes32) { // Ensure supplied consideration array length is not less than original. @@ -54,11 +57,11 @@ contract Assertions is orderParameters.totalOriginalConsiderationItems ); - // Derive and return order hash using current nonce for the offerer. + // Derive and return order hash using current counter for the offerer. return _deriveOrderHash( orderParameters, - _getNonce(orderParameters.offerer) + _getCounter(orderParameters.offerer) ); } @@ -89,7 +92,7 @@ contract Assertions is * @param amount The amount to check. */ function _assertNonZeroAmount(uint256 amount) internal pure { - // Revert if the supplied amont is equal to zero. + // Revert if the supplied amount is equal to zero. if (amount == 0) { revert MissingItemAmount(); } @@ -97,12 +100,14 @@ contract Assertions is /** * @dev Internal pure function to validate calldata offsets for dynamic - * types in BasicOrderParameters. This ensures that functions using the - * calldata object normally will be using the same data as the assembly - * functions. Note that no parameters are supplied as all basic order - * functions use the same calldata encoding. + * types in BasicOrderParameters and other parameters. This ensures + * that functions using the calldata object normally will be using the + * same data as the assembly functions and that values that are bound + * to a given range are within that range. Note that no parameters are + * supplied as all basic order functions use the same calldata + * encoding. */ - function _assertValidBasicOrderParameterOffsets() internal pure { + function _assertValidBasicOrderParameters() internal pure { // Declare a boolean designating basic order parameter offset validity. bool validOffsets; @@ -113,6 +118,7 @@ contract Assertions is * 1. Order parameters struct offset == 0x20 * 2. Additional recipients arr offset == 0x240 * 3. Signature offset == 0x260 + (recipients.length * 0x40) + * 4. BasicOrderType between 0 and 23 (i.e. < 24) */ validOffsets := and( // Order parameters at calldata 0x04 must have offset of 0x20. @@ -126,6 +132,7 @@ contract Assertions is BasicOrder_additionalRecipients_head_ptr ) ) + validOffsets := and( validOffsets, eq( @@ -145,6 +152,16 @@ contract Assertions is ) ) ) + + validOffsets := and( + validOffsets, + lt( + // BasicOrderType parameter at calldata offset 0x124. + calldataload(BasicOrder_basicOrderType_cdPtr), + // Value should be less than 24. + BasicOrder_basicOrderType_range + ) + ) } // Revert with an error if basic order parameter offsets are invalid. diff --git a/contracts/lib/BasicOrderFulfiller.sol b/contracts/lib/BasicOrderFulfiller.sol index d59af7fbc..62dcf1816 100644 --- a/contracts/lib/BasicOrderFulfiller.sol +++ b/contracts/lib/BasicOrderFulfiller.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { ConduitInterface } from "../interfaces/ConduitInterface.sol"; @@ -79,11 +79,14 @@ contract BasicOrderFulfiller is OrderValidator { // Utilize assembly to extract the order type and the basic order route. assembly { + // Read basicOrderType from calldata. + let basicOrderType := calldataload(BasicOrder_basicOrderType_cdPtr) + // Mask all but 2 least-significant bits to derive the order type. - orderType := and(calldataload(BasicOrder_basicOrderType_cdPtr), 3) + orderType := and(basicOrderType, 3) // Divide basicOrderType by four to derive the route. - route := div(calldataload(BasicOrder_basicOrderType_cdPtr), 4) + route := shr(2, basicOrderType) // If route > 1 additionalRecipient items are ERC20 (1) else Eth (0) additionalRecipientsItemType := gt(route, 1) @@ -111,52 +114,57 @@ contract BasicOrderFulfiller is OrderValidator { // Declare more arguments that will be derived from route and calldata. address additionalRecipientsToken; - ItemType receivedItemType; ItemType offeredItemType; + bool offerTypeIsAdditionalRecipientsType; - // Utilize assembly to retrieve function arguments and cast types. - assembly { - // Determine if offered item type == additional recipient item type. - let offerTypeIsAdditionalRecipientsType := gt(route, 3) + // Declare scope for received item type to manage stack pressure. + { + ItemType receivedItemType; - // If route > 3 additionalRecipientsToken is at 0xc4 else 0x24. - additionalRecipientsToken := calldataload( - add( - BasicOrder_considerationToken_cdPtr, - mul(offerTypeIsAdditionalRecipientsType, FiveWords) - ) - ) + // Utilize assembly to retrieve function arguments and cast types. + assembly { + // Check if offered item type == additional recipient item type. + offerTypeIsAdditionalRecipientsType := gt(route, 3) - // If route > 2, receivedItemType is route - 2. If route is 2, then - // receivedItemType is ERC20 (1). Otherwise, it is Eth (0). - receivedItemType := add( - mul(sub(route, 2), gt(route, 2)), - eq(route, 2) - ) + // If route > 3 additionalRecipientsToken is at 0xc4 else 0x24. + additionalRecipientsToken := calldataload( + add( + BasicOrder_considerationToken_cdPtr, + mul( + offerTypeIsAdditionalRecipientsType, + BasicOrder_common_params_size + ) + ) + ) - // If route > 3, offeredItemType is ERC20 (1). If route is 2 or 3, - // offeredItemType = route. If route is 0 or 1, it is route + 2. - offeredItemType := sub( - add(route, mul(iszero(additionalRecipientsItemType), 2)), - mul( - offerTypeIsAdditionalRecipientsType, - add(receivedItemType, 1) + // If route > 2, receivedItemType is route - 2. If route is 2, + // the receivedItemType is ERC20 (1). Otherwise, it is Eth (0). + receivedItemType := add( + mul(sub(route, 2), gt(route, 2)), + eq(route, 2) ) - ) - } - // Derive & validate order using parameters and update order status. - _prepareBasicFulfillmentFromCalldata( - parameters, - orderType, - receivedItemType, - additionalRecipientsItemType, - additionalRecipientsToken, - offeredItemType - ); + // If route > 3, offeredItemType is ERC20 (1). Route is 2 or 3, + // offeredItemType = route. Route is 0 or 1, it is route + 2. + offeredItemType := sub( + add(route, mul(iszero(additionalRecipientsItemType), 2)), + mul( + offerTypeIsAdditionalRecipientsType, + add(receivedItemType, 1) + ) + ) + } - // Read offerer from calldata and place on the stack. - address payable offerer = parameters.offerer; + // Derive & validate order using parameters and update order status. + _prepareBasicFulfillmentFromCalldata( + parameters, + orderType, + receivedItemType, + additionalRecipientsItemType, + additionalRecipientsToken, + offeredItemType + ); + } // Declare conduitKey argument used by transfer functions. bytes32 conduitKey; @@ -165,16 +173,28 @@ contract BasicOrderFulfiller is OrderValidator { assembly { // use offerer conduit for routes 0-3, fulfiller conduit otherwise. conduitKey := calldataload( - add(BasicOrder_offererConduit_cdPtr, mul(gt(route, 3), OneWord)) + add( + BasicOrder_offererConduit_cdPtr, + mul(offerTypeIsAdditionalRecipientsType, OneWord) + ) ) } // Transfer tokens based on the route. if (additionalRecipientsItemType == ItemType.NATIVE) { + // Ensure neither the token nor the identifier parameters are set. + if ( + (uint160(parameters.considerationToken) | + parameters.considerationIdentifier) != 0 + ) { + revert UnusedItemParameters(); + } + + // Transfer the ERC721 or ERC1155 item, bypassing the accumulator. _transferIndividual721Or1155Item( offeredItemType, parameters.offerToken, - offerer, + parameters.offerer, msg.sender, parameters.offerIdentifier, parameters.offerAmount, @@ -184,7 +204,7 @@ contract BasicOrderFulfiller is OrderValidator { // Transfer native to recipients, return excess to caller & wrap up. _transferEthAndFinalize( parameters.considerationAmount, - offerer, + parameters.offerer, parameters.additionalRecipients ); } else { @@ -195,72 +215,40 @@ contract BasicOrderFulfiller is OrderValidator { // still be accessed and modified, however. bytes memory accumulator = new bytes(AccumulatorDisarmed); + // Choose transfer method for ERC721 or ERC1155 item based on route. if (route == BasicOrderRouteType.ERC20_TO_ERC721) { // Transfer ERC721 to caller using offerer's conduit preference. _transferERC721( parameters.offerToken, - offerer, + parameters.offerer, msg.sender, parameters.offerIdentifier, parameters.offerAmount, conduitKey, accumulator ); - - // Transfer ERC20 tokens to all recipients and wrap up. - _transferERC20AndFinalize( - msg.sender, - offerer, - parameters.considerationToken, - parameters.considerationAmount, - parameters.additionalRecipients, - false, // Send full amount indicated by consideration items. - accumulator - ); } else if (route == BasicOrderRouteType.ERC20_TO_ERC1155) { // Transfer ERC1155 to caller with offerer's conduit preference. _transferERC1155( parameters.offerToken, - offerer, + parameters.offerer, msg.sender, parameters.offerIdentifier, parameters.offerAmount, conduitKey, accumulator ); - - // Transfer ERC20 tokens to all recipients and wrap up. - _transferERC20AndFinalize( - msg.sender, - offerer, - parameters.considerationToken, - parameters.considerationAmount, - parameters.additionalRecipients, - false, // Send full amount indicated by consideration items. - accumulator - ); } else if (route == BasicOrderRouteType.ERC721_TO_ERC20) { // Transfer ERC721 to offerer using caller's conduit preference. _transferERC721( parameters.considerationToken, msg.sender, - offerer, + parameters.offerer, parameters.considerationIdentifier, parameters.considerationAmount, conduitKey, accumulator ); - - // Transfer ERC20 tokens to all recipients and wrap up. - _transferERC20AndFinalize( - offerer, - msg.sender, - parameters.offerToken, - parameters.offerAmount, - parameters.additionalRecipients, - true, // Reduce fulfiller amount sent by additional amounts. - accumulator - ); } else { // route == BasicOrderRouteType.ERC1155_TO_ERC20 @@ -268,29 +256,29 @@ contract BasicOrderFulfiller is OrderValidator { _transferERC1155( parameters.considerationToken, msg.sender, - offerer, + parameters.offerer, parameters.considerationIdentifier, parameters.considerationAmount, conduitKey, accumulator ); - - // Transfer ERC20 tokens to all recipients and wrap up. - _transferERC20AndFinalize( - offerer, - msg.sender, - parameters.offerToken, - parameters.offerAmount, - parameters.additionalRecipients, - true, // Reduce fulfiller amount sent by additional amounts. - accumulator - ); } + // Transfer ERC20 tokens to all recipients and wrap up. + _transferERC20AndFinalize( + parameters.offerer, + parameters, + offerTypeIsAdditionalRecipientsType, + accumulator + ); + // Trigger any remaining accumulated transfers via call to conduit. _triggerIfArmed(accumulator); } + // Clear the reentrancy guard. + _clearReentrancyGuard(); + return true; } @@ -339,12 +327,13 @@ contract BasicOrderFulfiller is OrderValidator { // Verify that calldata offsets for all dynamic types were produced by // default encoding. This ensures that the constants we use for calldata // pointers to dynamic types are the same as those calculated by - // Solidity using their offsets. - _assertValidBasicOrderParameterOffsets(); + // Solidity using their offsets. Also verify that the basic order type + // is within range. + _assertValidBasicOrderParameters(); // Ensure supplied consideration array length is not less than original. _assertConsiderationLengthIsNotLessThanOriginalConsiderationLength( - parameters.additionalRecipients.length + 1, + parameters.additionalRecipients.length, parameters.totalOriginalAdditionalRecipients ); @@ -764,7 +753,7 @@ contract BasicOrderFulfiller is OrderValidator { * - 0x180: orderParameters.zoneHash * - 0x1a0: orderParameters.salt * - 0x1c0: orderParameters.conduitKey - * - 0x1e0: _nonces[orderParameters.offerer] (from storage) + * - 0x1e0: _counters[orderParameters.offerer] (from storage) */ // Read the offerer from calldata and place on the stack. @@ -773,8 +762,8 @@ contract BasicOrderFulfiller is OrderValidator { offerer := calldataload(BasicOrder_offerer_cdPtr) } - // Read offerer's current nonce from storage and place on the stack. - uint256 nonce = _getNonce(offerer); + // Read offerer's current counter from storage and place on stack. + uint256 counter = _getCounter(offerer); // Load order typehash from runtime code and place on stack. bytes32 typeHash = _ORDER_TYPEHASH; @@ -808,8 +797,8 @@ contract BasicOrderFulfiller is OrderValidator { FiveWords ) - // Take offerer's nonce retrieved from storage, write to struct. - mstore(BasicOrder_order_nonce_ptr, nonce) + // Write offerer's counter, retrieved from storage, to struct. + mstore(BasicOrder_order_counter_ptr, counter) // Compute the EIP712 Order hash. orderHash := keccak256( @@ -861,7 +850,7 @@ contract BasicOrderFulfiller is OrderValidator { // Write the order hash to the head of the event's data region. mstore(eventDataPtr, orderHash) - // Write the fulfiller (i.e. the caller) next. + // Write the fulfiller (i.e. the caller) next for receiver argument. mstore(add(eventDataPtr, OrderFulfilled_fulfiller_offset), caller()) // Write the SpentItem and ReceivedItem array offsets (constants). @@ -944,37 +933,33 @@ contract BasicOrderFulfiller is OrderValidator { // Retrieve total number of additional recipients and place on stack. uint256 totalAdditionalRecipients = additionalRecipients.length; - // Iterate over each additional recipient. - for (uint256 i = 0; i < totalAdditionalRecipients; ) { - // Retrieve the additional recipient. - AdditionalRecipient calldata additionalRecipient = ( - additionalRecipients[i] - ); + // Skip overflow check as for loop is indexed starting at zero. + unchecked { + // Iterate over each additional recipient. + for (uint256 i = 0; i < totalAdditionalRecipients; ++i) { + // Retrieve the additional recipient. + AdditionalRecipient calldata additionalRecipient = ( + additionalRecipients[i] + ); - // Read ether amount to transfer to recipient and place on stack. - uint256 additionalRecipientAmount = additionalRecipient.amount; + // Read ether amount to transfer to recipient & place on stack. + uint256 additionalRecipientAmount = additionalRecipient.amount; - // Ensure that sufficient Ether is available. - if (additionalRecipientAmount > etherRemaining) { - revert InsufficientEtherSupplied(); - } + // Ensure that sufficient Ether is available. + if (additionalRecipientAmount > etherRemaining) { + revert InsufficientEtherSupplied(); + } - // Transfer Ether to the additional recipient. - _transferEth( - additionalRecipient.recipient, - additionalRecipientAmount - ); + // Transfer Ether to the additional recipient. + _transferEth( + additionalRecipient.recipient, + additionalRecipientAmount + ); - // Skip underflow check as subtracted value is less than remaining. - unchecked { - // Reduce ether value available. + // Reduce ether value available. Skip underflow check as + // subtracted value is confirmed above as less than remaining. etherRemaining -= additionalRecipientAmount; } - - // Skip overflow check as for loop is indexed starting at zero. - unchecked { - ++i; - } } // Ensure that sufficient Ether is still available. @@ -993,38 +978,71 @@ contract BasicOrderFulfiller is OrderValidator { _transferEth(payable(msg.sender), etherRemaining - amount); } } - - // Clear the reentrancy guard. - _clearReentrancyGuard(); } /** * @dev Internal function to transfer ERC20 tokens to a given recipient as * part of basic order fulfillment. * - * @param from The originator of the ERC20 token transfer. - * @param to The recipient of the ERC20 token transfer. - * @param erc20Token The ERC20 token to transfer. - * @param amount The amount of ERC20 tokens to transfer. - * @param additionalRecipients The additional recipients of the order. - * @param fromOfferer A boolean indicating whether to decrement - * amount from the offered amount. + * @param offerer The offerer of the fulfiller order. + * @param parameters The basic order parameters. + * @param fromOfferer A boolean indicating whether to decrement amount from + * the offered amount. + * @param accumulator An open-ended array that collects transfers to execute + * against a given conduit in a single call. */ function _transferERC20AndFinalize( - address from, - address to, - address erc20Token, - uint256 amount, - AdditionalRecipient[] calldata additionalRecipients, + address offerer, + BasicOrderParameters calldata parameters, bool fromOfferer, bytes memory accumulator ) internal { + // Declare from and to variables determined by fromOfferer value. + address from; + address to; + + // Declare token and amount variables determined by fromOfferer value. + address token; + uint256 amount; + + // Declare and check identifier variable within an isolated scope. + { + // Declare identifier variable determined by fromOfferer value. + uint256 identifier; + + // Set ERC20 token transfer variables based on fromOfferer boolean. + if (fromOfferer) { + // Use offerer as from value and msg.sender as to value. + from = offerer; + to = msg.sender; + + // Use offer token and related values if token is from offerer. + token = parameters.offerToken; + identifier = parameters.offerIdentifier; + amount = parameters.offerAmount; + } else { + // Use msg.sender as from value and offerer as to value. + from = msg.sender; + to = offerer; + + // Otherwise, use consideration token and related values. + token = parameters.considerationToken; + identifier = parameters.considerationIdentifier; + amount = parameters.considerationAmount; + } + + // Ensure that no identifier is supplied. + if (identifier != 0) { + revert UnusedItemParameters(); + } + } + // Determine the appropriate conduit to utilize. bytes32 conduitKey; // Utilize assembly to derive conduit (if relevant) based on route. assembly { - // use offerer conduit if fromOfferer, fulfiller conduit otherwise. + // Use offerer conduit if fromOfferer, fulfiller conduit otherwise. conduitKey := calldataload( sub( BasicOrder_fulfillerConduit_cdPtr, @@ -1034,13 +1052,15 @@ contract BasicOrderFulfiller is OrderValidator { } // Retrieve total number of additional recipients and place on stack. - uint256 totalAdditionalRecipients = additionalRecipients.length; + uint256 totalAdditionalRecipients = ( + parameters.additionalRecipients.length + ); // Iterate over each additional recipient. for (uint256 i = 0; i < totalAdditionalRecipients; ) { // Retrieve the additional recipient. AdditionalRecipient calldata additionalRecipient = ( - additionalRecipients[i] + parameters.additionalRecipients[i] ); uint256 additionalRecipientAmount = additionalRecipient.amount; @@ -1052,7 +1072,7 @@ contract BasicOrderFulfiller is OrderValidator { // Transfer ERC20 tokens to additional recipient given approval. _transferERC20( - erc20Token, + token, from, additionalRecipient.recipient, additionalRecipientAmount, @@ -1067,9 +1087,6 @@ contract BasicOrderFulfiller is OrderValidator { } // Transfer ERC20 token amount (from account must have proper approval). - _transferERC20(erc20Token, from, to, amount, conduitKey, accumulator); - - // Clear the reentrancy guard. - _clearReentrancyGuard(); + _transferERC20(token, from, to, amount, conduitKey, accumulator); } } diff --git a/contracts/Consideration.sol b/contracts/lib/Consideration.sol similarity index 93% rename from contracts/Consideration.sol rename to contracts/lib/Consideration.sol index fd13c89bf..765f6b38b 100644 --- a/contracts/Consideration.sol +++ b/contracts/lib/Consideration.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; // prettier-ignore import { ConsiderationInterface -} from "./interfaces/ConsiderationInterface.sol"; +} from "../interfaces/ConsiderationInterface.sol"; // prettier-ignore import { @@ -18,16 +18,16 @@ import { Fulfillment, FulfillmentComponent, Execution -} from "./lib/ConsiderationStructs.sol"; +} from "./ConsiderationStructs.sol"; -import { OrderCombiner } from "./lib/OrderCombiner.sol"; +import { OrderCombiner } from "./OrderCombiner.sol"; /** * @title Consideration * @author 0age * @custom:coauthor d1ll0n * @custom:coauthor transmissions11 - * @custom:version 1 + * @custom:version 1.1 * @notice Consideration is a generalized ETH/ERC20/ERC721/ERC1155 marketplace. * It minimizes external calls to the greatest extent possible and * provides lightweight methods for common routes as well as more @@ -115,7 +115,8 @@ contract Consideration is ConsiderationInterface, OrderCombiner { fulfilled = _validateAndFulfillAdvancedOrder( _convertOrderToAdvanced(order), new CriteriaResolver[](0), // No criteria resolvers supplied. - fulfillerConduitKey + fulfillerConduitKey, + msg.sender ); } @@ -144,7 +145,7 @@ contract Consideration is ConsiderationInterface, OrderCombiner { * contained in the merkle root held by the item * in question's criteria element. Note that an * empty criteria indicates that any - * (transferrable) token identifier on the token + * (transferable) token identifier on the token * in question is valid and that no associated * proof needs to be supplied. * @param fulfillerConduitKey A bytes32 value indicating what conduit, if @@ -152,6 +153,9 @@ contract Consideration is ConsiderationInterface, OrderCombiner { * from. The zero hash signifies that no conduit * should be used (and direct approvals set on * Consideration). + * @param recipient The intended recipient for all received items, + * with `address(0)` indicating that the caller + * should receive the items. * * @return fulfilled A boolean indicating whether the order has been * successfully fulfilled. @@ -159,13 +163,15 @@ contract Consideration is ConsiderationInterface, OrderCombiner { function fulfillAdvancedOrder( AdvancedOrder calldata advancedOrder, CriteriaResolver[] calldata criteriaResolvers, - bytes32 fulfillerConduitKey + bytes32 fulfillerConduitKey, + address recipient ) external payable override returns (bool fulfilled) { // Validate and fulfill the order. fulfilled = _validateAndFulfillAdvancedOrder( advancedOrder, criteriaResolvers, - fulfillerConduitKey + fulfillerConduitKey, + recipient == address(0) ? msg.sender : recipient ); } @@ -232,6 +238,7 @@ contract Consideration is ConsiderationInterface, OrderCombiner { offerFulfillments, considerationFulfillments, fulfillerConduitKey, + msg.sender, maximumFulfilled ); } @@ -272,7 +279,7 @@ contract Consideration is ConsiderationInterface, OrderCombiner { * is contained in the merkle root held by * the item in question's criteria element. * Note that an empty criteria indicates - * that any (transferrable) token + * that any (transferable) token * identifier on the token in question is * valid and that no associated proof needs * to be supplied. @@ -288,6 +295,9 @@ contract Consideration is ConsiderationInterface, OrderCombiner { * approvals from. The zero hash signifies * that no conduit should be used (and * direct approvals set on Consideration). + * @param recipient The intended recipient for all received + * items, with `address(0)` indicating that + * the caller should receive the items. * @param maximumFulfilled The maximum number of orders to fulfill. * * @return availableOrders An array of booleans indicating if each order @@ -303,6 +313,7 @@ contract Consideration is ConsiderationInterface, OrderCombiner { FulfillmentComponent[][] calldata offerFulfillments, FulfillmentComponent[][] calldata considerationFulfillments, bytes32 fulfillerConduitKey, + address recipient, uint256 maximumFulfilled ) external @@ -318,6 +329,7 @@ contract Consideration is ConsiderationInterface, OrderCombiner { offerFulfillments, considerationFulfillments, fulfillerConduitKey, + recipient == address(0) ? msg.sender : recipient, maximumFulfilled ); } @@ -383,7 +395,7 @@ contract Consideration is ConsiderationInterface, OrderCombiner { * offer or consideration, a token identifier, and * a proof that the supplied token identifier is * contained in the order's merkle root. Note that - * an empty root indicates that any (transferrable) + * an empty root indicates that any (transferable) * token identifier is valid and that no associated * proof needs to be supplied. * @param fulfillments An array of elements allocating offer components @@ -455,14 +467,14 @@ contract Consideration is ConsiderationInterface, OrderCombiner { /** * @notice Cancel all orders from a given offerer with a given zone in bulk - * by incrementing a nonce. Note that only the offerer may increment - * the nonce. + * by incrementing a counter. Note that only the offerer may + * increment the counter. * - * @return newNonce The new nonce. + * @return newCounter The new counter. */ - function incrementNonce() external override returns (uint256 newNonce) { - // Increment current nonce for the supplied offerer. - newNonce = _incrementNonce(); + function incrementCounter() external override returns (uint256 newCounter) { + // Increment current counter for the supplied offerer. + newCounter = _incrementCounter(); } /** @@ -478,7 +490,7 @@ contract Consideration is ConsiderationInterface, OrderCombiner { override returns (bytes32 orderHash) { - // Derive order hash by supplying order parameters along with the nonce. + // Derive order hash by supplying order parameters along with counter. orderHash = _deriveOrderHash( OrderParameters( order.offerer, @@ -493,7 +505,7 @@ contract Consideration is ConsiderationInterface, OrderCombiner { order.conduitKey, order.consideration.length ), - order.nonce + order.counter ); } @@ -530,20 +542,20 @@ contract Consideration is ConsiderationInterface, OrderCombiner { } /** - * @notice Retrieve the current nonce for a given offerer. + * @notice Retrieve the current counter for a given offerer. * * @param offerer The offerer in question. * - * @return nonce The current nonce. + * @return counter The current counter. */ - function getNonce(address offerer) + function getCounter(address offerer) external view override - returns (uint256 nonce) + returns (uint256 counter) { - // Return the nonce for the supplied offerer. - nonce = _getNonce(offerer); + // Return the counter for the supplied offerer. + counter = _getCounter(offerer); } /** diff --git a/contracts/lib/ConsiderationBase.sol b/contracts/lib/ConsiderationBase.sol index 611a7c261..f7e46be2a 100644 --- a/contracts/lib/ConsiderationBase.sol +++ b/contracts/lib/ConsiderationBase.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; // prettier-ignore import { @@ -94,11 +94,20 @@ contract ConsiderationBase is ConsiderationEventsAndErrors { function _name() internal pure virtual returns (string memory) { // Return the name of the contract. assembly { - mstore(0, OneWord) // First element is the offset. + // First element is the offset for the returned string. Offset the + // value in memory by one word so that the free memory pointer will + // be overwritten by the next write. + mstore(OneWord, OneWord) + // Name is right padded, so it touches the length which is left - // padded. This enables writing both values at once. + // padded. This enables writing both values at once. The free memory + // pointer will be overwritten in the process. mstore(NameLengthPtr, NameWithLength) - return(0, ThreeWords) // Return all three words. + + // Standard ABI encoding pads returned data to the nearest word. Use + // the already empty zero slot memory region for this purpose and + // return the final name string, offset by the original single word. + return(OneWord, ThreeWords) } } @@ -143,7 +152,7 @@ contract ConsiderationBase is ConsiderationEventsAndErrors { nameHash = keccak256(bytes(_nameString())); // Derive hash of the version string of the contract. - versionHash = keccak256(bytes("1")); + versionHash = keccak256(bytes("1.1")); // Construct the OfferItem type string. // prettier-ignore @@ -184,7 +193,7 @@ contract ConsiderationBase is ConsiderationEventsAndErrors { "bytes32 zoneHash,", "uint256 salt,", "bytes32 conduitKey,", - "uint256 nonce", + "uint256 counter", ")" ); diff --git a/contracts/lib/ConsiderationConstants.sol b/contracts/lib/ConsiderationConstants.sol index 52e5edf7b..12f4b0963 100644 --- a/contracts/lib/ConsiderationConstants.sol +++ b/contracts/lib/ConsiderationConstants.sol @@ -8,7 +8,7 @@ pragma solidity >=0.8.7; * offset or pointer to the body of a dynamic type. In calldata, the head * is always an offset (relative to the parent object), while in memory, * the head is always the pointer to the body. More information found here: - * https://docs.soliditylang.org/en/v0.8.13/abi-spec.html#argument-encoding + * https://docs.soliditylang.org/en/v0.8.14/abi-spec.html#argument-encoding * - Note that the length of an array is separate from and precedes the * head of the array. * @@ -35,15 +35,15 @@ pragma solidity >=0.8.7; // Declare constants for name, version, and reentrancy sentinel values. -// Name is right padded, so it touches the length which is left padded. -// This lets us write both values at once. -// Length goes at byte 63, and name fills bytes 64-77, so we write -// both values left-padded to 45. -uint256 constant NameLengthPtr = 45; +// Name is right padded, so it touches the length which is left padded. This +// enables writing both values at once. Length goes at byte 95 in memory, and +// name fills bytes 96-109, so both values can be written left-padded to 77. +uint256 constant NameLengthPtr = 77; uint256 constant NameWithLength = 0x0d436F6E73696465726174696F6E; -uint256 constant Version = 0x31; -uint256 constant Version_length = 1; +uint256 constant Version = 0x312e31; +uint256 constant Version_length = 3; +uint256 constant Version_shift = 0xe8; uint256 constant _NOT_ENTERED = 1; uint256 constant _ENTERED = 2; @@ -72,7 +72,7 @@ uint256 constant Execution_conduit_offset = 0x40; uint256 constant InvalidFulfillmentComponentData_error_signature = ( 0x7fda727900000000000000000000000000000000000000000000000000000000 ); -uint256 constant InvalidFulfillmentComponentData_error_len = 0x20; +uint256 constant InvalidFulfillmentComponentData_error_len = 0x04; uint256 constant Panic_error_signature = ( 0x4e487b7100000000000000000000000000000000000000000000000000000000 @@ -84,17 +84,18 @@ uint256 constant Panic_arithmetic = 0x11; uint256 constant MissingItemAmount_error_signature = ( 0x91b3e51400000000000000000000000000000000000000000000000000000000 ); -uint256 constant MissingItemAmount_error_len = 0x20; +uint256 constant MissingItemAmount_error_len = 0x04; uint256 constant OrderParameters_offer_head_offset = 0x40; uint256 constant OrderParameters_consideration_head_offset = 0x60; uint256 constant OrderParameters_conduit_offset = 0x120; -uint256 constant OrderParameters_nonce_offset = 0x140; +uint256 constant OrderParameters_counter_offset = 0x140; uint256 constant Fulfillment_itemIndex_offset = 0x20; uint256 constant AdvancedOrder_numerator_offset = 0x20; +uint256 constant AlmostOneWord = 0x1f; uint256 constant OneWord = 0x20; uint256 constant TwoWords = 0x40; uint256 constant ThreeWords = 0x60; @@ -109,7 +110,7 @@ uint256 constant Slot0x80 = 0x80; uint256 constant Slot0xA0 = 0xa0; uint256 constant BasicOrder_endAmount_cdPtr = 0x104; - +uint256 constant BasicOrder_common_params_size = 0xa0; uint256 constant BasicOrder_considerationHashesArray_ptr = 0x160; uint256 constant EIP712_Order_size = 0x180; @@ -203,6 +204,8 @@ uint256 constant BasicOrder_additionalRecipients_data_cdPtr = 0x284; uint256 constant BasicOrder_parameters_ptr = 0x20; +uint256 constant BasicOrder_basicOrderType_range = 0x18; // 24 values + /* * Memory layout in _prepareBasicFulfillmentFromCalldata of * EIP712 data for ConsiderationItem @@ -253,7 +256,7 @@ uint256 constant BasicOrder_offerItem_endAmount_ptr = 0x120; * - 0x180: zoneHash * - 0x1a0: salt * - 0x1c0: conduit - * - 0x1e0: _nonces[orderParameters.offerer] (from storage) + * - 0x1e0: _counters[orderParameters.offerer] (from storage) */ uint256 constant BasicOrder_order_typeHash_ptr = 0x80; uint256 constant BasicOrder_order_offerer_ptr = 0xa0; @@ -266,7 +269,7 @@ uint256 constant BasicOrder_order_startTime_ptr = 0x140; // uint256 constant BasicOrder_order_zoneHash_ptr = 0x180; // uint256 constant BasicOrder_order_salt_ptr = 0x1a0; // uint256 constant BasicOrder_order_conduitKey_ptr = 0x1c0; -uint256 constant BasicOrder_order_nonce_ptr = 0x1e0; +uint256 constant BasicOrder_order_counter_ptr = 0x1e0; uint256 constant BasicOrder_additionalRecipients_head_ptr = 0x240; uint256 constant BasicOrder_signature_ptr = 0x260; @@ -274,6 +277,22 @@ uint256 constant BasicOrder_signature_ptr = 0x260; bytes32 constant EIP2098_allButHighestBitMask = ( 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff ); +bytes32 constant ECDSA_twentySeventhAndTwentyEighthBytesSet = ( + 0x0000000000000000000000000000000000000000000000000000000101000000 +); +uint256 constant ECDSA_MaxLength = 65; +uint256 constant ECDSA_signature_s_offset = 0x40; +uint256 constant ECDSA_signature_v_offset = 0x60; + +bytes32 constant EIP1271_isValidSignature_selector = ( + 0x1626ba7e00000000000000000000000000000000000000000000000000000000 +); +uint256 constant EIP1271_isValidSignature_signatureHead_negativeOffset = 0x20; +uint256 constant EIP1271_isValidSignature_digest_negativeOffset = 0x40; +uint256 constant EIP1271_isValidSignature_selector_negativeOffset = 0x44; +uint256 constant EIP1271_isValidSignature_calldata_baseLength = 0x64; + +uint256 constant EIP1271_isValidSignature_signature_head_offset = 0x40; // abi.encodeWithSignature("NoContract(address)") uint256 constant NoContract_error_signature = ( @@ -289,7 +308,7 @@ uint256 constant EIP_712_PREFIX = ( uint256 constant ExtraGasBuffer = 0x20; uint256 constant CostPerWord = 3; -uint256 constant MemoryExpansionCoefficient = 0x200; +uint256 constant MemoryExpansionCoefficient = 0x200; // 512 uint256 constant Create2AddressDerivation_ptr = 0x0b; uint256 constant Create2AddressDerivation_length = 0x55; @@ -310,6 +329,9 @@ uint256 constant Conduit_execute_signature = ( 0x4ce34aa200000000000000000000000000000000000000000000000000000000 ); +uint256 constant MaxUint8 = 0xff; +uint256 constant MaxUint120 = 0xffffffffffffffffffffffffffffff; + uint256 constant Conduit_execute_ConduitTransfer_ptr = 0x20; uint256 constant Conduit_execute_ConduitTransfer_length = 0x01; @@ -341,3 +363,50 @@ uint256 constant Conduit_transferItem_from_ptr = 0x40; uint256 constant Conduit_transferItem_to_ptr = 0x60; uint256 constant Conduit_transferItem_identifier_ptr = 0x80; uint256 constant Conduit_transferItem_amount_ptr = 0xa0; + +// Declare constant for errors related to amount derivation. +// error InexactFraction() @ AmountDerivationErrors.sol +uint256 constant InexactFraction_error_signature = ( + 0xc63cf08900000000000000000000000000000000000000000000000000000000 +); +uint256 constant InexactFraction_error_len = 0x04; + +// Declare constant for errors related to signature verification. +uint256 constant Ecrecover_precompile = 1; +uint256 constant Ecrecover_args_size = 0x80; +uint256 constant Signature_lower_v = 27; + +// error BadSignatureV(uint8) @ SignatureVerificationErrors.sol +uint256 constant BadSignatureV_error_signature = ( + 0x1f003d0a00000000000000000000000000000000000000000000000000000000 +); +uint256 constant BadSignatureV_error_offset = 0x04; +uint256 constant BadSignatureV_error_length = 0x24; + +// error InvalidSigner() @ SignatureVerificationErrors.sol +uint256 constant InvalidSigner_error_signature = ( + 0x815e1d6400000000000000000000000000000000000000000000000000000000 +); +uint256 constant InvalidSigner_error_length = 0x04; + +// error InvalidSignature() @ SignatureVerificationErrors.sol +uint256 constant InvalidSignature_error_signature = ( + 0x8baa579f00000000000000000000000000000000000000000000000000000000 +); +uint256 constant InvalidSignature_error_length = 0x04; + +// error BadContractSignature() @ SignatureVerificationErrors.sol +uint256 constant BadContractSignature_error_signature = ( + 0x4f7fb80d00000000000000000000000000000000000000000000000000000000 +); +uint256 constant BadContractSignature_error_length = 0x04; + +uint256 constant NumBitsAfterSelector = 0xe0; + +// 69 is the lowest modulus for which the remainder +// of every selector other than the two match functions +// is greater than those of the match functions. +uint256 constant NonMatchSelector_MagicModulus = 69; +// Of the two match function selectors, the highest +// remainder modulo 69 is 29. +uint256 constant NonMatchSelector_MagicRemainder = 0x1d; diff --git a/contracts/lib/ConsiderationEnums.sol b/contracts/lib/ConsiderationEnums.sol index 33bb3e1b5..c8797f204 100644 --- a/contracts/lib/ConsiderationEnums.sol +++ b/contracts/lib/ConsiderationEnums.sol @@ -137,7 +137,7 @@ enum ItemType { enum Side { // 0: Items that can be spent OFFER, - + // 1: Items that must be received CONSIDERATION } diff --git a/contracts/lib/ConsiderationStructs.sol b/contracts/lib/ConsiderationStructs.sol index eb6ec363b..064d01d9a 100644 --- a/contracts/lib/ConsiderationStructs.sol +++ b/contracts/lib/ConsiderationStructs.sol @@ -10,12 +10,12 @@ import { } from "./ConsiderationEnums.sol"; /** - * @dev An order contains ten components: an offerer, a zone (or account that + * @dev An order contains eleven components: an offerer, a zone (or account that * can cancel the order or restrict who can fulfill the order depending on * the type), the order type (specifying partial fill support as well as * restricted order status), the start and end time, a hash that will be * provided to the zone when validating restricted orders, a salt, a key - * corresponding to a given conduit, a nonce, and an arbitrary number of + * corresponding to a given conduit, a counter, and an arbitrary number of * offer items that can be spent along with consideration items that must * be received by their respective recipient. */ @@ -30,7 +30,7 @@ struct OrderComponents { bytes32 zoneHash; uint256 salt; bytes32 conduitKey; - uint256 nonce; + uint256 counter; } /** @@ -65,7 +65,7 @@ struct ConsiderationItem { } /** - * @dev A spent item is translated from a utilized offer item an has four + * @dev A spent item is translated from a utilized offer item and has four * components: an item type (ETH or other native tokens, ERC20, ERC721, and * ERC1155), a token address, a tokenId, and an amount. */ @@ -131,8 +131,8 @@ struct AdditionalRecipient { } /** - * @dev The full set of order components, with the exception of the nonce, must - * be supplied when fulfilling more sophisticated orders or groups of + * @dev The full set of order components, with the exception of the counter, + * must be supplied when fulfilling more sophisticated orders or groups of * orders. The total number of original consideration items must also be * supplied, as the caller may specify additional consideration items. */ @@ -178,7 +178,7 @@ struct AdvancedOrder { /** * @dev Orders can be validated (either explicitly via `validate`, or as a * consequence of a full or partial fill), specifically cancelled (they can - * also be cancelled in bulk via incrementing a per-zone nonce), and + * also be cancelled in bulk via incrementing a per-zone counter), and * partially or fully filled (with the fraction filled represented by a * numerator and denominator). */ diff --git a/contracts/lib/CounterManager.sol b/contracts/lib/CounterManager.sol new file mode 100644 index 000000000..69532b391 --- /dev/null +++ b/contracts/lib/CounterManager.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +// prettier-ignore +import { + ConsiderationEventsAndErrors +} from "../interfaces/ConsiderationEventsAndErrors.sol"; + +import { ReentrancyGuard } from "./ReentrancyGuard.sol"; + +/** + * @title CounterManager + * @author 0age + * @notice CounterManager contains a storage mapping and related functionality + * for retrieving and incrementing a per-offerer counter. + */ +contract CounterManager is ConsiderationEventsAndErrors, ReentrancyGuard { + // Only orders signed using an offerer's current counter are fulfillable. + mapping(address => uint256) private _counters; + + /** + * @dev Internal function to cancel all orders from a given offerer with a + * given zone in bulk by incrementing a counter. Note that only the + * offerer may increment the counter. + * + * @return newCounter The new counter. + */ + function _incrementCounter() internal returns (uint256 newCounter) { + // Ensure that the reentrancy guard is not currently set. + _assertNonReentrant(); + + // Skip overflow check as counter cannot be incremented that far. + unchecked { + // Increment current counter for the supplied offerer. + newCounter = ++_counters[msg.sender]; + } + + // Emit an event containing the new counter. + emit CounterIncremented(newCounter, msg.sender); + } + + /** + * @dev Internal view function to retrieve the current counter for a given + * offerer. + * + * @param offerer The offerer in question. + * + * @return currentCounter The current counter. + */ + function _getCounter(address offerer) + internal + view + returns (uint256 currentCounter) + { + // Return the counter for the supplied offerer. + currentCounter = _counters[offerer]; + } +} diff --git a/contracts/lib/CriteriaResolution.sol b/contracts/lib/CriteriaResolution.sol index 893df98d5..6c814958a 100644 --- a/contracts/lib/CriteriaResolution.sol +++ b/contracts/lib/CriteriaResolution.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { ItemType, Side } from "./ConsiderationEnums.sol"; @@ -37,7 +37,7 @@ contract CriteriaResolution is CriteriaResolutionErrors { * identifier, and a proof that the supplied token * identifier is contained in the order's merkle * root. Note that a root of zero indicates that - * any transferrable token identifier is valid and + * any transferable token identifier is valid and * that no proof needs to be supplied. */ function _applyCriteriaResolvers( @@ -102,8 +102,10 @@ contract CriteriaResolution is CriteriaResolutionErrors { identifierOrCriteria = offerItem.identifierOrCriteria; // Optimistically update item type to remove criteria usage. + // Use assembly to operate on ItemType enum as a number. ItemType newItemType; assembly { + // Item type 4 becomes 2 and item type 5 becomes 3. newItemType := sub(3, eq(itemType, 4)) } offerItem.itemType = newItemType; @@ -134,8 +136,10 @@ contract CriteriaResolution is CriteriaResolutionErrors { ); // Optimistically update item type to remove criteria usage. + // Use assembly to operate on ItemType enum as a number. ItemType newItemType; assembly { + // Item type 4 becomes 2 and item type 5 becomes 3. newItemType := sub(3, eq(itemType, 4)) } considerationItem.itemType = newItemType; @@ -174,7 +178,7 @@ contract CriteriaResolution is CriteriaResolutionErrors { // Retrieve the parameters for the order. OrderParameters memory orderParameters = ( - advancedOrders[i].parameters + advancedOrder.parameters ); // Read consideration length from memory and place on stack. @@ -243,34 +247,41 @@ contract CriteriaResolution is CriteriaResolutionErrors { uint256 root, bytes32[] memory proof ) internal pure { + // Declare a variable that will be used to determine proof validity. bool isValid; + // Utilize assembly to efficiently verify the proof against the root. assembly { - // Start the hash off as just the starting leaf. - let computedHash := leaf + // Store the leaf at the beginning of scratch space. + mstore(0, leaf) + // Derive the hash of the leaf to use as the initial proof element. + let computedHash := keccak256(0, OneWord) + + // Based on: https://github.com/Rari-Capital/solmate/blob/v7/src/utils/MerkleProof.sol // Get memory start location of the first element in proof array. let data := add(proof, OneWord) - // Iterate over proof elements to compute root hash. + // Iterate over each proof element to compute the root hash. for { - let end := add(data, mul(mload(proof), OneWord)) + // Left shift by 5 is equivalent to multiplying by 0x20. + let end := add(data, shl(5, mload(proof))) } lt(data, end) { + // Increment by one word at a time. data := add(data, OneWord) } { // Get the proof element. let loadedData := mload(data) - // Sort and store proof element and hash. - switch gt(computedHash, loadedData) - case 0 { - mstore(0, computedHash) // Place existing hash first. - mstore(0x20, loadedData) // Place new hash next. - } - default { - mstore(0, loadedData) // Place new hash first. - mstore(0x20, computedHash) // Place existing hash next. - } + // Sort proof elements and place them in scratch space. + // Slot of `computedHash` in scratch space. + // If the condition is true: 0x20, otherwise: 0x00. + let scratch := shl(5, gt(computedHash, loadedData)) + + // Store elements to hash contiguously in scratch space. Scratch + // space is 64 bytes (0x00 - 0x3f) & both elements are 32 bytes. + mstore(scratch, computedHash) + mstore(xor(scratch, OneWord), loadedData) // Derive the updated hash. computedHash := keccak256(0, TwoWords) diff --git a/contracts/lib/Executor.sol b/contracts/lib/Executor.sol index b00f5f63e..6e030b29b 100644 --- a/contracts/lib/Executor.sol +++ b/contracts/lib/Executor.sol @@ -1,15 +1,10 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; - -// prettier-ignore -import { - ERC20Interface, - ERC721Interface, - ERC1155Interface -} from "../interfaces/AbridgedTokenInterfaces.sol"; +pragma solidity >=0.8.13; import { ConduitInterface } from "../interfaces/ConduitInterface.sol"; +import { ConduitItemType } from "../conduit/lib/ConduitEnums.sol"; + import { ItemType } from "./ConsiderationEnums.sol"; import { ReceivedItem } from "./ConsiderationStructs.sol"; @@ -59,9 +54,19 @@ contract Executor is Verifiers, TokenTransferrer { ) internal { // If the item type indicates Ether or a native token... if (item.itemType == ItemType.NATIVE) { + // Ensure neither the token nor the identifier parameters are set. + if ((uint160(item.token) | item.identifier) != 0) { + revert UnusedItemParameters(); + } + // transfer the native tokens to the recipient. _transferEth(item.recipient, item.amount); } else if (item.itemType == ItemType.ERC20) { + // Ensure that no identifier is supplied. + if (item.identifier != 0) { + revert UnusedItemParameters(); + } + // Transfer ERC20 tokens from the source to the recipient. _transferERC20( item.token, @@ -280,7 +285,7 @@ contract Executor is Verifiers, TokenTransferrer { _insert( conduitKey, accumulator, - uint256(1), + ConduitItemType.ERC20, token, from, to, @@ -333,7 +338,7 @@ contract Executor is Verifiers, TokenTransferrer { _insert( conduitKey, accumulator, - uint256(2), + ConduitItemType.ERC721, token, from, to, @@ -384,7 +389,7 @@ contract Executor is Verifiers, TokenTransferrer { _insert( conduitKey, accumulator, - uint256(3), + ConduitItemType.ERC1155, token, from, to, @@ -430,7 +435,7 @@ contract Executor is Verifiers, TokenTransferrer { */ function _triggerIfArmed(bytes memory accumulator) internal { // Exit if the accumulator is not "armed". - if (accumulator.length != 64) { + if (accumulator.length != AccumulatorArmed) { return; } @@ -504,6 +509,7 @@ contract Executor is Verifiers, TokenTransferrer { address conduit = _deriveConduit(conduitKey); bool success; + bytes4 result; // call the conduit. assembly { @@ -520,6 +526,9 @@ contract Executor is Verifiers, TokenTransferrer { 0, OneWord ) + + // Take value from scratch space and place it on the stack. + result := mload(0) } // If the call failed... @@ -531,13 +540,6 @@ contract Executor is Verifiers, TokenTransferrer { revert InvalidCallToConduit(conduit); } - // Ensure that the conduit returned the correct magic value. - bytes4 result; - assembly { - // Take value from scratch space and place it on the stack. - result := mload(0) - } - // Ensure result was extracted and matches EIP-1271 magic value. if (result != ConduitInterface.execute.selector) { revert InvalidConduit(conduitKey, conduit); @@ -588,7 +590,7 @@ contract Executor is Verifiers, TokenTransferrer { function _insert( bytes32 conduitKey, bytes memory accumulator, - uint256 itemType, + ConduitItemType itemType, address token, address from, address to, diff --git a/contracts/lib/FulfillmentApplier.sol b/contracts/lib/FulfillmentApplier.sol index 2b076358f..02c0957f5 100644 --- a/contracts/lib/FulfillmentApplier.sol +++ b/contracts/lib/FulfillmentApplier.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { ItemType, Side } from "./ConsiderationEnums.sol"; @@ -31,7 +31,7 @@ import { */ contract FulfillmentApplier is FulfillmentApplicationErrors { /** - * @dev Internal view function to match offer items to consideration items + * @dev Internal pure function to match offer items to consideration items * on a group of orders via a supplied fulfillment. * * @param advancedOrders The orders to match. @@ -49,7 +49,7 @@ contract FulfillmentApplier is FulfillmentApplicationErrors { AdvancedOrder[] memory advancedOrders, FulfillmentComponent[] calldata offerComponents, FulfillmentComponent[] calldata considerationComponents - ) internal view returns (Execution memory execution) { + ) internal pure returns (Execution memory execution) { // Ensure 1+ of both offer and consideration components are supplied. if ( offerComponents.length == 0 || considerationComponents.length == 0 @@ -70,6 +70,8 @@ contract FulfillmentApplier is FulfillmentApplicationErrors { // Retrieve the consideration item from the execution struct. ReceivedItem memory considerationItem = considerationExecution.item; + // Recipient does not need to be specified because it will always be set + // to that of the consideration. // Validate & aggregate offer items to Execution object. _aggregateValidFulfillmentOfferItems( advancedOrders, @@ -93,27 +95,39 @@ contract FulfillmentApplier is FulfillmentApplicationErrors { considerationComponents[0] ); - // Add excess consideration item amount to original array of orders. - advancedOrders[targetComponent.orderIndex] - .parameters - .consideration[targetComponent.itemIndex] - .startAmount = considerationItem.amount - execution.item.amount; + // Skip underflow check as the conditional being true implies that + // considerationItem.amount > execution.item.amount. + unchecked { + // Add excess consideration item amount to original order array. + advancedOrders[targetComponent.orderIndex] + .parameters + .consideration[targetComponent.itemIndex] + .startAmount = (considerationItem.amount - + execution.item.amount); + } // Reduce total consideration amount to equal the offer amount. considerationItem.amount = execution.item.amount; } else { // Retrieve the first offer component from the fulfillment. - FulfillmentComponent memory targetComponent = (offerComponents[0]); + FulfillmentComponent memory targetComponent = offerComponents[0]; + + // Skip underflow check as the conditional being false implies that + // execution.item.amount >= considerationItem.amount. + unchecked { + // Add excess offer item amount to the original array of orders. + advancedOrders[targetComponent.orderIndex] + .parameters + .offer[targetComponent.itemIndex] + .startAmount = (execution.item.amount - + considerationItem.amount); + } - // Add excess offer item amount to the original array of orders. - advancedOrders[targetComponent.orderIndex] - .parameters - .offer[targetComponent.itemIndex] - .startAmount = execution.item.amount - considerationItem.amount; + // Reduce total offer amount to equal the consideration amount. + execution.item.amount = considerationItem.amount; } - // Reuse execution struct with consideration amount and recipient. - execution.item.amount = considerationItem.amount; + // Reuse consideration recipient. execution.item.recipient = considerationItem.recipient; // Return the final execution that will be triggered for relevant items. @@ -135,6 +149,8 @@ contract FulfillmentApplier is FulfillmentApplicationErrors { * approvals from. The zero hash signifies that * no conduit should be used, with approvals * set directly on this contract. + * @param recipient The intended recipient for all received + * items. * * @return execution The transfer performed as a result of the fulfillment. */ @@ -142,7 +158,8 @@ contract FulfillmentApplier is FulfillmentApplicationErrors { AdvancedOrder[] memory advancedOrders, Side side, FulfillmentComponent[] memory fulfillmentComponents, - bytes32 fulfillerConduitKey + bytes32 fulfillerConduitKey, + address recipient ) internal view returns (Execution memory execution) { // Skip overflow / underflow checks; conditions checked or unreachable. unchecked { @@ -154,6 +171,9 @@ contract FulfillmentApplier is FulfillmentApplicationErrors { // If the fulfillment components are offer components... if (side == Side.OFFER) { + // Set the supplied recipient on the execution item. + execution.item.recipient = payable(recipient); + // Return execution for aggregated items provided by offerer. _aggregateValidFulfillmentOfferItems( advancedOrders, @@ -177,9 +197,11 @@ contract FulfillmentApplier is FulfillmentApplicationErrors { execution.conduitKey = fulfillerConduitKey; } - // Set the offerer as the receipient if execution amount is nonzero. + // Set the offerer and recipient to null address if execution + // amount is zero. This will cause the execution item to be skipped. if (execution.item.amount == 0) { - execution.item.recipient = payable(execution.offerer); + execution.offerer = address(0); + execution.item.recipient = payable(0); } } } @@ -199,7 +221,7 @@ contract FulfillmentApplier is FulfillmentApplicationErrors { AdvancedOrder[] memory advancedOrders, FulfillmentComponent[] memory offerComponents, Execution memory execution - ) internal view { + ) internal pure { assembly { // Declare function for reverts on invalid fulfillment data. function throwInvalidFulfillmentComponentData() { @@ -291,12 +313,6 @@ contract FulfillmentApplier is FulfillmentApplicationErrors { // Retrieve the received item pointer. let receivedItemPtr := mload(execution) - // Set the caller as the recipient on the received item. - mstore( - add(receivedItemPtr, ReceivedItem_recipient_offset), - caller() - ) - // Set the item type on the received item. mstore(receivedItemPtr, mload(offerItemPtr)) @@ -402,7 +418,7 @@ contract FulfillmentApplier is FulfillmentApplicationErrors { // Add offer amount to execution amount. let newAmount := add(amount, mload(amountPtr)) - // Update error buffer (1 = zero amount, 2 = overflow). + // Update error buffer: 1 = zero amount, 2 = overflow, 3 = both. errorBuffer := or( errorBuffer, or( @@ -461,17 +477,19 @@ contract FulfillmentApplier is FulfillmentApplicationErrors { // Write final amount to execution. mstore(add(mload(execution), Common_amount_offset), amount) - // Determine if an error code is contained in the error buffer. - switch errorBuffer - case 1 { - // Store the MissingItemAmount error signature. - mstore(0, MissingItemAmount_error_signature) + // Determine whether the error buffer contains a nonzero error code. + if errorBuffer { + // If errorBuffer is 1, an item had an amount of zero. + if eq(errorBuffer, 1) { + // Store the MissingItemAmount error signature. + mstore(0, MissingItemAmount_error_signature) - // Return, supplying MissingItemAmount signature. - revert(0, MissingItemAmount_error_len) - } - case 2 { - // If the sum overflowed, panic. + // Return, supplying MissingItemAmount signature. + revert(0, MissingItemAmount_error_len) + } + + // If errorBuffer is not 1 or 0, the sum overflowed. + // Panic! throwOverflow() } } @@ -692,7 +710,7 @@ contract FulfillmentApplier is FulfillmentApplicationErrors { // Add offer amount to execution amount. let newAmount := add(amount, mload(amountPtr)) - // Update error buffer (1 = zero amount, 2 = overflow). + // Update error buffer: 1 = zero amount, 2 = overflow, 3 = both. errorBuffer := or( errorBuffer, or( @@ -742,17 +760,19 @@ contract FulfillmentApplier is FulfillmentApplicationErrors { // Write final amount to execution. mstore(add(receivedItem, Common_amount_offset), amount) - // Determine if an error code is contained in the error buffer. - switch errorBuffer - case 1 { - // Store the MissingItemAmount error signature. - mstore(0, MissingItemAmount_error_signature) + // Determine whether the error buffer contains a nonzero error code. + if errorBuffer { + // If errorBuffer is 1, an item had an amount of zero. + if eq(errorBuffer, 1) { + // Store the MissingItemAmount error signature. + mstore(0, MissingItemAmount_error_signature) - // Return, supplying MissingItemAmount signature. - revert(0, MissingItemAmount_error_len) - } - case 2 { - // If the sum overflowed, panic. + // Return, supplying MissingItemAmount signature. + revert(0, MissingItemAmount_error_len) + } + + // If errorBuffer is not 1 or 0, the sum overflowed. + // Panic! throwOverflow() } } diff --git a/contracts/lib/GettersAndDerivers.sol b/contracts/lib/GettersAndDerivers.sol index 664d1328c..4b0d16357 100644 --- a/contracts/lib/GettersAndDerivers.sol +++ b/contracts/lib/GettersAndDerivers.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { OrderParameters } from "./ConsiderationStructs.sol"; @@ -33,13 +33,13 @@ contract GettersAndDerivers is ConsiderationBase { * caller. * * @param orderParameters The parameters of the order to hash. - * @param nonce The nonce of the order to hash. + * @param counter The counter of the order to hash. * * @return orderHash The hash. */ function _deriveOrderHash( OrderParameters memory orderParameters, - uint256 nonce + uint256 counter ) internal view returns (bytes32 orderHash) { // Get length of original consideration array and place it on the stack. uint256 originalConsiderationLength = ( @@ -65,7 +65,9 @@ contract GettersAndDerivers is ConsiderationBase { let hashArrPtr := mload(FreeMemoryPointerSlot) // Get the pointer to the offers array. - let offerArrPtr := mload(add(orderParameters, TwoWords)) + let offerArrPtr := mload( + add(orderParameters, OrderParameters_offer_head_offset) + ) // Load the length. let offerLength := mload(offerArrPtr) @@ -128,7 +130,7 @@ contract GettersAndDerivers is ConsiderationBase { OneWord ) - // Iterate over the offer items (not including tips). + // Iterate over the consideration items (not including tips). // prettier-ignore for { let i := 0 } lt(i, originalConsiderationLength) { i := add(i, 1) @@ -144,7 +146,10 @@ contract GettersAndDerivers is ConsiderationBase { mstore(ptr, typeHash) // Take the EIP712 hash and store it in the hash array. - mstore(hashArrPtr, keccak256(ptr, EIP712_ConsiderationItem_size)) + mstore( + hashArrPtr, + keccak256(ptr, EIP712_ConsiderationItem_size) + ) // Restore the previous word. mstore(ptr, value) @@ -199,11 +204,14 @@ contract GettersAndDerivers is ConsiderationBase { // Store the consideration hash at the retrieved memory location. mstore(considerationHeadPtr, considerationHash) - // Retrieve the pointer for the nonce. - let noncePtr := add(orderParameters, OrderParameters_nonce_offset) + // Retrieve the pointer for the counter. + let counterPtr := add( + orderParameters, + OrderParameters_counter_offset + ) - // Store the nonce at the retrieved memory location. - mstore(noncePtr, nonce) + // Store the counter at the retrieved memory location. + mstore(counterPtr, counter) // Derive the order hash using the full range of order parameters. orderHash := keccak256(typeHashPtr, EIP712_Order_size) @@ -217,8 +225,8 @@ contract GettersAndDerivers is ConsiderationBase { // Restore consideration data pointer at the consideration head ptr. mstore(considerationHeadPtr, considerationDataPtr) - // Restore original consideration item length at the nonce pointer. - mstore(noncePtr, originalConsiderationLength) + // Restore consideration item length at the counter pointer. + mstore(counterPtr, originalConsiderationLength) } } @@ -283,6 +291,8 @@ contract GettersAndDerivers is ConsiderationBase { * chainId matches the chainId set on deployment, the cached domain * separator will be returned; otherwise, it will be derived from * scratch. + * + * @return The domain separator. */ function _domainSeparator() internal view returns (bytes32) { // prettier-ignore @@ -319,7 +329,7 @@ contract GettersAndDerivers is ConsiderationBase { // Set the version as data on the newly allocated string. assembly { - mstore(add(version, OneWord), shl(0xf8, Version)) + mstore(add(version, OneWord), shl(Version_shift, Version)) } } diff --git a/contracts/lib/LowLevelHelpers.sol b/contracts/lib/LowLevelHelpers.sol index 8c22c9deb..c3bba2398 100644 --- a/contracts/lib/LowLevelHelpers.sol +++ b/contracts/lib/LowLevelHelpers.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import "./ConsiderationConstants.sol"; @@ -51,7 +51,10 @@ contract LowLevelHelpers { // Ensure that sufficient gas is available to copy returndata // while expanding memory where necessary. Start by computing // the word size of returndata and allocated memory. - let returnDataWords := div(returndatasize(), OneWord) + let returnDataWords := div( + add(returndatasize(), AlmostOneWord), + OneWord + ) // Note: use the free memory pointer in place of msize() to work // around a Yul warning that prevents accessing msize directly diff --git a/contracts/lib/NonceManager.sol b/contracts/lib/NonceManager.sol deleted file mode 100644 index a22c25c7e..000000000 --- a/contracts/lib/NonceManager.sol +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.13; - -// prettier-ignore -import { - ConsiderationEventsAndErrors -} from "../interfaces/ConsiderationEventsAndErrors.sol"; - -import { ReentrancyGuard } from "./ReentrancyGuard.sol"; - -/** - * @title NonceManager - * @author 0age - * @notice NonceManager contains a storage mapping and related functionality - * for retrieving and incrementing a per-offerer nonce. - */ -contract NonceManager is ConsiderationEventsAndErrors, ReentrancyGuard { - // Only orders signed using an offerer's current nonce are fulfillable. - mapping(address => uint256) private _nonces; - - /** - * @dev Internal function to cancel all orders from a given offerer with a - * given zone in bulk by incrementing a nonce. Note that only the - * offerer may increment the nonce. - * - * @return newNonce The new nonce. - */ - function _incrementNonce() internal returns (uint256 newNonce) { - // Ensure that the reentrancy guard is not currently set. - _assertNonReentrant(); - - // No need to check for overflow; nonce cannot be incremented that far. - unchecked { - // Increment current nonce for the supplied offerer. - newNonce = ++_nonces[msg.sender]; - } - - // Emit an event containing the new nonce. - emit NonceIncremented(newNonce, msg.sender); - } - - /** - * @dev Internal view function to retrieve the current nonce for a given - * offerer. - * - * @param offerer The offerer in question. - * - * @return currentNonce The current nonce. - */ - function _getNonce(address offerer) - internal - view - returns (uint256 currentNonce) - { - // Return the nonce for the supplied offerer. - currentNonce = _nonces[offerer]; - } -} diff --git a/contracts/lib/OrderCombiner.sol b/contracts/lib/OrderCombiner.sol index babef4942..7de31ccef 100644 --- a/contracts/lib/OrderCombiner.sol +++ b/contracts/lib/OrderCombiner.sol @@ -1,14 +1,12 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { Side, ItemType } from "./ConsiderationEnums.sol"; // prettier-ignore import { - AdditionalRecipient, OfferItem, ConsiderationItem, - SpentItem, ReceivedItem, OrderParameters, Fulfillment, @@ -79,7 +77,7 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { * is contained in the merkle root held by * the item in question's criteria element. * Note that an empty criteria indicates - * that any (transferrable) token + * that any (transferable) token * identifier on the token in question is * valid and that no associated proof needs * to be supplied. @@ -95,6 +93,8 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { * approvals from. The zero hash signifies * that no conduit should be used (and * direct approvals set on Consideration). + * @param recipient The intended recipient for all received + * items. * @param maximumFulfilled The maximum number of orders to fulfill. * * @return availableOrders An array of booleans indicating if each order @@ -110,6 +110,7 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { FulfillmentComponent[][] calldata offerFulfillments, FulfillmentComponent[][] calldata considerationFulfillments, bytes32 fulfillerConduitKey, + address recipient, uint256 maximumFulfilled ) internal @@ -120,7 +121,8 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { advancedOrders, criteriaResolvers, false, // Signifies that invalid orders should NOT revert. - maximumFulfilled + maximumFulfilled, + recipient ); // Aggregate used offer and consideration items and execute transfers. @@ -128,7 +130,8 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { advancedOrders, offerFulfillments, considerationFulfillments, - fulfillerConduitKey + fulfillerConduitKey, + recipient ); // Return order fulfillment details and executions. @@ -147,19 +150,21 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { * offer or consideration, a token identifier, and * a proof that the supplied token identifier is * contained in the order's merkle root. Note that - * a root of zero indicates that any transferrable + * a root of zero indicates that any transferable * token identifier is valid and that no proof * needs to be supplied. * @param revertOnInvalid A boolean indicating whether to revert on any * order being invalid; setting this to false will * instead cause the invalid order to be skipped. * @param maximumFulfilled The maximum number of orders to fulfill. + * @param recipient The intended recipient for all received items. */ function _validateOrdersAndPrepareToFulfill( AdvancedOrder[] memory advancedOrders, CriteriaResolver[] memory criteriaResolvers, bool revertOnInvalid, - uint256 maximumFulfilled + uint256 maximumFulfilled, + address recipient ) internal { // Ensure this function cannot be triggered during a reentrant call. _setReentrancyGuard(); @@ -175,6 +180,32 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { mstore(orderHashes, 0) } + // Declare an error buffer indicating status of any native offer items. + // {00} == 0 => In a match function, no native offer items: allow. + // {01} == 1 => In a match function, some native offer items: allow. + // {10} == 2 => Not in a match function, no native offer items: allow. + // {11} == 3 => Not in a match function, some native offer items: THROW. + uint256 invalidNativeOfferItemErrorBuffer; + + // Use assembly to set the value for the second bit of the error buffer. + assembly { + // Use the second bit of the error buffer to indicate whether the + // current function is not matchAdvancedOrders or matchOrders. + invalidNativeOfferItemErrorBuffer := shl( + 1, + gt( + // Take the remainder of the selector modulo a magic value. + mod( + shr(NumBitsAfterSelector, calldataload(0)), + NonMatchSelector_MagicModulus + ), + // Check if remainder is higher than the greatest remainder + // of the two match selectors modulo the magic value. + NonMatchSelector_MagicRemainder + ) + ) + } + // Skip overflow checks as all for loops are indexed starting at zero. unchecked { // Iterate over each order. @@ -226,28 +257,36 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { orderHashes[i] = orderHash; // Decrement the number of fulfilled orders. + // Skip underflow check as the condition before + // implies that maximumFulfilled > 0. maximumFulfilled--; // Place the start time for the order on the stack. uint256 startTime = advancedOrder.parameters.startTime; - // Derive the duration for the order and place it on the stack. - uint256 duration = advancedOrder.parameters.endTime - startTime; - - // Derive time elapsed since the order started & place on stack. - uint256 elapsed = block.timestamp - startTime; - - // Derive time remaining until order expires and place on stack. - uint256 remaining = duration - elapsed; + // Place the end time for the order on the stack. + uint256 endTime = advancedOrder.parameters.endTime; // Retrieve array of offer items for the order in question. OfferItem[] memory offer = advancedOrder.parameters.offer; + // Read length of offer array and place on the stack. + uint256 totalOfferItems = offer.length; + // Iterate over each offer item on the order. - for (uint256 j = 0; j < offer.length; ++j) { + for (uint256 j = 0; j < totalOfferItems; ++j) { // Retrieve the offer item. OfferItem memory offerItem = offer[j]; + assembly { + // If the offer item is for the native token, set the + // first bit of the error buffer to true. + invalidNativeOfferItemErrorBuffer := or( + invalidNativeOfferItemErrorBuffer, + iszero(mload(offerItem)) + ) + } + // Apply order fill fraction to offer item end amount. uint256 endAmount = _getFraction( numerator, @@ -275,9 +314,8 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { offerItem.startAmount = _locateCurrentAmount( offerItem.startAmount, offerItem.endAmount, - elapsed, - remaining, - duration, + startTime, + endTime, false // round down ); } @@ -287,8 +325,11 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { advancedOrder.parameters.consideration ); + // Read length of consideration array and place on the stack. + uint256 totalConsiderationItems = consideration.length; + // Iterate over each consideration item on the order. - for (uint256 j = 0; j < consideration.length; ++j) { + for (uint256 j = 0; j < totalConsiderationItems; ++j) { // Retrieve the consideration item. ConsiderationItem memory considerationItem = ( consideration[j] @@ -325,9 +366,8 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { _locateCurrentAmount( considerationItem.startAmount, considerationItem.endAmount, - elapsed, - remaining, - duration, + startTime, + endTime, true // round up ) ); @@ -354,18 +394,17 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { } } + // If the first bit is set, a native offer item was encountered. If the + // second bit is set in the error buffer, the current function is not + // matchOrders or matchAdvancedOrders. If the value is three, both the + // first and second bits were set; in that case, revert with an error. + if (invalidNativeOfferItemErrorBuffer == 3) { + revert InvalidNativeOfferItem(); + } + // Apply criteria resolvers to each order as applicable. _applyCriteriaResolvers(advancedOrders, criteriaResolvers); - // Determine the fulfiller (revertOnInvalid ? address(0) : msg.sender). - address fulfiller; - - // Utilize assembly to operate on revertOnInvalid boolean as an integer. - assembly { - // Set the fulfiller to the caller if revertOnValid is false. - fulfiller := mul(iszero(revertOnInvalid), caller()) - } - // Emit an event for each order signifying that it has been fulfilled. // Skip overflow checks as all for loops are indexed starting at zero. unchecked { @@ -386,7 +425,7 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { orderHashes[i], orderParameters.offerer, orderParameters.zone, - fulfiller, + recipient, orderParameters.offer, orderParameters.consideration ); @@ -434,6 +473,8 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { * approvals from. The zero hash signifies * that no conduit should be used, with * direct approvals set on Consideration. + * @param recipient The intended recipient for all received + * items. * * @return availableOrders An array of booleans indicating if each order * with an index corresponding to the index of the @@ -446,7 +487,8 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { AdvancedOrder[] memory advancedOrders, FulfillmentComponent[][] memory offerFulfillments, FulfillmentComponent[][] memory considerationFulfillments, - bytes32 fulfillerConduitKey + bytes32 fulfillerConduitKey, + address recipient ) internal returns (bool[] memory availableOrders, Execution[] memory executions) @@ -481,13 +523,14 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { advancedOrders, Side.OFFER, components, - fulfillerConduitKey + fulfillerConduitKey, + recipient ); // If offerer and recipient on the execution are the same... if (execution.item.recipient == execution.offerer) { - // increment total filtered executions. - totalFilteredExecutions += 1; + // Increment total filtered executions. + ++totalFilteredExecutions; } else { // Otherwise, assign the execution to the executions array. executions[i - totalFilteredExecutions] = execution; @@ -506,13 +549,14 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { advancedOrders, Side.CONSIDERATION, components, - fulfillerConduitKey + fulfillerConduitKey, + address(0) // unused ); // If offerer and recipient on the execution are the same... if (execution.item.recipient == execution.offerer) { - // increment total filtered executions. - totalFilteredExecutions += 1; + // Increment total filtered executions. + ++totalFilteredExecutions; } else { // Otherwise, assign the execution to the executions array. executions[ @@ -594,8 +638,11 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { advancedOrder.parameters.consideration ); + // Read length of consideration array and place on the stack. + uint256 totalConsiderationItems = consideration.length; + // Iterate over each consideration item to ensure it is met. - for (uint256 j = 0; j < consideration.length; ++j) { + for (uint256 j = 0; j < totalConsiderationItems; ++j) { // Retrieve remaining amount on the consideration item. uint256 unmetAmount = consideration[j].startAmount; @@ -617,8 +664,11 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { // accessed and modified, however. bytes memory accumulator = new bytes(AccumulatorDisarmed); + // Retrieve the length of the executions array and place on stack. + uint256 totalExecutions = executions.length; + // Iterate over each execution. - for (uint256 i = 0; i < executions.length; ) { + for (uint256 i = 0; i < totalExecutions; ) { // Retrieve the execution and the associated received item. Execution memory execution = executions[i]; ReceivedItem memory item = execution.item; @@ -689,7 +739,7 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { * offer or consideration, a token identifier, and * a proof that the supplied token identifier is * contained in the order's merkle root. Note that - * an empty root indicates that any (transferrable) + * an empty root indicates that any (transferable) * token identifier is valid and that no associated * proof needs to be supplied. * @param fulfillments An array of elements allocating offer components @@ -711,7 +761,8 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { advancedOrders, criteriaResolvers, true, // Signifies that invalid orders should revert. - advancedOrders.length + advancedOrders.length, + address(0) // OrderFulfilled event has no recipient when matching. ); // Fulfill the orders using the supplied fulfillments. @@ -764,8 +815,8 @@ contract OrderCombiner is OrderFulfiller, FulfillmentApplier { // If offerer and recipient on the execution are the same... if (execution.item.recipient == execution.offerer) { - // increment total filtered executions. - totalFilteredExecutions += 1; + // Increment total filtered executions. + ++totalFilteredExecutions; } else { // Otherwise, assign the execution to the executions array. executions[i - totalFilteredExecutions] = execution; diff --git a/contracts/lib/OrderFulfiller.sol b/contracts/lib/OrderFulfiller.sol index 0060ba905..4a7fe6a21 100644 --- a/contracts/lib/OrderFulfiller.sol +++ b/contracts/lib/OrderFulfiller.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; -import { OrderType, ItemType } from "./ConsiderationEnums.sol"; +import { ItemType } from "./ConsiderationEnums.sol"; // prettier-ignore import { @@ -62,20 +62,22 @@ contract OrderFulfiller is * that the supplied token identifier is * contained in the order's merkle root. Note * that a criteria of zero indicates that any - * (transferrable) token identifier is valid and + * (transferable) token identifier is valid and * that no proof needs to be supplied. * @param fulfillerConduitKey A bytes32 value indicating what conduit, if * any, to source the fulfiller's token approvals * from. The zero hash signifies that no conduit * should be used, with direct approvals set on * Consideration. + * @param recipient The intended recipient for all received items. * * @return A boolean indicating whether the order has been fulfilled. */ function _validateAndFulfillAdvancedOrder( AdvancedOrder memory advancedOrder, CriteriaResolver[] memory criteriaResolvers, - bytes32 fulfillerConduitKey + bytes32 fulfillerConduitKey, + address recipient ) internal returns (bool) { // Ensure this function cannot be triggered during a reentrant call. _setReentrancyGuard(); @@ -112,8 +114,8 @@ contract OrderFulfiller is orderParameters, fillNumerator, fillDenominator, - orderParameters.conduitKey, - fulfillerConduitKey + fulfillerConduitKey, + recipient ); // Emit an event signifying that the order has been fulfilled. @@ -121,7 +123,7 @@ contract OrderFulfiller is orderHash, orderParameters.offerer, orderParameters.zone, - msg.sender, + recipient, orderParameters.offer, orderParameters.consideration ); @@ -141,31 +143,23 @@ contract OrderFulfiller is * @param numerator A value indicating the portion of the order * that should be filled. * @param denominator A value indicating the total order size. - * @param offererConduitKey An address indicating what conduit, if any, to - * source the offerer's token approvals from. The - * zero hash signifies that no conduit should be - * used, with direct approvals set on - * Consideration. * @param fulfillerConduitKey A bytes32 value indicating what conduit, if * any, to source the fulfiller's token approvals * from. The zero hash signifies that no conduit * should be used, with direct approvals set on * Consideration. + * @param recipient The intended recipient for all received items. */ function _applyFractionsAndTransferEach( OrderParameters memory orderParameters, uint256 numerator, uint256 denominator, - bytes32 offererConduitKey, - bytes32 fulfillerConduitKey + bytes32 fulfillerConduitKey, + address recipient ) internal { - // Derive order duration, time elapsed, and time remaining. - uint256 duration = orderParameters.endTime - orderParameters.startTime; - uint256 elapsed = block.timestamp - orderParameters.startTime; - uint256 remaining = duration - elapsed; - - // Put ether value supplied by the caller on the stack. - uint256 etherRemaining = msg.value; + // Read start time & end time from order parameters and place on stack. + uint256 startTime = orderParameters.startTime; + uint256 endTime = orderParameters.endTime; // Initialize an accumulator array. From this point forward, no new // memory regions can be safely allocated until the accumulator is no @@ -195,7 +189,7 @@ contract OrderFulfiller is */ // Declare a nested scope to minimize stack depth. - { + unchecked { // Declare a virtual function pointer taking an OfferItem argument. function(OfferItem memory, address, bytes32, bytes memory) internal _transferOfferItem; @@ -213,62 +207,63 @@ contract OrderFulfiller is } } + // Read offer array length from memory and place on stack. + uint256 totalOfferItems = orderParameters.offer.length; + // Iterate over each offer on the order. - for (uint256 i = 0; i < orderParameters.offer.length; ) { + // Skip overflow check as for loop is indexed starting at zero. + for (uint256 i = 0; i < totalOfferItems; ++i) { // Retrieve the offer item. OfferItem memory offerItem = orderParameters.offer[i]; - // Apply fill fraction to derive offer item amount to transfer. - uint256 amount = _applyFraction( - offerItem.startAmount, - offerItem.endAmount, - numerator, - denominator, - elapsed, - remaining, - duration, - false - ); - - // Utilize assembly to set overloaded offerItem arguments. - assembly { - // Write derived fractional amount to startAmount as amount. - mstore(add(offerItem, ReceivedItem_amount_offset), amount) - // Write fulfiller (i.e. caller) to endAmount as recipient. - mstore( - add(offerItem, ReceivedItem_recipient_offset), - caller() - ) + // Offer items for the native token can not be received + // outside of a match order function. + if (offerItem.itemType == ItemType.NATIVE) { + revert InvalidNativeOfferItem(); } - // Reduce available value if offer spent ETH or a native token. - if (offerItem.itemType == ItemType.NATIVE) { - // Ensure that sufficient native tokens are still available. - if (amount > etherRemaining) { - revert InsufficientEtherSupplied(); - } + // Declare an additional nested scope to minimize stack depth. + { + // Apply fill fraction to get offer item amount to transfer. + uint256 amount = _applyFraction( + offerItem.startAmount, + offerItem.endAmount, + numerator, + denominator, + startTime, + endTime, + false + ); + + // Utilize assembly to set overloaded offerItem arguments. + assembly { + // Write new fractional amount to startAmount as amount. + mstore( + add(offerItem, ReceivedItem_amount_offset), + amount + ) - // Skip underflow check as a comparison has just been made. - unchecked { - etherRemaining -= amount; + // Write recipient to endAmount. + mstore( + add(offerItem, ReceivedItem_recipient_offset), + recipient + ) } } - // Transfer the item from the offerer to the caller. + // Transfer the item from the offerer to the recipient. _transferOfferItem( offerItem, orderParameters.offerer, - offererConduitKey, + orderParameters.conduitKey, accumulator ); - - // Skip overflow check as for loop is indexed starting at zero. - unchecked { - ++i; - } } } + // Put ether value supplied by the caller on the stack. + uint256 etherRemaining = msg.value; + /** * Repurpose existing ConsiderationItem memory regions on the * consideration array for the order by overriding the _transfer @@ -285,7 +280,7 @@ contract OrderFulfiller is */ // Declare a nested scope to minimize stack depth. - { + unchecked { // Declare virtual function pointer with ConsiderationItem argument. function(ConsiderationItem memory, address, bytes32, bytes memory) internal _transferConsiderationItem; @@ -302,8 +297,14 @@ contract OrderFulfiller is } } + // Read consideration array length from memory and place on stack. + uint256 totalConsiderationItems = orderParameters + .consideration + .length; + // Iterate over each consideration item on the order. - for (uint256 i = 0; i < orderParameters.consideration.length; ) { + // Skip overflow check as for loop is indexed starting at zero. + for (uint256 i = 0; i < totalConsiderationItems; ++i) { // Retrieve the consideration item. ConsiderationItem memory considerationItem = ( orderParameters.consideration[i] @@ -315,9 +316,8 @@ contract OrderFulfiller is considerationItem.endAmount, numerator, denominator, - elapsed, - remaining, - duration, + startTime, + endTime, true ); @@ -349,9 +349,7 @@ contract OrderFulfiller is } // Skip underflow check as a comparison has just been made. - unchecked { - etherRemaining -= amount; - } + etherRemaining -= amount; } // Transfer item from caller to recipient specified by the item. @@ -361,11 +359,6 @@ contract OrderFulfiller is fulfillerConduitKey, accumulator ); - - // Skip overflow check as for loop is indexed starting at zero. - unchecked { - ++i; - } } } diff --git a/contracts/lib/OrderValidator.sol b/contracts/lib/OrderValidator.sol index 09f10cbbc..70e05f049 100644 --- a/contracts/lib/OrderValidator.sol +++ b/contracts/lib/OrderValidator.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { OrderType } from "./ConsiderationEnums.sol"; @@ -13,6 +13,8 @@ import { CriteriaResolver } from "./ConsiderationStructs.sol"; +import "./ConsiderationConstants.sol"; + import { Executor } from "./Executor.sol"; import { ZoneInteraction } from "./ZoneInteraction.sol"; @@ -51,7 +53,7 @@ contract OrderValidator is Executor, ZoneInteraction { bytes memory signature ) internal { // Retrieve the order status for the given order hash. - OrderStatus memory orderStatus = _orderStatus[orderHash]; + OrderStatus storage orderStatus = _orderStatus[orderHash]; // Ensure order is fillable and is not cancelled. _verifyOrderStatus( @@ -67,10 +69,10 @@ contract OrderValidator is Executor, ZoneInteraction { } // Update order status as fully filled, packing struct values. - _orderStatus[orderHash].isValidated = true; - _orderStatus[orderHash].isCancelled = false; - _orderStatus[orderHash].numerator = 1; - _orderStatus[orderHash].denominator = 1; + orderStatus.isValidated = true; + orderStatus.isCancelled = false; + orderStatus.numerator = 1; + orderStatus.denominator = 1; } /** @@ -87,7 +89,7 @@ contract OrderValidator is Executor, ZoneInteraction { * identifier, and a proof that the supplied token * identifier is contained in the order's merkle * root. Note that a criteria of zero indicates - * that any (transferrable) token identifier is + * that any (transferable) token identifier is * valid and that no proof needs to be supplied. * @param revertOnInvalid A boolean indicating whether to revert if the * order is invalid due to the time or status. @@ -147,10 +149,8 @@ contract OrderValidator is Executor, ZoneInteraction { revert PartialFillsNotEnabledForOrder(); } - // Retrieve current nonce and use it w/ parameters to derive order hash. - orderHash = _assertConsiderationLengthAndGetNoncedOrderHash( - orderParameters - ); + // Retrieve current counter & use it w/ parameters to derive order hash. + orderHash = _assertConsiderationLengthAndGetOrderHash(orderParameters); // Ensure restricted orders have a valid submitter or pass a zone check. _assertRestrictedAdvancedOrderValidity( @@ -165,7 +165,7 @@ contract OrderValidator is Executor, ZoneInteraction { ); // Retrieve the order status using the derived order hash. - OrderStatus memory orderStatus = _orderStatus[orderHash]; + OrderStatus storage orderStatus = _orderStatus[orderHash]; // Ensure order is fillable and is not cancelled. if ( @@ -193,7 +193,8 @@ contract OrderValidator is Executor, ZoneInteraction { uint256 filledNumerator = orderStatus.numerator; uint256 filledDenominator = orderStatus.denominator; - // If order currently has a non-zero denominator it is partially filled. + // If order (orderStatus) currently has a non-zero denominator it is + // partially filled. if (filledDenominator != 0) { // If denominator of 1 supplied, fill all remaining amount on order. if (denominator == 1) { @@ -220,22 +221,72 @@ contract OrderValidator is Executor, ZoneInteraction { } } + // Increment the filled numerator by the new numerator. + filledNumerator += numerator; + + // Use assembly to ensure fractional amounts are below max uint120. + assembly { + // Check filledNumerator and denominator for uint120 overflow. + if or( + gt(filledNumerator, MaxUint120), + gt(denominator, MaxUint120) + ) { + // Derive greatest common divisor using euclidean algorithm. + function gcd(_a, _b) -> out { + for { + + } _b { + + } { + let _c := _b + _b := mod(_a, _c) + _a := _c + } + out := _a + } + let scaleDown := gcd( + numerator, + gcd(filledNumerator, denominator) + ) + + // Ensure that the divisor is at least one. + let safeScaleDown := add(scaleDown, iszero(scaleDown)) + + // Scale all fractional values down by gcd. + numerator := div(numerator, safeScaleDown) + filledNumerator := div(filledNumerator, safeScaleDown) + denominator := div(denominator, safeScaleDown) + + // Perform the overflow check a second time. + if or( + gt(filledNumerator, MaxUint120), + gt(denominator, MaxUint120) + ) { + // Store the Panic error signature. + mstore(0, Panic_error_signature) + + // Set arithmetic (0x11) panic code as initial argument. + mstore(Panic_error_offset, Panic_arithmetic) + + // Return, supplying Panic signature & arithmetic code. + revert(0, Panic_error_length) + } + } + } // Skip overflow check: checked above unless numerator is reduced. unchecked { // Update order status and fill amount, packing struct values. - _orderStatus[orderHash].isValidated = true; - _orderStatus[orderHash].isCancelled = false; - _orderStatus[orderHash].numerator = uint120( - filledNumerator + numerator - ); - _orderStatus[orderHash].denominator = uint120(denominator); + orderStatus.isValidated = true; + orderStatus.isCancelled = false; + orderStatus.numerator = uint120(filledNumerator); + orderStatus.denominator = uint120(denominator); } } else { // Update order status and fill amount, packing struct values. - _orderStatus[orderHash].isValidated = true; - _orderStatus[orderHash].isCancelled = false; - _orderStatus[orderHash].numerator = uint120(numerator); - _orderStatus[orderHash].denominator = uint120(denominator); + orderStatus.isValidated = true; + orderStatus.isCancelled = false; + orderStatus.numerator = uint120(numerator); + orderStatus.denominator = uint120(denominator); } // Return order hash, a modified numerator, and a modified denominator. @@ -260,6 +311,8 @@ contract OrderValidator is Executor, ZoneInteraction { // Ensure that the reentrancy guard is not currently set. _assertNonReentrant(); + // Declare variables outside of the loop. + OrderStatus storage orderStatus; address offerer; address zone; @@ -281,7 +334,7 @@ contract OrderValidator is Executor, ZoneInteraction { revert InvalidCanceller(); } - // Derive order hash using the order parameters and the nonce. + // Derive order hash using the order parameters and the counter. bytes32 orderHash = _deriveOrderHash( OrderParameters( offerer, @@ -296,12 +349,15 @@ contract OrderValidator is Executor, ZoneInteraction { order.conduitKey, order.consideration.length ), - order.nonce + order.counter ); + // Retrieve the order status using the derived order hash. + orderStatus = _orderStatus[orderHash]; + // Update the order status as not valid and cancelled. - _orderStatus[orderHash].isValidated = false; - _orderStatus[orderHash].isCancelled = true; + orderStatus.isValidated = false; + orderStatus.isCancelled = true; // Emit an event signifying that the order has been cancelled. emit OrderCancelled(orderHash, offerer, zone); @@ -338,6 +394,7 @@ contract OrderValidator is Executor, ZoneInteraction { _assertNonReentrant(); // Declare variables outside of the loop. + OrderStatus storage orderStatus; bytes32 orderHash; address offerer; @@ -357,13 +414,13 @@ contract OrderValidator is Executor, ZoneInteraction { // Move offerer from memory to the stack. offerer = orderParameters.offerer; - // Get current nonce and use it w/ params to derive order hash. - orderHash = _assertConsiderationLengthAndGetNoncedOrderHash( + // Get current counter & use it w/ params to derive order hash. + orderHash = _assertConsiderationLengthAndGetOrderHash( orderParameters ); // Retrieve the order status using the derived order hash. - OrderStatus memory orderStatus = _orderStatus[orderHash]; + orderStatus = _orderStatus[orderHash]; // Ensure order is fillable and retrieve the filled amount. _verifyOrderStatus( @@ -379,7 +436,7 @@ contract OrderValidator is Executor, ZoneInteraction { _verifySignature(offerer, orderHash, order.signature); // Update order status to mark the order as valid. - _orderStatus[orderHash].isValidated = true; + orderStatus.isValidated = true; // Emit an event signifying the order has been validated. emit OrderValidated( @@ -426,7 +483,7 @@ contract OrderValidator is Executor, ZoneInteraction { ) { // Retrieve the order status using the order hash. - OrderStatus memory orderStatus = _orderStatus[orderHash]; + OrderStatus storage orderStatus = _orderStatus[orderHash]; // Return the fields on the order status. return ( diff --git a/contracts/lib/ReentrancyGuard.sol b/contracts/lib/ReentrancyGuard.sol index a1220c357..69169fe0a 100644 --- a/contracts/lib/ReentrancyGuard.sol +++ b/contracts/lib/ReentrancyGuard.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { ReentrancyErrors } from "../interfaces/ReentrancyErrors.sol"; diff --git a/contracts/lib/SignatureVerification.sol b/contracts/lib/SignatureVerification.sol index 3ba592362..023d3c25f 100644 --- a/contracts/lib/SignatureVerification.sol +++ b/contracts/lib/SignatureVerification.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { EIP1271Interface } from "../interfaces/EIP1271Interface.sol"; @@ -21,10 +21,8 @@ contract SignatureVerification is SignatureVerificationErrors, LowLevelHelpers { /** * @dev Internal view function to verify the signature of an order. An * ERC-1271 fallback will be attempted if either the signature length - * is not 32 or 33 bytes or if the recovered signer does not match the - * supplied signer. Note that in cases where a 32 or 33 byte signature - * is supplied, only standard ECDSA signatures that recover to a - * non-zero address are supported. + * is not 64 or 65 bytes or if the recovered signer does not match the + * supplied signer. * * @param signer The signer for the order. * @param digest The digest to verify the signature against. @@ -36,110 +34,221 @@ contract SignatureVerification is SignatureVerificationErrors, LowLevelHelpers { bytes32 digest, bytes memory signature ) internal view { - // Declare r, s, and v signature parameters. - bytes32 r; - bytes32 s; - uint8 v; - - // If signature contains 64 bytes, parse as EIP-2098 signature (r+s&v). - if (signature.length == 64) { - // Declare temporary vs that will be decomposed into s and v. - bytes32 vs; - - // Read each parameter directly from the signature's memory region. - assembly { - // Put the first word from the signature onto the stack as r. - r := mload(add(signature, OneWord)) - - // Put the second word from the signature onto the stack as vs. - vs := mload(add(signature, TwoWords)) - - // Extract canonical s from vs (all but the highest bit). - s := and(vs, EIP2098_allButHighestBitMask) - - // Extract yParity from highest bit of vs and add 27 to get v. - v := add(shr(255, vs), 27) - } - } else if (signature.length == 65) { - // If signature is 65 bytes, parse as a standard signature (r+s+v). - // Read each parameter directly from the signature's memory region. - assembly { - // Place first word on the stack at r. - r := mload(add(signature, OneWord)) - - // Place second word on the stack at s. - s := mload(add(signature, TwoWords)) - - // Place final byte on the stack at v. - v := byte(0, mload(add(signature, ThreeWords))) + // Declare value for ecrecover equality or 1271 call success status. + bool success; + + // Utilize assembly to perform optimized signature verification check. + assembly { + // Ensure that first word of scratch space is empty. + mstore(0, 0) + + // Declare value for v signature parameter. + let v + + // Get the length of the signature. + let signatureLength := mload(signature) + + // Get the pointer to the value preceding the signature length. + // This will be used for temporary memory overrides - either the + // signature head for isValidSignature or the digest for ecrecover. + let wordBeforeSignaturePtr := sub(signature, OneWord) + + // Cache the current value behind the signature to restore it later. + let cachedWordBeforeSignature := mload(wordBeforeSignaturePtr) + + // Declare lenDiff + recoveredSigner scope to manage stack pressure. + { + // Take the difference between the max ECDSA signature length + // and the actual signature length. Overflow desired for any + // values > 65. If the diff is not 0 or 1, it is not a valid + // ECDSA signature - move on to EIP1271 check. + let lenDiff := sub(ECDSA_MaxLength, signatureLength) + + // Declare variable for recovered signer. + let recoveredSigner + + // If diff is 0 or 1, it may be an ECDSA signature. + // Try to recover signer. + if iszero(gt(lenDiff, 1)) { + // Read the signature `s` value. + let originalSignatureS := mload( + add(signature, ECDSA_signature_s_offset) + ) + + // Read the first byte of the word after `s`. If the + // signature is 65 bytes, this will be the real `v` value. + // If not, it will need to be modified - doing it this way + // saves an extra condition. + v := byte( + 0, + mload(add(signature, ECDSA_signature_v_offset)) + ) + + // If lenDiff is 1, parse 64-byte signature as ECDSA. + if lenDiff { + // Extract yParity from highest bit of vs and add 27 to + // get v. + v := add( + shr(MaxUint8, originalSignatureS), + Signature_lower_v + ) + + // Extract canonical s from vs, all but the highest bit. + // Temporarily overwrite the original `s` value in the + // signature. + mstore( + add(signature, ECDSA_signature_s_offset), + and( + originalSignatureS, + EIP2098_allButHighestBitMask + ) + ) + } + // Temporarily overwrite the signature length with `v` to + // conform to the expected input for ecrecover. + mstore(signature, v) + + // Temporarily overwrite the word before the length with + // `digest` to conform to the expected input for ecrecover. + mstore(wordBeforeSignaturePtr, digest) + + // Attempt to recover the signer for the given signature. Do + // not check the call status as ecrecover will return a null + // address if the signature is invalid. + pop( + staticcall( + gas(), + Ecrecover_precompile, // Call ecrecover precompile. + wordBeforeSignaturePtr, // Use data memory location. + Ecrecover_args_size, // Size of digest, v, r, and s. + 0, // Write result to scratch space. + OneWord // Provide size of returned result. + ) + ) + + // Restore cached word before signature. + mstore(wordBeforeSignaturePtr, cachedWordBeforeSignature) + + // Restore cached signature length. + mstore(signature, signatureLength) + + // Restore cached signature `s` value. + mstore( + add(signature, ECDSA_signature_s_offset), + originalSignatureS + ) + + // Read the recovered signer from the buffer given as return + // space for ecrecover. + recoveredSigner := mload(0) + } + + // Set success to true if the signature provided was a valid + // ECDSA signature and the signer is not the null address. Use + // gt instead of direct as success is used outside of assembly. + success := and(eq(signer, recoveredSigner), gt(signer, 0)) } - // Ensure v value is properly formatted. - if (v != 27 && v != 28) { - revert BadSignatureV(v); + // If the signature was not verified with ecrecover, try EIP1271. + if iszero(success) { + // Temporarily overwrite the word before the signature length + // and use it as the head of the signature input to + // `isValidSignature`, which has a value of 64. + mstore( + wordBeforeSignaturePtr, + EIP1271_isValidSignature_signature_head_offset + ) + + // Get pointer to use for the selector of `isValidSignature`. + let selectorPtr := sub( + signature, + EIP1271_isValidSignature_selector_negativeOffset + ) + + // Cache the value currently stored at the selector pointer. + let cachedWordOverwrittenBySelector := mload(selectorPtr) + + // Get pointer to use for `digest` input to `isValidSignature`. + let digestPtr := sub( + signature, + EIP1271_isValidSignature_digest_negativeOffset + ) + + // Cache the value currently stored at the digest pointer. + let cachedWordOverwrittenByDigest := mload(digestPtr) + + // Write the selector first, since it overlaps the digest. + mstore(selectorPtr, EIP1271_isValidSignature_selector) + + // Next, write the digest. + mstore(digestPtr, digest) + + // Call signer with `isValidSignature` to validate signature. + success := staticcall( + gas(), + signer, + selectorPtr, + add( + signatureLength, + EIP1271_isValidSignature_calldata_baseLength + ), + 0, + OneWord + ) + + // Determine if the signature is valid on successful calls. + if success { + // If first word of scratch space does not contain EIP-1271 + // signature selector, revert. + if iszero(eq(mload(0), EIP1271_isValidSignature_selector)) { + // Revert with bad 1271 signature if signer has code. + if extcodesize(signer) { + // Bad contract signature. + mstore(0, BadContractSignature_error_signature) + revert(0, BadContractSignature_error_length) + } + + // Check if signature length was invalid. + if gt(sub(ECDSA_MaxLength, signatureLength), 1) { + // Revert with generic invalid signature error. + mstore(0, InvalidSignature_error_signature) + revert(0, InvalidSignature_error_length) + } + + // Check if v was invalid. + if iszero( + byte(v, ECDSA_twentySeventhAndTwentyEighthBytesSet) + ) { + // Revert with invalid v value. + mstore(0, BadSignatureV_error_signature) + mstore(BadSignatureV_error_offset, v) + revert(0, BadSignatureV_error_length) + } + + // Revert with generic invalid signer error message. + mstore(0, InvalidSigner_error_signature) + revert(0, InvalidSigner_error_length) + } + } + + // Restore the cached values overwritten by selector, digest and + // signature head. + mstore(wordBeforeSignaturePtr, cachedWordBeforeSignature) + mstore(selectorPtr, cachedWordOverwrittenBySelector) + mstore(digestPtr, cachedWordOverwrittenByDigest) } - } else { - // For all other signature lengths, try verification via EIP-1271. - // Attempt EIP-1271 static call to signer in case it's a contract. - _assertValidEIP1271Signature(signer, digest, signature); - - // Return early if the ERC-1271 signature check succeeded. - return; } - // Attempt to recover signer using the digest and signature parameters. - address recoveredSigner = ecrecover(digest, v, r, s); - - // Disallow invalid signers. - if (recoveredSigner == address(0)) { - revert InvalidSignature(); - // Should a signer be recovered, but it doesn't match the signer... - } else if (recoveredSigner != signer) { - // Attempt EIP-1271 static call to signer in case it's a contract. - _assertValidEIP1271Signature(signer, digest, signature); - } - } - - /** - * @dev Internal view function to verify the signature of an order using - * ERC-1271 (i.e. contract signatures via `isValidSignature`). Note - * that, in contrast to standard ECDSA signatures, 1271 signatures may - * be valid in certain contexts and invalid in others, or vice versa; - * orders that validate signatures ahead of time must explicitly cancel - * those orders to invalidate them. - * - * @param signer The signer for the order. - * @param digest The signature digest, derived from the domain separator - * and the order hash. - * @param signature A signature (or other data) used to validate the digest. - */ - function _assertValidEIP1271Signature( - address signer, - bytes32 digest, - bytes memory signature - ) internal view { - // Attempt an EIP-1271 staticcall to the signer. - bool success = _staticcall( - signer, - abi.encodeWithSelector( - EIP1271Interface.isValidSignature.selector, - digest, - signature - ) - ); - - // If the call fails... + // If the call failed... if (!success) { // Revert and pass reason along if one was returned. _revertWithReasonIfOneIsReturned(); - // Otherwise, revert with a generic error message. - revert BadContractSignature(); - } - - // Ensure result was extracted and matches EIP-1271 magic value. - if (_doesNotMatchMagic(EIP1271Interface.isValidSignature.selector)) { - revert InvalidSigner(); + // Otherwise, revert with error indicating bad contract signature. + assembly { + mstore(0, BadContractSignature_error_signature) + revert(0, BadContractSignature_error_length) + } } } } diff --git a/contracts/lib/TokenTransferrer.sol b/contracts/lib/TokenTransferrer.sol index 0c4d45a2b..197a28355 100644 --- a/contracts/lib/TokenTransferrer.sol +++ b/contracts/lib/TokenTransferrer.sol @@ -10,6 +10,18 @@ import { import { ConduitBatch1155Transfer } from "../conduit/lib/ConduitStructs.sol"; +/** + * @title TokenTransferrer + * @author 0age + * @custom:coauthor d1ll0n + * @custom:coauthor transmissions11 + * @notice TokenTransferrer is a library for performing optimized ERC20, ERC721, + * ERC1155, and batch ERC1155 transfers, used by both Seaport as well as + * by conduits deployed by the ConduitController. Use great caution when + * considering these functions for use in other codebases, as there are + * significant side effects and edge cases that need to be thoroughly + * understood and carefully addressed. + */ contract TokenTransferrer is TokenTransferrerErrors { /** * @dev Internal function to transfer ERC20 tokens from a given originator @@ -29,16 +41,22 @@ contract TokenTransferrer is TokenTransferrerErrors { ) internal { // Utilize assembly to perform an optimized ERC20 token transfer. assembly { - // Write calldata to the free memory pointer, but restore it later. + // The free memory pointer memory slot will be used when populating + // call data for the transfer; read the value and restore it later. let memPointer := mload(FreeMemoryPointerSlot) - // Write calldata into memory, starting with function selector. + // Write call data into memory, starting with function selector. mstore(ERC20_transferFrom_sig_ptr, ERC20_transferFrom_signature) mstore(ERC20_transferFrom_from_ptr, from) mstore(ERC20_transferFrom_to_ptr, to) mstore(ERC20_transferFrom_amount_ptr, amount) // Make call & copy up to 32 bytes of return data to scratch space. + // Scratch space does not need to be cleared ahead of time, as the + // subsequent check will ensure that either at least a full word of + // return data is received (in which case it will be overwritten) or + // that no data is received (in which case scratch space will be + // ignored) on a successful call to the given token. let callStatus := call( gas(), token, @@ -61,14 +79,14 @@ contract TokenTransferrer is TokenTransferrerErrors { callStatus ) - // If the transfer failed or it returned nothing: - // Group these because they should be uncommon. + // Handle cases where either the transfer failed or no data was + // returned. Group these, as most transfers will succeed with data. // Equivalent to `or(iszero(success), iszero(returndatasize()))` // but after it's inverted for JUMPI this expression is cheaper. if iszero(and(success, iszero(iszero(returndatasize())))) { - // If the token has no code or the transfer failed: - // Equivalent to `or(iszero(success), iszero(extcodesize(token)))` - // but after it's inverted for JUMPI this expression is cheaper. + // If the token has no code or the transfer failed: Equivalent + // to `or(iszero(success), iszero(extcodesize(token)))` but + // after it's inverted for JUMPI this expression is cheaper. if iszero(and(iszero(iszero(extcodesize(token))), success)) { // If the transfer failed: if iszero(success) { @@ -80,9 +98,10 @@ contract TokenTransferrer is TokenTransferrerErrors { // Ensure that sufficient gas is available to // copy returndata while expanding memory where // necessary. Start by computing the word size - // of returndata and allocated memory. + // of returndata and allocated memory. Round up + // to the nearest full word. let returnDataWords := div( - returndatasize(), + add(returndatasize(), AlmostOneWord), OneWord ) @@ -161,7 +180,7 @@ contract TokenTransferrer is TokenTransferrerErrors { } // Otherwise revert with a message about the token - // returning false. + // returning false or non-compliant return values. mstore( BadReturnValueFromERC20OnTransfer_error_sig_ptr, BadReturnValueFromERC20OnTransfer_error_signature @@ -188,14 +207,14 @@ contract TokenTransferrer is TokenTransferrerErrors { ) } - // Otherwise revert with error about token not having code: + // Otherwise, revert with error about token not having code: mstore(NoContract_error_sig_ptr, NoContract_error_signature) mstore(NoContract_error_token_ptr, token) revert(NoContract_error_sig_ptr, NoContract_error_length) } - // Otherwise the token just returned nothing but otherwise - // succeeded; no need to optimize for this as it's not + // Otherwise, the token just returned no data despite the call + // having succeeded; no need to optimize for this as it's not // technically ERC20 compliant. } @@ -210,7 +229,9 @@ contract TokenTransferrer is TokenTransferrerErrors { /** * @dev Internal function to transfer an ERC721 token from a given * originator to a given recipient. Sufficient approvals must be set on - * the contract performing the transfer. + * the contract performing the transfer. Note that this function does + * not check whether the receiver can accept the ERC721 token (i.e. it + * does not use `safeTransferFrom`). * * @param token The ERC721 token to transfer. * @param from The originator of the transfer. @@ -232,10 +253,11 @@ contract TokenTransferrer is TokenTransferrerErrors { revert(NoContract_error_sig_ptr, NoContract_error_length) } - // Write calldata to free memory pointer (restore it later). + // The free memory pointer memory slot will be used when populating + // call data for the transfer; read the value and restore it later. let memPointer := mload(FreeMemoryPointerSlot) - // Write calldata to memory starting with function selector. + // Write call data to memory starting with function selector. mstore(ERC721_transferFrom_sig_ptr, ERC721_transferFrom_signature) mstore(ERC721_transferFrom_from_ptr, from) mstore(ERC721_transferFrom_to_ptr, to) @@ -260,7 +282,11 @@ contract TokenTransferrer is TokenTransferrerErrors { // Ensure that sufficient gas is available to copy // returndata while expanding memory where necessary. Start // by computing word size of returndata & allocated memory. - let returnDataWords := div(returndatasize(), OneWord) + // Round up to the nearest full word. + let returnDataWords := div( + add(returndatasize(), AlmostOneWord), + OneWord + ) // Note: use the free memory pointer in place of msize() to // work around a Yul warning that prevents accessing msize @@ -330,8 +356,8 @@ contract TokenTransferrer is TokenTransferrerErrors { * @dev Internal function to transfer ERC1155 tokens from a given * originator to a given recipient. Sufficient approvals must be set on * the contract performing the transfer and contract recipients must - * implement onReceived to indicate that they are willing to accept the - * transfer. + * implement the ERC1155TokenReceiver interface to indicate that they + * are willing to accept the transfer. * * @param token The ERC1155 token to transfer. * @param from The originator of the transfer. @@ -355,13 +381,14 @@ contract TokenTransferrer is TokenTransferrerErrors { revert(NoContract_error_sig_ptr, NoContract_error_length) } - // Write calldata to these slots below, but restore them later. + // The following memory slots will be used when populating call data + // for the transfer; read the values and restore them later. let memPointer := mload(FreeMemoryPointerSlot) let slot0x80 := mload(Slot0x80) let slot0xA0 := mload(Slot0xA0) let slot0xC0 := mload(Slot0xC0) - // Write calldata into memory, beginning with function selector. + // Write call data into memory, beginning with function selector. mstore( ERC1155_safeTransferFrom_sig_ptr, ERC1155_safeTransferFrom_signature @@ -376,6 +403,7 @@ contract TokenTransferrer is TokenTransferrerErrors { ) mstore(ERC1155_safeTransferFrom_data_length_ptr, 0) + // Perform the call, ignoring return data. let success := call( gas(), token, @@ -394,7 +422,11 @@ contract TokenTransferrer is TokenTransferrerErrors { // Ensure that sufficient gas is available to copy // returndata while expanding memory where necessary. Start // by computing word size of returndata & allocated memory. - let returnDataWords := div(returndatasize(), OneWord) + // Round up to the nearest full word. + let returnDataWords := div( + add(returndatasize(), AlmostOneWord), + OneWord + ) // Note: use the free memory pointer in place of msize() to // work around a Yul warning that prevents accessing msize @@ -468,8 +500,14 @@ contract TokenTransferrer is TokenTransferrerErrors { * @dev Internal function to transfer ERC1155 tokens from a given * originator to a given recipient. Sufficient approvals must be set on * the contract performing the transfer and contract recipients must - * implement onReceived to indicate that they are willing to accept the - * transfer. + * implement the ERC1155TokenReceiver interface to indicate that they + * are willing to accept the transfer. NOTE: this function is not + * memory-safe; it will overwrite existing memory, restore the free + * memory pointer to the default value, and overwrite the zero slot. + * This function should only be called once memory is no longer + * required and when uninitialized arrays are not utilized, and memory + * should be considered fully corrupted (aside from the existence of a + * default-value free memory pointer) after calling this function. * * @param batchTransfers The group of 1155 batch transfers to perform. */ @@ -510,56 +548,21 @@ contract TokenTransferrer is TokenTransferrerErrors { calldataload(nextElementHeadPtr) ) - // Update the offset position for the next loop - nextElementHeadPtr := add(nextElementHeadPtr, OneWord) + // Retrieve the token from calldata. + let token := calldataload(elementPtr) - // Copy the first section of calldata (before dynamic values). - calldatacopy( - BatchTransfer1155Params_ptr, - add(elementPtr, ConduitBatch1155Transfer_from_offset), - ConduitBatch1155Transfer_usable_head_size - ) + // If the token has no code, revert. + if iszero(extcodesize(token)) { + mstore(NoContract_error_sig_ptr, NoContract_error_signature) + mstore(NoContract_error_token_ptr, token) + revert(NoContract_error_sig_ptr, NoContract_error_length) + } // Get the total number of supplied ids. let idsLength := calldataload( add(elementPtr, ConduitBatch1155Transfer_ids_length_offset) ) - // Determine size of calldata required for ids and amounts. Note - // that the size includes both lengths as well as the data. - let idsAndAmountsSize := add(TwoWords, mul(idsLength, TwoWords)) - - // Update the offset for the data array in memory. - mstore( - BatchTransfer1155Params_data_head_ptr, - add( - BatchTransfer1155Params_ids_length_offset, - idsAndAmountsSize - ) - ) - - // Set the length of the data array in memory to zero. - mstore( - add( - BatchTransfer1155Params_data_length_basePtr, - idsAndAmountsSize - ), - 0 - ) - - // Determine the total calldata size for the call to transfer. - let transferDataSize := add( - BatchTransfer1155Params_data_length_basePtr, - mul(idsLength, TwoWords) - ) - - // Copy second section of calldata (including dynamic values). - calldatacopy( - BatchTransfer1155Params_ids_length_ptr, - add(elementPtr, ConduitBatch1155Transfer_ids_length_offset), - idsAndAmountsSize - ) - // Determine the expected offset for the amounts array. let expectedAmountsOffset := add( ConduitBatch1155Transfer_amounts_length_baseOffset, @@ -590,7 +593,7 @@ contract TokenTransferrer is TokenTransferrerErrors { calldataload( add( elementPtr, - ConduitBatch1155Transfer_amounts_head_offset + ConduitBatchTransfer_amounts_head_offset ) ), expectedAmountsOffset @@ -611,15 +614,50 @@ contract TokenTransferrer is TokenTransferrerErrors { ) } - // Retrieve the token from calldata. - let token := calldataload(elementPtr) + // Update the offset position for the next loop + nextElementHeadPtr := add(nextElementHeadPtr, OneWord) - // If the token has no code, revert. - if iszero(extcodesize(token)) { - mstore(NoContract_error_sig_ptr, NoContract_error_signature) - mstore(NoContract_error_token_ptr, token) - revert(NoContract_error_sig_ptr, NoContract_error_length) - } + // Copy the first section of calldata (before dynamic values). + calldatacopy( + BatchTransfer1155Params_ptr, + add(elementPtr, ConduitBatch1155Transfer_from_offset), + ConduitBatch1155Transfer_usable_head_size + ) + + // Determine size of calldata required for ids and amounts. Note + // that the size includes both lengths as well as the data. + let idsAndAmountsSize := add(TwoWords, mul(idsLength, TwoWords)) + + // Update the offset for the data array in memory. + mstore( + BatchTransfer1155Params_data_head_ptr, + add( + BatchTransfer1155Params_ids_length_offset, + idsAndAmountsSize + ) + ) + + // Set the length of the data array in memory to zero. + mstore( + add( + BatchTransfer1155Params_data_length_basePtr, + idsAndAmountsSize + ), + 0 + ) + + // Determine the total calldata size for the call to transfer. + let transferDataSize := add( + BatchTransfer1155Params_calldata_baseSize, + idsAndAmountsSize + ) + + // Copy second section of calldata (including dynamic values). + calldatacopy( + BatchTransfer1155Params_ids_length_ptr, + add(elementPtr, ConduitBatch1155Transfer_ids_length_offset), + idsAndAmountsSize + ) // Perform the call to transfer 1155 tokens. let success := call( @@ -640,8 +678,11 @@ contract TokenTransferrer is TokenTransferrerErrors { // Ensure that sufficient gas is available to copy // returndata while expanding memory where necessary. // Start by computing word size of returndata and - // allocated memory. - let returnDataWords := div(returndatasize(), OneWord) + // allocated memory. Round up to the nearest full word. + let returnDataWords := div( + add(returndatasize(), AlmostOneWord), + OneWord + ) // Note: use transferDataSize in place of msize() to // work around a Yul warning that prevents accessing @@ -700,11 +741,13 @@ contract TokenTransferrer is TokenTransferrerErrors { // Write the token. mstore(ERC1155BatchTransferGenericFailure_token_ptr, token) - // Move the ids and amounts offsets forward a word. + // Increase the offset to ids by 32. mstore( BatchTransfer1155Params_ids_head_ptr, - ConduitBatch1155Transfer_amounts_head_offset + ERC1155BatchTransferGenericFailure_ids_offset ) + + // Increase the offset to amounts by 32. mstore( BatchTransfer1155Params_amounts_head_ptr, add( @@ -713,16 +756,16 @@ contract TokenTransferrer is TokenTransferrerErrors { ) ) - // Return modified region with one fewer word at the end. - revert( - 0, - add(transferDataSize, BatchTransfer1155Params_ptr) - ) + // Return modified region. The total size stays the same as + // `token` uses the same number of bytes as `data.length`. + revert(0, transferDataSize) } } // Reset the free memory pointer to the default value; memory must // be assumed to be dirtied and not reused from this point forward. + // Also note that the zero slot is not reset to zero, meaning empty + // arrays cannot be safely created or utilized until it is restored. mstore(FreeMemoryPointerSlot, DefaultFreeMemoryPointer) } } diff --git a/contracts/lib/TokenTransferrerConstants.sol b/contracts/lib/TokenTransferrerConstants.sol index 9663b7315..adb01c58d 100644 --- a/contracts/lib/TokenTransferrerConstants.sol +++ b/contracts/lib/TokenTransferrerConstants.sol @@ -8,7 +8,7 @@ pragma solidity >=0.8.7; * offset or pointer to the body of a dynamic type. In calldata, the head * is always an offset (relative to the parent object), while in memory, * the head is always the pointer to the body. More information found here: - * https://docs.soliditylang.org/en/v0.8.13/abi-spec.html#argument-encoding + * https://docs.soliditylang.org/en/v0.8.14/abi-spec.html#argument-encoding * - Note that the length of an array is separate from and precedes the * head of the array. * @@ -33,6 +33,7 @@ pragma solidity >=0.8.7; * codebase but have been left in for readability. */ +uint256 constant AlmostOneWord = 0x1f; uint256 constant OneWord = 0x20; uint256 constant TwoWords = 0x40; uint256 constant ThreeWords = 0x60; @@ -113,11 +114,6 @@ uint256 constant TokenTransferGenericFailure_error_amount_ptr = 0x84; // 4 + 32 * 5 == 164 uint256 constant TokenTransferGenericFailure_error_length = 0xa4; -uint256 constant ERC1155BatchTransferGenericFailure_error_signature = ( - 0xafc445e200000000000000000000000000000000000000000000000000000000 -); -uint256 constant ERC1155BatchTransferGenericFailure_token_ptr = 0x04; - // abi.encodeWithSignature( // "BadReturnValueFromERC20OnTransfer(address,address,address,uint256)" // ) @@ -140,10 +136,11 @@ uint256 constant MemoryExpansionCoefficient = 0x200; // Values are offset by 32 bytes in order to write the token to the beginning // in the event of a revert uint256 constant BatchTransfer1155Params_ptr = 0x24; -uint256 constant BatchTransfer1155Params_ids_head_ptr = 0x44; +uint256 constant BatchTransfer1155Params_ids_head_ptr = 0x64; uint256 constant BatchTransfer1155Params_amounts_head_ptr = 0x84; uint256 constant BatchTransfer1155Params_data_head_ptr = 0xa4; -uint256 constant BatchTransfer1155Params_data_length_basePtr = 0x104; +uint256 constant BatchTransfer1155Params_data_length_basePtr = 0xc4; +uint256 constant BatchTransfer1155Params_calldata_baseSize = 0xc4; uint256 constant BatchTransfer1155Params_ids_length_ptr = 0xc4; @@ -160,8 +157,17 @@ uint256 constant ConduitBatch1155Transfer_ids_length_offset = 0xa0; uint256 constant ConduitBatch1155Transfer_amounts_length_baseOffset = 0xc0; uint256 constant ConduitBatch1155Transfer_calldata_baseSize = 0xc0; +// Note: abbreviated version of above constant to adhere to line length limit. +uint256 constant ConduitBatchTransfer_amounts_head_offset = 0x80; + uint256 constant Invalid1155BatchTransferEncoding_ptr = 0x00; uint256 constant Invalid1155BatchTransferEncoding_length = 0x04; uint256 constant Invalid1155BatchTransferEncoding_selector = ( 0xeba2084c00000000000000000000000000000000000000000000000000000000 ); + +uint256 constant ERC1155BatchTransferGenericFailure_error_signature = ( + 0xafc445e200000000000000000000000000000000000000000000000000000000 +); +uint256 constant ERC1155BatchTransferGenericFailure_token_ptr = 0x04; +uint256 constant ERC1155BatchTransferGenericFailure_ids_offset = 0xc0; diff --git a/contracts/lib/Verifiers.sol b/contracts/lib/Verifiers.sol index b0cf46799..baba8da90 100644 --- a/contracts/lib/Verifiers.sol +++ b/contracts/lib/Verifiers.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { OrderStatus } from "./ConsiderationStructs.sol"; @@ -85,7 +85,7 @@ contract Verifiers is Assertions, SignatureVerification { } /** - * @dev Internal pure function to validate that a given order is fillable + * @dev Internal view function to validate that a given order is fillable * and not cancelled based on the order status. * * @param orderHash The order hash. @@ -101,10 +101,10 @@ contract Verifiers is Assertions, SignatureVerification { */ function _verifyOrderStatus( bytes32 orderHash, - OrderStatus memory orderStatus, + OrderStatus storage orderStatus, bool onlyAllowUnused, bool revertOnInvalid - ) internal pure returns (bool valid) { + ) internal view returns (bool valid) { // Ensure that the order has not been cancelled. if (orderStatus.isCancelled) { // Only revert if revertOnInvalid has been supplied as true. @@ -116,14 +116,18 @@ contract Verifiers is Assertions, SignatureVerification { return false; } + // Read order status numerator from storage and place on stack. + uint256 orderStatusNumerator = orderStatus.numerator; + // If the order is not entirely unused... - if (orderStatus.numerator != 0) { + if (orderStatusNumerator != 0) { // ensure the order has not been partially filled when not allowed. if (onlyAllowUnused) { // Always revert on partial fills when onlyAllowUnused is true. revert OrderPartiallyFilled(orderHash); - // Otherwise, ensure that order has not been entirely filled. - } else if (orderStatus.numerator >= orderStatus.denominator) { + } + // Otherwise, ensure that order has not been entirely filled. + else if (orderStatusNumerator >= orderStatus.denominator) { // Only revert if revertOnInvalid has been supplied as true. if (revertOnInvalid) { revert OrderAlreadyFilled(orderHash); diff --git a/contracts/lib/ZoneInteraction.sol b/contracts/lib/ZoneInteraction.sol index be200121f..fbe88a514 100644 --- a/contracts/lib/ZoneInteraction.sol +++ b/contracts/lib/ZoneInteraction.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { ZoneInterface } from "../interfaces/ZoneInterface.sol"; @@ -89,7 +89,7 @@ contract ZoneInteraction is ZoneInteractionErrors, LowLevelHelpers { * identifier, and a proof that the supplied token * identifier is contained in the order's merkle * root. Note that a criteria of zero indicates - * that any (transferrable) token identifier is + * that any (transferable) token identifier is * valid and that no proof needs to be supplied. * @param priorOrderHashes The order hashes of each order supplied prior to * the current order as part of a "match" variety diff --git a/contracts/test/EIP1271Wallet.sol b/contracts/test/EIP1271Wallet.sol index 062c3b7cc..b0e5b50a1 100644 --- a/contracts/test/EIP1271Wallet.sol +++ b/contracts/test/EIP1271Wallet.sol @@ -67,6 +67,12 @@ contract EIP1271Wallet { return _EIP_1271_MAGIC_VALUE; } + // NOTE: this is obviously not secure, do not use outside of testing. + if (signature.length == 64) { + // All signatures of length 64 are OK as long as valid is true + return isValid ? _EIP_1271_MAGIC_VALUE : bytes4(0xffffffff); + } + if (signature.length != 65) { revert(); } diff --git a/contracts/test/ERC1155BatchRecipient.sol b/contracts/test/ERC1155BatchRecipient.sol new file mode 100644 index 000000000..2704c169d --- /dev/null +++ b/contracts/test/ERC1155BatchRecipient.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.7; + +contract ERC1155BatchRecipient { + error UnexpectedBatchData(); + + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes memory data + ) external pure returns (bytes4) { + if (data.length != 0) { + revert UnexpectedBatchData(); + } + return ERC1155BatchRecipient.onERC1155BatchReceived.selector; + } +} diff --git a/contracts/test/ExcessReturnDataRecipient.sol b/contracts/test/ExcessReturnDataRecipient.sol index 83c2d48d7..7196920ab 100644 --- a/contracts/test/ExcessReturnDataRecipient.sol +++ b/contracts/test/ExcessReturnDataRecipient.sol @@ -2,12 +2,20 @@ pragma solidity >=0.8.7; contract ExcessReturnDataRecipient { - uint256 revertDataSize; + uint256 private revertDataSize; function setRevertDataSize(uint256 size) external { revertDataSize = size; } + // Code created with the help of Stack Exchange question + // https://ethereum.stackexchange.com/questions/8086 + // Question by Doug King: + // https://ethereum.stackexchange.com/users/2041/doug-king + // Answer by Tjaden Hess: + // https://ethereum.stackexchange.com/users/131/tjaden-hess + // Modified to use Yul instead of Solidity and added change of + // base to convert to natural logarithm function ln(uint256 x) internal pure returns (uint256 y) { assembly { let arg := x diff --git a/diagrams/README.md b/diagrams/README.md new file mode 100644 index 000000000..ade0fd6ba --- /dev/null +++ b/diagrams/README.md @@ -0,0 +1,9 @@ +# Diagrams + +## Seaport + +![Seaport diagram](./Seaport.drawio.svg) + +## Dev + +To modify diagrams, open in [diagrams.net](https://diagrams.net) (formerly draw.io) and save both file and svg. Export svg with settings `Zoom: 100%` and `Border Width: 5`. \ No newline at end of file diff --git a/diagrams/Seaport.drawio b/diagrams/Seaport.drawio new file mode 100644 index 000000000..43dfff4bd --- /dev/null +++ b/diagrams/Seaport.drawio @@ -0,0 +1 @@ +7R1Zd6O2+tfknPYhPiwG24+O48ykczOZJml7py/3KEaxmWJwAWeZX38lQBihj9XCSxy/JBYC5G/f9OlMnyxfP/lotbjxLOycaYr1eqZfnmmaqvR18oeOvMUjA9WMB+a+bSWTNgP39k/M7kxG17aFA25i6HlOaK/4wZnnungWcmPI970XftqT5/BvXaE5FgbuZ8gRR/+yrXARjw4NZTP+GdvzBXuzqiRXlohNTgaCBbK8l8yQPj3TJ77nhfF/y9cJdijwGFzi+64KrqYL87Eb1rnh/u+FdffFNe5X3teLyc2Pv3+Yv53r/fgxz8hZJ784WW34xkCALQKR5Kvnhwtv7rnImW5GL3xv7VqYvkcl3zZz/uN5q2TwBw7DtwS9aB16ZGgRLp3k6pPnhhPP8fzojfoo+pBxCwWL9Ln0yzcUhth3oxFNoaPxcukaC+GiptAmZIq9JQ79NzLFxw4K7Wf+PpTQyzydl976zbPJEzUloW3NTG55Y6hnpM2eEXhrf4aT27KYyT+pX/WkEPlzHApPIv9kftFmKMJ8EypQP6igLRWoAx53OpNcTalAHVU9SR4VzCfr8Tfny2/oevnp85ff/7r7cTE9Z0TXEulKO6QXIEpApnTM9ZmkTuCtEVqrA+6x76O3zLQVnRAUvydFI8PrcFRKB/n5bF0bbMcrqPm2UT1ylEVEoj65fXrC/nWIlwJ1BS/20kEuFllfUUYjRSmjjmfsh/i1lD4Yh+bwPEy+vmzUeTplwavyYpLiwNYURoYAIwqeh7cVPtNMh7z+4tEn/83pfw/eP9gdW5aPg0C8em0RuNhPNvZv/YlvE6FoI3HWPaHkcLwknBqKF6euxS7l0EPgG/K8ihx7TqXujLwVE1RdUCzYxFgaJxeWtmXFYgEH9k/0GD2K4jFhEvJc4+LMuKTPIpIgiIUCfXQQ+uSXMhJwvZgqbMfJDYkEUUqGtamkb/BMZwpEktJDlkiGXdEIUysHIoxjWVi2XmM3UtvIybe6SrKp1DaG+ff0G0jh/N2pE7AjKWwKEmbiuQHxpHwCe89tKI3p5+pKjjTuD1tK435XnDY4UGmcvySO3OGZvbLxOxPdZlOSMnKcKlIUKLpTT30birp9+G4ao9lgOrh2f3rT/2Ht8ct5v40h3UJaz9b+c+oXFfJmLGpKAD4SRTz8s3TZIn47KLfRkIcPZX1vUC5bdtae94kaKdMeErSE3s/pT6OmmtDMzgwy0WpPJTIHCvPfNQ3pXcxi6TemqJo//kLWNaHBCkOP/1F+jWV6MpvJ9MhbOqPmQvxsstZHdu0XRE2OX0tUQ52FGGq8APZXMwx4KZzJULWkd6SARo2plfcdWDQvq4B0gFb1zkh1KDItJStKWII1QoCPwrUPGDviSMT6sGUUWTUP9hK4RGwa+MKEGjrgopBTywAiFGqt7fALfhOv/U1xDo5+RsHitAnWrLTBh5DBNOiKYDUo/hyjbqHm0U+dE0DwJZSD0YroeEhUZcagR5Dh7LuOgTyEoBn9SFLAI55EDFGkQerX6IxCtEIKsRnSrhzvZbZAHPbtD4ym4e6clhIjXAPIoJKAUfOvf17c+++W9mJPfqrzlXOphOdmv4X5ztniLWx5Aj7/7b8J1qIv3+mXnq712cDla/by5Vv22zfi0ZNfT4kjGqToukJL26GzPmPnGVN6SS4ki1C1ErzydFGY82rvdWjSvYl6yRR9MOopg9Hmwz+w40wW+9V5ryU2XRrEvRSFuGMwmuRwpWrwXKmzhHyV8ZhP68iTtGLskMlTy36GlbEyS0GzcSIYlQt+xdXacW5X2K1WyMALJa3hG1ETNtr7Migo7jAhL3sWEm7fP0BaL+YItWvK3jXN7FSqNOBujrmhpBJkRcmIYYCxJUMVENVJyKxIzar6sI2a5QtLZOpMOAJ3JkTqytK4klVr00RVaqGl4W8tSyeV89Uk0wgmtlooZJjwtBaEt3U9EX61w4gKe0by7TsjM/L/hgLpF44A45sUZcSTr2K2od5t6fXA6IsQTCl99QfbzS+lx+rV6bnfX7f6ytCVHrNScyvpwGotkztgIrLYZt2dbQpm1rqyTeGUjwChrzHla8ov04fPQGR8ejfZBK654QH99cC4mv7sQzdnRBSXklX9gqkc2gcC1qHoYGdGi9rGaOmuEqbSfGC55s5d7lyOTBWKkeTJKzCCo4l8soMITqLT2f/fOf0O6/S2YZpUUQsytjowdzBkYph5MsmH9DomE1PfB5lszDiDM+IqLDj5ET2JpJL1SEBQ69JNxu0ktyLo6+krnhFk+QJJ8Mb+y8IO8f0KRWB58dGqpnBukOAY5s3OEWDdaB0pOhB7rUo+t2WU5iKvLWvV3sWxh70Ag1ykRhVs2AIhKTzJHOSf1K/nkTT1y8Ql99UcCXbgyKuik/In8XdpzWMgUC/npSCfyU8Njrk1ZmFBs4Es3AdYWEbVNgwe0UOJwPMGF0DE10pKMOIJUe7iPiRzRBgfs2eidhFQhUo9ZaSlQZG9Fwu4pfztxDIZipYJHADs78syATc8tsHbMWxvrI0y6RrUzFVhD/PV1ZL2ZZg6/J5CfVw+f2stCZLX6IO8JJOXVoBG2eRV9J5ujSqxXjOpY7umKvyJukEltpUEU4qwL/ezB4poSekGoFnVkdGRau1rAlC2zLRTQyUpXi9IbW/jk+40yFRORvXNqeqdM+YuPeC+6F1sg/MbFM4WHxjnMJ7f1rh3lBdvq2iD8j+J92OhEH9gncN6PucPiPcRgHUZey5hrENtZ9pjfYLcGf6Q7WWyHUI5WHzUHc6LCwvb4Pzanfl4iSm8uV0kJ494ocAHwjwU2O4O8+IWa4atFYcchmPHdvE5Wxrdt6b2+mfQFrVPOIy4Pt7Skyb6gxVywQdH6D4PYnzTB698DD74jIaW9LiQIqWr+LH8m1lYbsfvLtw6de0+ef4yv2VvB4v6ijYRTnJpdXyloXl+LmT83bJzjpv7IjeDuruzegxNdMySfXgPPnKDp843B+cb+qj6AJBwUNxfzae+5QGl2HPJlEnnG0tUVnRH/SZqz77yvWWDR9eeuulrUfsWvonFMRaL12TolBsabMTiqBcor4IcsM5q6jTRFOc2fk9936vK7RUVuBZZRVKEAAfFASQWoVSQZmpdAbLYk42IH9C+oJqttIjZhqqiewuVfYVWL9fvkbUzdnyMrLcrwi7bbFc5clBcu880zJDN6Z4qDBLNf7JguLGDwHbnt749t13kCF22gpOFDCMQ5DgP3qmTCUcXX73wBp8uLK7dYP30ZM9o47JpuCAu9Hq1cuwT1icRFJjz9Am72LdnV8h2MiVBJweTZD8qtTQCwjBTl9r41pXnl0d53ztYol9/HcQB71O2wSJAJETifBikia6NqeJ02eMCWVc+msXRzxOFQUIMN8H8z8gTPXE4XKDAnjFpgaKNuVN35lnEaj9Z0Hz17ld4RiN6VgSZYPxMzI0oitYMJO8pkNe4Zfh55T5JDUqzSen7B/8EMcNK+91FRWSxUDzQYN5ASFaPoDY4AxGYMiq7YVgW5yzb8HMijDbtVRoVKxw1XzXu56z3qqv8oW3nnbUq0wSEdNw1pVlhf/MGxaWNhytr+VkO80B2Gepi6m1sPVNT2NpFq+J+bhdSuiupqqROVYY9CcWzMEyK66tqV1xooJWR1NFiP9t7FeiwII58XS9pAMwDihYusestbRe+OH0NfXSJQpSRlkdWXVBTVOrNWzXkM+E9oFEnZIR01/xeF4vZoyw2i2/5xP6uYYXktgvuxSwRNhj2gVKqtF0GB97ObDxdbMguwS6J+6EwFBWdQJEkO2heo2jKH+46wBadkXpbwEEZHEHAAU+hJ8sFLeaue88FIqZWuPbdyAWmdRFRI5hbd1OiIognmqQIqZ18ClaZ3rgO6VzLm2WAvwOZZR2esyH2F2DHrhBD23PWdV2eQxA2eo9th023MyuiE6Tpg95O5Q3bvS5J3kQWGYckAqHbdXj7dIfcOcDJf7h+PMuKWtgXH6uzmcgl24pv4J5XZyngc2vdmMzl8hjxGUz5qYk8/uZ73tMpyKGUhbeQQ6J7qIHFfzLkENy4Q0DJgRxEU3qMz7H5ecDmsE1st0y+SxDO+RMT6jt5Mg6RhMGxWxev2r8bW5ZNJSNtIpycS5atfnmfLlt/2xYWak2HTcbZG3B7rhbC6z21sGCdKapbWEjv97sd3vaLOA5tGyweU1s1CPMwqA+rrZohSv7YrPaWj0TGi7pw373VRhqgLXfaWm2vLQjVZrzSCakbNYUca9Z4IEJuHx3xDko51cab9G6i2+FtL4f7sHbvZ/Vbw2aQbUZnQWTbvauDCpTv6FAgiYTUdSNFXRv0Rqay+fR5a1cZGj1VU0fsM+Rf0HGHdUPcohNpztTjOTjVqSrqDnUnDDSxqiSBF919P6a127ND3ilmsEA1s0aA2hIdgmhnW+4MubUlSVomgxRiCa6IG+uGt+54PvfxHGUrInMh0LHLR0zv8L9r28fWrZt5IpgMWtKUDLYy08AHpquBjkyP453Q2nMp33cbAjXqF8gURzxBAu5OJAANy1gT07gd7EFLBFMr6vabFbLQIQejzny64rRu84LTwqLRRzT7Zx6ptPPModC2a9Mq+tJHNpNLwKvpA85jJopij9rqFXzhBbI2pFRdjZqtapVdt3vMYEzkKgUllOz+AKwEwFYcm/0B2xa8z2owBPgWQBMEex34vicDonlPmcpKDrDAtrPwXJt+udtGC7rwshmAqlOrhxWuoffvLVxz9Cf5yI+9wbEV8byDfNVPx+ET9v58+CRpunmQp7rUbX4nQ7aBvcC1Nnm67Y++LEg+aHUydfvvIm4CNSogdGNBuq9MnAlsPYgKYy+JE/y8bTixhazaoonaTgONcJvINnVUErM92k5OCquhirY5KamaterWO+iHVf5lilnv/L6CA2Y3QTGlZxDtjd3M/R5ec9YoKb5vw6/0uL3q7OzeTr+BMT/co7nPH8bNrlRmZw/lFEdZsnk7gmIO24EQFFu32BL2gEWy0CF9/yJZh1pI7pxTj8CygaEnvXYJ9sOFbgUd+uGlPzTX56FnB5EnDhfE79oVF6MVu3TFYb23rwNWj1EXyS96LeCmnCM6qnnyqixmMkXdlS9vOWjPIs9n+y+2HQLWgLAt84Ahqhm5jZjKEOhAstMgImhfZXp0P+PkVLut6yxygBSgBsC2uM4idybkwNCg/gTQ6QUyaq/gcCxgYXFpzvbpy8oULE0ynid5xXE0A/lwNperSrQaJztlrUBoM7nzFbDzvPayguSQlfSkIa7DZEGS+VjL1CoYvVgqFXJ/9TkHO5ahYvYqE6zf1KuVi1FJrQK2kqu5M0pVBYItdFpnf9QVbEWxeu3iVzQLN+04j4EXtkJaowRVQSUGYGhAh7TJaHUGq0exNDmJ5Oywj8Z2zNFTtVH2w/OKNgJ4ZaT3BkBXDXMko0EXDGexgrZduVWiqRbIdbEzcbwAUpVJ0Vr++CFhgtBnp7JT6jEwdXvmjbmhkNTOKa3x/KsABq4GcbCMxpXgmsEYYkMLV62wcG3wKeL8TbvLs02POPKrbDZhiQmdkWs/KaYEqlyuA7reOQ0uEEitVr5HflXvqImxQ3dLNXhl0q/bOLNF8yDy1fcorjehGOKkL248C9MZ/wc= \ No newline at end of file diff --git a/diagrams/Seaport.drawio.svg b/diagrams/Seaport.drawio.svg new file mode 100644 index 000000000..f9c8a13e0 --- /dev/null +++ b/diagrams/Seaport.drawio.svg @@ -0,0 +1,4 @@ + + + +OfferItem
ItemType
TokenAddress
IdentifierOrCriteria
StartAmount
EndAmount
ItemType...
ConsiderationItem
ItemType
TokenAddress
IdentifierOrCriteria
StartAmount
EndAmount

Recipient
ItemType...
Order
Offer (array)
Consideration (array)
Offer (array)...
Offerer
Signature

OrderType
StartTime
EndTime
Counter
Salt

ConduitKey
Zone
ZoneHash
Offerer...

Seaport

Seaport
Flowchart
Flowchart
OrderType
FullOpen
PartialOpen
FullRestricted
PartialRestricted
FullOpen...
ItemType
Native (ETH)
ERC20
ERC721
ERC1155
Native (ETH)...
Executor
Executor
Verifiers
VerifyTime
VerifySignature
VerifyOrderStatus
VerifyTime...
SeaportInterface
FulfillOrder
FulfillOrder
MatchOrder
MatchOrder
ValidateOrder
ValidateOrder
CancelOrder
CancelOrder
IncrementCounter
IncrementCounter

GetOrderHash GetOrderStatus GetCounter
Information Name

GetOrderHash GetOrderStatus GetCounter...
ConduitTransfer
ItemType
Token
From
To
Identifier
Amount
ItemType...
ConsiderationErrors
OrderAlreadyFilled
InvalidTime
InvalidConduit
MissingOriginalConsiderationItems
InvalidCallToConduit
ConsiderationNotMet
InsufficientEtherSupplied
EtherTransferGenericFailure
PartialFillsNotEnabledForOrder
OrderIsCancelled
OrderPartiallyFilled
InvalidCanceller
BadFraction
InvalidMsgValue
InvalidBasicOrderParameterEncoding
NoSpecifiedOrdersAvailable
OrderAlreadyFilled...
ZoneInteractionErrors
InvalidRestrictedOrder
InvalidRestrictedOrder
AdvancedOrder

FulfillerConduitKey

Numerator
Denominator
ExtraData

FulfillerConduitKey...
TokenTransferrerErrors
InvalidERC721TransferAmount
MissingItemAmount
UnusedItemParameters
TokenTransferGenericFailure
ERC1155BatchTransferGenericFailure
BadReturnValueFromERC20OnTransfer
NoContract
InvalidERC721TransferAmount...
CriteriaResolutionErrors
OrderCriteriaResolverOutOfRange
UnresolvedOfferCriteria
UnresolvedConsiderationCriteria
OfferCriteriaResolverOutOfRange
ConsiderationCriteriaResolverOutOfRange
CriteriaNotEnabledForItem
InvalidProof
OrderCriteriaResolverOutOfRange...
BasicOrder

FulfillerConduitKey

AdditionalRecipients

FulfillerConduitKey...
OrderCombiner
OrderCombiner
OrderFulfiller
OrderFulfiller
FulfillmentApplicationErrors
MissingFulfillmentComponentOnAggregation
OfferAndConsiderationRequiredOnFulfillment
MismatchedFulfillmentOfferAndConsiderationComponents
InvalidFulfillmentComponentData
MissingFulfillmentComponentOnAggregation...
SignatureVerificationErrors
BadSignatureV
InvalidSigner
InvalidSignature
BadContractSignature
BadSignatureV...
OrderValidator
OrderValidator
AmountDeriver
AmountDeriver
TokenTransferrer
TokenTransferrer
Conduit
Conduit
Zone.isValidOrder
Zone.isValidOrder
FulfillmentApplier
FulfillmentApplier
CriteriaResolution
CriteriaResolution
ConsiderationEvents
OrderFulfilled
OrderCancelled
OrderValidated
CounterIncremented
OrderFulfilled...
AmountDerivationError
InexactFraction
InexactFraction
ConduitErrors
ChannelClosed
InvalidItemType
Invalid1155BatchTransferEncoding
ChannelClosed...
Restricted means zone
must give approval.
Restricted means zone...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/Code4rena-Guidelines.md b/docs/Code4rena-Guidelines.md new file mode 100644 index 000000000..9cea5eae5 --- /dev/null +++ b/docs/Code4rena-Guidelines.md @@ -0,0 +1,97 @@ +# Code4rena Guidelines + +## Overview: + +Seaport is a marketplace protocol for safely and efficiently buying and selling NFTs. Each listing contains an arbitrary number of items that the offerer is willing to give (the "offer") along with an arbitrary number of items that must be received along with their respective receivers (the "consideration"). + +## In Scope + +At a high level, the core invariants that we expect to be upheld are that: + +- Only items that are explicitly offered as part of a valid order may be transferred from an offerer’s account as long as they only set token approvals on either Seaport directly or on a conduit that only has Seaport set as a channel. +- No order fulfillment may spend more than the offer items that are explicitly set for the order in question. Note that not all offer items need to be spent. +- All consideration items (or fractions thereof in the case of orders that support partial fills) must be received in full by the named recipients in order for the corresponding offer items (or fractions thereof) to be spent. Note that additional consideration items may be added to any order on fulfillment as “tips”. Note also that when calling any fulfillment method other than `matchOrders` or `matchAdvancedOrders` that an implied “mirror” order is created for the fulfiller and so offer items on fulfilled orders should be treated as consideration items for the fulfiller (with the exception of `fulfillBasicOrder` where ERC721 ⇒ ERC20 and ERC1155 ⇒ ERC20 route types will use a portion of the offered item on the fulfilled order to pay out consideration items on that order). +- In all cases, assume that that all items contain standard ERC20/721/1155 behavior. This may include popular tokens or contracts (though reporting particular tokens that would violate these invariants would be categorized as a low-severity finding). + +## Out of scope + +There are a number of known limitations that are explicitly out of scope for the context of the competition: + +- As a malicious or vulnerable conduit owner may set a channel on a conduit that allows for approved tokens to be taken at will, we make the assumption in the context of this contest that only the Seaport contract will be added as a channel to any conduit. +- As all offer and consideration items are allocated against one another in memory, there are scenarios in which the actual received item amount will differ from the amount specified by the order — notably, this includes items with a fee-on-transfer mechanic. +- As all offer items are taken directly from the offerer and all consideration items are given directly to the named recipient, there are scenarios where those accounts can increase the gas cost of order fulfillment or block orders from being fulfilled outright depending on the item being transferred. If the item in question is Ether or a similar native token, a recipient can throw in the payable fallback or even spend excess gas from the submitter. Similar mechanics can be leveraged by both offerers and receives if the item in question is a token with a transfer hook (like ERC1155 and ERC777) or a non-standard token implementation. +- As fulfillments may be executed in whatever sequence the fulfiller specifies as long as the fulfillments are all executable, as restricted orders are validated via zones prior to execution, and as orders may be combined with other orders or have additional consideration items supplied, **any items with modifiable state are at risk of having that state modified during execution** if a payable Ether recipient or onReceived 1155 transfer hook is able to modify that state. By way of example, imagine an offerer offers WETH and requires some ERC721 item as consideration, where the ERC721 should have some additional property like not having been used to mint some other ERC721 item. Then, even if the offerer enforces that the ERC721 have that property via a restricted order that checks for the property, a malicious fulfiller could include a second order (or even just an additional consideration item) that uses the ERC721 item being sold to mint before it is transferred to the offerer. +- As all consideration items are supplied at the time of order creation, dynamic adjustment of recipients or amounts after creation (e.g. modifications to royalty payout info) is not supported. +- As all criteria-based items are tied to a particular token, there is no native way to construct orders where items specify cross-token criteria. Additionally, each potential identifier for a particular criteria-based item must have the same amount as any other identifier. +- As orders that contain items with ascending or descending amounts may not be filled as quickly as a fulfiller would like (e.g. transactions taking longer than expected to be included), there is a risk that fulfillment on those orders will supply a larger item amount, or receive back a smaller item amount, than they intended or expected. +- As all items on orders supporting partial fills must be "cleanly divisible" when performing a partial fill, orders with multiple items should to be constructed with care. A straightforward heuristic is to start with a "unit" bundle (e.g. 1 NFT item A, 3 NFT item B, and 5 NFT item C for 2 ETH) then applying a multiple to that unit bundle (e.g. 7 of those units results in a partial order for 7 NFT item A, 21 NFT item B, and 35 NFT item C for 14 ETH). +- As Ether cannot be "taken" from an account, any order that contains Ether or other native tokens as an offer item (including "implied" mirror orders) must be supplied by the caller executing the order(s) as msg.value. This also explains why there are no `ERC721_TO_ERC20` and `ERC1155_TO_ERC20` basic order route types, as Ether cannot be taken from the offerer in these cases. One important takeaway from this mechanic is that, technically, anyone can supply Ether on behalf of a given offerer (whereas the offerer themselves must supply all other items). It also means that all Ether must be supplied at the time the order or group of orders is originally called (and the amount available to spend by offer items cannot be increased by an external source during execution as is the case for token balances). +- As extensions to the consideration array on fulfillment (i.e. "tipping") can be arbitrarily set by the caller, fulfillments where all matched orders have already been signed for or validated can be frontrun on submission, with the frontrunner modifying any tips. Therefore, it is important that orders fulfilled in this manner either leverage "restricted" order types with a zone that enforces appropriate allocation of consideration extensions, or that each offer item is fully spent and each consideration item is appropriately declared on order creation. +- As orders that have been verified (via a call to `validate`) or partially filled will skip signature validation on subsequent fulfillments, orders that utilize EIP-1271 for verifying orders may end up in an inconsistent state where the original signature is no longer valid but the order is still fulfillable. In these cases, the offerer must explicitly cancel the previously verified order in question if they no longer wish for the order to be fulfillable. +- As orders filled by the "fulfill available" method will only be skipped if those orders have been cancelled, fully filled, or are inactive, fulfillments may still be attempted on unfulfillable orders (examples include revoked approvals or insufficient balances). This scenario (as well as issues with order formatting) will result in the full batch failing. +- As order parameters must be supplied upon cancellation, orders that were meant to remain private (e.g. were not published publicly) will be made visible upon cancellation. While these orders would not be *fulfillable* without a corresponding signature, cancellation of private orders without broadcasting intent currently requires the offerer (or the zone, if the order type is restricted and the zone supports it) to increment the counter. +- As order fulfillment attempts may become public before being included in a block, there is a risk of those orders being front-run. This risk is magnified in cases where offered items contain ascending amounts or consideration items contain descending amounts, as there is added incentive to leave the order unfulfilled until another interested fulfiller attempts to fulfill the order in question. +- As validated orders may still be unfulfillable due to invalid item amounts or other factors, callers should determine whether validated orders are fulfillable by simulating the fulfillment call prior to execution. Also note that anyone can validate a signed order, but only the offerer can validate an order without supplying a signature. +- As the offerer or the zone of a given order may cancel an order that differs from the intended order, callers should ensure that the intended order was cancelled by calling `getOrderStatus` and confirming that `isCancelled` returns `true`. +- As all derived amounts of partial fills and ascending/descending orders need to be derived without integer overflows, some categories of order may end up in a state where they can no longer be fulfilled due to reverting amount calculations. +- As many functions expect the default ABI encoding to be used, calling functions with non-standard encoding should not be expected to succeed. +- As ERC1271-compliant wallets implement their own signature verification, there is a risk that an improperly configured ERC1271 offerer could have funds stolen due to overly permissive signature verification. +- More generally, any finding reported in the Trail of Bits audit is additionally out of scope. + +## Tiers: + +**Low / Informational:** + +- Informational issues, like returning the wrong error type. +- Issues with the reference implementation where behavior does not map 1:1 with the optimized contracts (with the exception of revert reasons as some are not reproducible without optimizations) +- Gas optimizations. + +**Medium:** + +- Any behavior that is not in line with expected behavior on standard interaction with the protocol (bearing in mind all known limitations) — this would include a halt of functionality where orders that should succeed revert, or edge cases that do not result in widespread loss of funds but might lead to a small subset of funds being at risk. + +**High / Critical** + +- Any of the core invariants listed above being exploitable in a manner that places most or all user funds at risk. + +### **Areas of focus** + +While wardens should submit any bugs they identify for review, we particularly encourage review of code which has any of the following: + +- transfer of multiple assets +- arithmetic for order amounts +- aggregation of fulfillments + - FulfillmentApplier.sol +- transfer accumulation + - Executor.sol + - OrderCombiner.sol + - OrderFulfiller.sol + - BasicOrderFulfiller.sol +- low-level handling of nested dynamic types in calldata or loaded from calldata + - OrderFulfiller.sol + - BasicOrderFulfiller.sol + - FulfillmentApplier.sol + - GettersAndDerivers.sol + - CriteriaResolution.sol +- order matching / fulfillment validation + - OrderFulfiller.sol + - BasicOrderFulfiller.sol + - OrderCombiner.sol + - FulfillmentApplier.sol + - CriteriaResolution.sol + +## **Tests** + +A full suite of unit tests using Hardhat and Foundry have been provided in this repo, found in the `test` folder. + +## Information: + +[https://docs.opensea.io/v2.0/reference/seaport-overview](https://docs.opensea.io/v2.0/reference/seaport-overview) + +### Reference Implementation: + +The reference folder has its own implementation of Seaport which is designed to be readable and have feature parity with the Seaport.sol. We created the Reference implementation because a lot of Seaport is optimized by using assembly and interesting memory management techniques, that often make the code hard to read and understand. The Reference should be easy to read and work the same exact way, but it is NOT what is deployed. So if you find an issue with parity or a bug / vulnerability in the reference implementation, please report it but be advised that it will not classify as a medium or high-severity finding. + +## Test contracts + +Test contracts and non-solidity files are explicitly out of scope for the competition, though issues and PRs with any new tests you write as part of your investigation are greatly appreciated. \ No newline at end of file diff --git a/docs/SeaportDocumentation.md b/docs/SeaportDocumentation.md new file mode 100644 index 000000000..73c66a4de --- /dev/null +++ b/docs/SeaportDocumentation.md @@ -0,0 +1,168 @@ +# Seaport Documentation + +Documentation around creating orders, fulfillment, and interacting with Seaport. + +## Table of Contents + +- [Order](#order) +- [Order Fulfillment](#order-fulfillment) +- [Sequence of Events](#sequence-of-events) +- [Known Limitations And Workarounds](#known-limitations-and-workarounds) + +## Order + +Each order contains eleven key components: + +- The `offerer` of the order supplies all offered items and must either fulfill the order personally (i.e. `msg.sender == offerer`) or approve the order via signature (either standard 65-byte EDCSA, 64-byte EIP-2098, or an EIP-1271 `isValidSignature` check) or by listing the order on-chain (i.e. calling `validate`). +- The `zone` of the order is an optional secondary account attached to the order with two additional privileges: + - The zone may cancel orders where it is named as the zone by calling `cancel`. (Note that offerers can also cancel their own orders, either individually or for all orders signed with their current counter at once by calling `incrementCounter`). + - "Restricted" orders (as specified by the order type) must either be executed by the zone or the offerer, or must be approved as indicated by a call to an `isValidOrder` or `isValidOrderIncludingExtraData` view function on the zone. +- The `offer` contains an array of items that may be transferred from the offerer's account, where each item consists of the following components: + - The `itemType` designates the type of item, with valid types being Ether (or other native token for the given chain), ERC20, ERC721, ERC1155, ERC721 with "criteria" (explained below), and ERC1155 with criteria. + - The `token` designates the account of the item's token contract (with the null address used for Ether or other native tokens). + - The `identifierOrCriteria` represents either the ERC721 or ERC1155 token identifier or, in the case of a criteria-based item type, a merkle root composed of the valid set of token identifiers for the item. This value will be ignored for Ether and ERC20 item types, and can optionally be zero for criteria-based item types to allow for any identifier. + - The `startAmount` represents the amount of the item in question that will be required should the order be fulfilled at the moment the order becomes active. + - The `endAmount` represents the amount of the item in question that will be required should the order be fulfilled at the moment the order expires. If this value differs from the item's `startAmount`, the realized amount is calculated linearly based on the time elapsed since the order became active. +- The `consideration` contains an array of items that must be received in order to fulfill the order. It contains all of the same components as an offered item, and additionally includes a `recipient` that will receive each item. This array may be extended by the fulfiller on order fulfillment so as to support "tipping" (e.g. relayer or referral payments). +- The `orderType` designates one of four types for the order depending on two distinct preferences: + - `FULL` indicates that the order does not support partial fills, whereas `PARTIAL` enables filling some fraction of the order, with the important caveat that each item must be cleanly divisible by the supplied fraction (i.e. no remainder after division). + - `OPEN` indicates that the call to execute the order can be submitted by any account, whereas `RESTRICTED` requires that the order either be executed by the offerer or the zone of the order, or that a magic value indicating that the order is approved is returned upon calling an `isValidOrder` or `isValidOrderIncludingExtraData` view function on the zone. +- The `startTime` indicates the block timestamp at which the order becomes active. +- The `endTime` indicates the block timestamp at which the order expires. This value and the `startTime` are used in conjunction with the `startAmount` and `endAmount` of each item to derive their current amount. +- The `zoneHash` represents an arbitrary 32-byte value that will be supplied to the zone when fulfilling restricted orders that the zone can utilize when making a determination on whether to authorize the order. +- The `salt` represents an arbitrary source of entropy for the order. +- The `conduitKey` is a `bytes32` value that indicates what conduit, if any, should be utilized as a source for token approvals when performing transfers. By default (i.e. when `conduitKey` is set to the zero hash), the offerer will grant ERC20, ERC721, and ERC1155 token approvals to Seaport directly so that it can perform any transfers specified by the order during fulfillment. In contrast, an offerer that elects to utilize a conduit will grant token approvals to the conduit contract corresponding to the supplied conduit key, and Seaport will then instruct that conduit to transfer the respective tokens. +- The `counter` indicates a value that must match the current counter for the given offerer. + +## Order Fulfillment + +Orders are fulfilled via one of four methods: + +- Calling one of two "standard" functions, `fulfillOrder` and `fulfillAdvancedOrder`, where a second implied order will be constructed with the caller as the offerer, the consideration of the fulfilled order as the offer, and the offer of the fulfilled order as the consideration (with "advanced" orders containing the fraction that should be filled alongside a set of "criteria resolvers" that designate an identifier and a corresponding inclusion proof for each criteria-based item on the fulfilled order). All offer items will be transferred from the offerer of the order to the fulfiller, then all consideration items will be transferred from the fulfiller to the named recipient. +- Calling the "basic" function, `fulfillBasicOrder` with one of six basic route types supplied (`ETH_TO_ERC721`, `ETH_TO_ERC1155`, `ERC20_TO_ERC721`, `ERC20_TO_ERC1155`, `ERC721_TO_ERC20`, and `ERC1155_TO_ERC20`) will derive the order to fulfill from a subset of components, assuming the order in question adheres to the following: + - The order only contains a single offer item and contains at least one consideration item. + - The order contains exactly one ERC721 or ERC1155 item and that item is not criteria-based. + - The offerer of the order is the recipient of the first consideration item. + - All other items have the same Ether (or other native tokens) or ERC20 item type and token. + - The order does not offer an item with Ether (or other native tokens) as its item type. + - The `startAmount` on each item must match that item's `endAmount` (i.e. items cannot have an ascending/descending amount). + - All "ignored" item fields (i.e. `token` and `identifierOrCriteria` on native items and `identifierOrCriteria` on ERC20 items) are set to the null address or zero. + - If the order has an ERC721 item, that item has an amount of `1`. + - If the order has multiple consideration items and all consideration items other than the first consideration item have the same item type as the offered item, the offered item amount is not less than the sum of all consideration item amounts excluding the first consideration item amount. +- Calling one of two "fulfill available" functions, `fulfillAvailableOrders` and `fulfillAvailableAdvancedOrders`, where a group of orders are supplied alongside a group of fulfillments specifying which offer items can be aggregated into distinct transfers and which consideration items can be accordingly aggregated, and where any orders that have been cancelled, have an invalid time, or have already been fully filled will be skipped without causing the rest of the available orders to revert. Additionally, any remaining orders will be skipped once `maximumFulfilled` available orders have been located. Similar to the standard fulfillment method, all offer items will be transferred from the respective offerer to the fulfiller, then all consideration items will be transferred from the fulfiller to the named recipient. +- Calling one of two "match" functions, `matchOrders` and `matchAdvancedOrders`, where a group of explicit orders are supplied alongside a group of fulfillments specifying which offer items to apply to which consideration items (and with the "advanced" case operating in a similar fashion to the standard method, but supporting partial fills via supplied `numerator` and `denominator` fractional values as well as an optional `extraData` argument that will be supplied as part of a call to the `isValidOrderIncludingExtraData` view function on the zone when fulfilling restricted order types). Note that orders fulfilled in this manner do not have an explicit fulfiller; instead, Seaport will simply ensure coincidence of wants across each order. + +While the standard method can technically be used for fulfilling any order, it suffers from key efficiency limitations in certain scenarios: + +- It requires additional calldata compared to the basic method for simple "hot paths". +- It requires the fulfiller to approve each consideration item, even if the consideration item can be fulfilled using an offer item (as is commonly the case when fulfilling an order that offers ERC20 items for an ERC721 or ERC1155 item and also includes consideration items with the same ERC20 item type for paying fees). +- It can result in unnecessary transfers, whereas in the "match" case those transfers can be reduced to a more minimal set. + +### Balance and Approval Requirements + +When creating an offer, the following requirements should be checked to ensure that the order will be fulfillable: + +- The offerer should have sufficient balance of all offered items. +- If the order does not indicate to use a conduit, the offerer should have sufficient approvals set for the Seaport contract for all offered ERC20, ERC721, and ERC1155 items. +- If the order _does_ indicate to use a conduit, the offerer should have sufficient approvals set for the respective conduit contract for all offered ERC20, ERC721 and ERC1155 items. + +When fulfilling a _basic_ order, the following requirements need to be checked to ensure that the order will be fulfillable: + +- The above checks need to be performed to ensure that the offerer still has sufficient balance and approvals. +- The fulfiller should have sufficient balance of all consideration items _except for those with an item type that matches the order's offered item type_ — by way of example, if the fulfilled order offers an ERC20 item and requires an ERC721 item to the offerer and the same ERC20 item to another recipient, the fulfiller needs to own the ERC721 item but does not need to own the ERC20 item as it will be sourced from the offerer. +- If the fulfiller does not elect to utilize a conduit, they need to have sufficient approvals set for the Seaport contract for all ERC20, ERC721, and ERC1155 consideration items on the fulfilled order _except for ERC20 items with an item type that matches the order's offered item type_. +- If the fulfiller _does_ elect to utilize a conduit, they need to have sufficient approvals set for their respective conduit for all ERC20, ERC721, and ERC1155 consideration items on the fulfilled order _except for ERC20 items with an item type that matches the order's offered item type_. +- If the fulfilled order specifies Ether (or other native tokens) as consideration items, the fulfiller must be able to supply the sum total of those items as `msg.value`. + +When fulfilling a _standard_ order, the following requirements need to be checked to ensure that the order will be fulfillable: + +- The above checks need to be performed to ensure that the offerer still has sufficient balance and approvals. +- The fulfiller should have sufficient balance of all consideration items _after receiving all offered items_ — by way of example, if the fulfilled order offers an ERC20 item and requires an ERC721 item to the offerer and the same ERC20 item to another recipient with an amount less than or equal to the offered amount, the fulfiller does not need to own the ERC20 item as it will first be received from the offerer. +- If the fulfiller does not elect to utilize a conduit, they need to have sufficient approvals set for the Seaport contract for all ERC20, ERC721, and ERC1155 consideration items on the fulfilled order. +- If the fulfiller _does_ elect to utilize a conduit, they need to have sufficient approvals set for their respective conduit for all ERC20, ERC721, and ERC1155 consideration items on the fulfilled order. +- If the fulfilled order specifies Ether (or other native tokens) as consideration items, the fulfiller must be able to supply the sum total of those items as `msg.value`. + +When fulfilling a set of _match_ orders, the following requirements need to be checked to ensure that the order will be fulfillable: + +- Each account that sources the ERC20, ERC721, or ERC1155 item for an execution that will be performed as part of the fulfillment must have sufficient balance and approval on Seaport or the indicated conduit at the time the execution is triggered. Note that prior executions may supply the necessary balance for subsequent executions. +- The sum total of all executions involving Ether (or other native tokens) must be supplied as `msg.value`. Note that executions where the offerer and the recipient are the same account will be filtered out of the final execution set. + +### Partial Fills + +When constructing an order, the offerer may elect to enable partial fills by setting an appropriate order type. Then, orders that support partial fills can be fulfilled for some _fraction_ of the respective order, allowing subsequent fills to bypass signature verification. To summarize a few key points on partial fills: + +- When creating orders that support partial fills or determining a fraction to fill on those orders, all items (both offer and consideration) on the order must be cleanly divisible by the supplied fraction (i.e. no remainder after division). +- If the desired fraction to fill would result in more than the full order amount being filled, that fraction will be reduced to the amount remaining to fill. This applies to both partial fill attempts as well as full fill attempts. If this behavior is not desired (i.e. the fill should be "all or none"), the fulfiller can either use a "basic" order method if available (which requires that the full order amount be filled), or use the "match" order method and explicitly provide an order that requires the full desired amount be received back. + - By way of example: if one fulfiller tries to fill 1/2 of an order but another fulfiller first fills 3/4 of the order, the original fulfiller will end up filling 1/4 of the order. +- If any of the items on a partially fillable order specify a different "startAmount" and "endAmount (e.g. they are ascending-amount or descending-amount items), the fraction will be applied to _both_ amounts prior to determining the current price. This ensures that cleanly divisible amounts can be chosen when constructing the order without a dependency on the time when the order is ultimately fulfilled. +- Partial fills can be combined with criteria-based items to enable constructing orders that offer or receive multiple items that would otherwise not be partially fillable (e.g. ERC721 items). + + - By way of example: an offerer can create a partially fillable order to supply up to 10 ETH for up to 10 ERC721 items from a given collection; then, any fulfiller can fill a portion of that order until it has been fully filled (or cancelled). + +## Sequence of Events + +### Fulfill Order + +When fulfilling an order via `fulfillOrder` or `fulfillAdvancedOrder`: + +1. Hash order + - Derive hashes for offer items and consideration items + - Retrieve current counter for the offerer + - Derive hash for order +2. Perform initial validation + - Ensure current time is inside order range + - Ensure valid caller for the order type; if the order type is restricted and the caller is not the offerer or the zone, call the zone to determine whether the order is valid +3. Retrieve and update order status + - Ensure order is not cancelled + - Ensure order is not fully filled + - If the order is _partially_ filled, reduce the supplied fill amount if necessary so that the order is not overfilled + - Verify the order signature if not already validated + - Determine fraction to fill based on preference + available amount + - Update order status (validated + fill fraction) +4. Determine amount for each item + - Compare start amount and end amount + - if they are equal: apply fill fraction to either one, ensure it divides cleanly, and use that amount + - if not: apply fill fraction to both, ensuring they both divide cleanly, then find linear fit based on current time +5. Apply criteria resolvers + - Ensure each criteria resolver refers to a criteria-based order item + - Ensure the supplied identifier for each item is valid via inclusion proof if the item has a non-zero criteria root + - Update each item type and identifier + - Ensure all remaining items are non-criteria-based +6. Emit OrderFulfilled event + - Include updated items (i.e. after amount adjustment and criteria resolution) +7. Transfer offer items from offerer to caller + - Use either conduit or Seaport directly to source approvals, depending on order type +8. Transfer consideration items from caller to respective recipients + - Use either conduit or Seaport directly to source approvals, depending on the fulfiller's stated preference + +> Note: `fulfillBasicOrder` works in a similar fashion, with a few exceptions: it reconstructs the order from a subset of order elements, skips linear fit amount adjustment and criteria resolution, requires that the full order amount be fillable, and performs a more minimal set of transfers by default when the offer item shares the same type and token as additional consideration items. + +### Match Orders + +When matching a group of orders via `matchOrders` or `matchAdvancedOrders`, steps 1 through 6 are nearly identical but are performed for _each_ supplied order. From there, the implementation diverges from standard fulfillments: + +7. Apply fulfillments + - Ensure each fulfillment refers to one or more offer items and one or more consideration items, all with the same type and token, and with the same approval source for each offer item and the same recipient for each consideration item + - Reduce the amount on each offer item and each consideration item to zero and track total reduced amounts for each + - Compare total amounts for each and add back the remaining amount to the first item on the appropriate side of the order + - Return a single execution for each fulfillment +8. Scan each consideration item and ensure that none still have a nonzero amount remaining +9. Perform transfers as part of each execution + - Use either conduit or Seaport directly to source approvals, depending on the original order type + - Ignore each execution where `to == from` or `amount == 0` _(NOTE: the current implementation does not perform this last optimization)_ + +## Known Limitations and Workarounds + +- As all offer and consideration items are allocated against one another in memory, there are scenarios in which the actual received item amount will differ from the amount specified by the order — notably, this includes items with a fee-on-transfer mechanic. Orders that contain items of this nature (or, more broadly, items that have some post-fulfillment state that should be met) should leverage "restricted" order types and route the order fulfillment through a zone contract that performs the necessary checks after order fulfillment is completed. +- As all offer items are taken directly from the offerer and all consideration items are given directly to the named recipient, there are scenarios where those accounts can increase the gas cost of order fulfillment or block orders from being fulfilled outright depending on the item being transferred. If the item in question is Ether or a similar native token, a recipient can throw in the payable fallback or even spend excess gas from the submitter. Similar mechanics can be leveraged by both offerers and receives if the item in question is a token with a transfer hook (like ERC1155 and ERC777) or a non-standard token implementation. Potential remediations to this category of issue include wrapping Ether as WETH as a fallback if the initial transfer fails and allowing submitters to specify the amount of gas that should be allocated as part of a given fulfillment. Orders that support explicit fulfillments can also elect to leave problematic or unwanted offer items unspent as long as all consideration items are received in full. +- As fulfillments may be executed in whatever sequence the fulfiller specifies as long as the fulfillments are all executable, as restricted orders are validated via zones prior to execution, and as orders may be combined with other orders or have additional consideration items supplied, any items with modifiable state are at risk of having that state modified during execution if a payable Ether recipient or onReceived 1155 transfer hook is able to modify that state. By way of example, imagine an offerer offers WETH and requires some ERC721 item as consideration, where the ERC721 should have some additional property like not having been used to mint some other ERC721 item. Then, even if the offerer enforces that the ERC721 have that property via a restricted order that checks for the property, a malicious fulfiller could include a second order (or even just an additional consideration item) that uses the ERC721 item being sold to mint before it is transferred to the offerer. One category of remediation for this problem is to use restricted orders that do not implement `isValidOrder` and actually require that order fulfillment is routed through them so that they can perform post-fulfillment validation. Another interesting solution to this problem that retains order composability is to "fight fire with fire" and have the offerer include a "validator" ERC1155 consideration item on orders that require additional assurances; this would be a contract that contains the ERC1155 interface but is not actually an 1155 token, and instead leverages the `onReceived` hook as a means to validate that the expected invariants were upheld, reverting the "transfer" if the check fails (so in the case of the example above, this hook would ensure that the offerer was the owner of the ERC721 item in question and that it had not yet been used to mint the other ERC721). The key limitation to this mechanic is the amount of data that can be supplied in-band via this route; only three arguments ("from", "identifier", and "amount") are available to utilize. +- As all consideration items are supplied at the time of order creation, dynamic adjustment of recipients or amounts after creation (e.g. modifications to royalty payout info) is not supported. However, a zone can enforce that a given restricted order contains _new_ dynamically computed consideration items by deriving them and either supplying them manually or ensuring that they are present via `isValidZoneIncludingExtraData` since consideration items can be extended arbitrarily, with the important caveat that no more than the original offer item amounts can be spent. +- As all criteria-based items are tied to a particular token, there is no native way to construct orders where items specify cross-token criteria. Additionally, each potential identifier for a particular criteria-based item must have the same amount as any other identifier. +- As orders that contain items with ascending or descending amounts may not be filled as quickly as a fulfiller would like (e.g. transactions taking longer than expected to be included), there is a risk that fulfillment on those orders will supply a larger item amount, or receive back a smaller item amount, than they intended or expected. One way to prevent these outcomes is to utilize `matchOrders`, supplying a contrasting order for the fulfiller that explicitly specifies the maximum allowable offer items to be spent and consideration items to be received back. Special care should be taken when handling orders that contain both brief durations as well as items with ascending or descending amounts, as realized amounts may shift appreciably in a short window of time. +- As all items on orders supporting partial fills must be "cleanly divisible" when performing a partial fill, orders with multiple items should be constructed with care. A straightforward heuristic is to start with a "unit" bundle (e.g. 1 NFT item A, 3 NFT item B, and 5 NFT item C for 2 ETH) then applying a multiple to that unit bundle (e.g. 7 of those units results in a partial order for 7 NFT item A, 21 NFT item B, and 35 NFT item C for 14 ETH). +- As Ether cannot be "taken" from an account, any order that contains Ether or other native tokens as an offer item (including "implied" mirror orders) must be supplied by the caller executing the order(s) as msg.value. This also explains why there are no `ERC721_TO_ETH` and `ERC1155_TO_ETH` basic order route types, as Ether cannot be taken from the offerer in these cases. One important takeaway from this mechanic is that, technically, anyone can supply Ether on behalf of a given offerer (whereas the offerer themselves must supply all other items). It also means that all Ether must be supplied at the time the order or group of orders is originally called (and the amount available to spend by offer items cannot be increased by an external source during execution as is the case for token balances). +- As extensions to the consideration array on fulfillment (i.e. "tipping") can be arbitrarily set by the caller, fulfillments where all matched orders have already been signed for or validated can be frontrun on submission, with the frontrunner modifying any tips. Therefore, it is important that orders fulfilled in this manner either leverage "restricted" order types with a zone that enforces appropriate allocation of consideration extensions, or that each offer item is fully spent and each consideration item is appropriately declared on order creation. +- As orders that have been verified (via a call to `validate`) or partially filled will skip signature validation on subsequent fulfillments, orders that utilize EIP-1271 for verifying orders may end up in an inconsistent state where the original signature is no longer valid but the order is still fulfillable. In these cases, the offerer must explicitly cancel the previously verified order in question if they no longer wish for the order to be fulfillable. +- As orders filled by the "fulfill available" method will only be skipped if those orders have been cancelled, fully filled, or are inactive, fulfillments may still be attempted on unfulfillable orders (examples include revoked approvals or insufficient balances). This scenario (as well as issues with order formatting) will result in the full batch failing. One remediation to this failure condition is to perform additional checks from an executing zone or wrapper contract when constructing the call and filtering orders based on those checks. +- As order parameters must be supplied upon cancellation, orders that were meant to remain private (e.g. were not published publicly) will be made visible upon cancellation. While these orders would not be _fulfillable_ without a corresponding signature, cancellation of private orders without broadcasting intent currently requires the offerer (or the zone, if the order type is restricted and the zone supports it) to increment the counter. +- As order fulfillment attempts may become public before being included in a block, there is a risk of those orders being front-run. This risk is magnified in cases where offered items contain ascending amounts or consideration items contain descending amounts, as there is added incentive to leave the order unfulfilled until another interested fulfiller attempts to fulfill the order in question. Remediation efforts include utilization of a private mempool (e.g. flashbots) and/or restricted orders where the respective zone enforces a commit-reveal scheme. diff --git a/eip-712-types/order.js b/eip-712-types/order.js index 6e5500f44..f6af0235a 100644 --- a/eip-712-types/order.js +++ b/eip-712-types/order.js @@ -10,7 +10,7 @@ const orderType = { { name: "zoneHash", type: "bytes32" }, { name: "salt", type: "uint256" }, { name: "conduitKey", type: "bytes32" }, - { name: "nonce", type: "uint256" }, + { name: "counter", type: "uint256" }, ], OfferItem: [ { name: "itemType", type: "uint8" }, diff --git a/foundry.toml b/foundry.toml index 5441a30a8..30d596dca 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,32 +1,33 @@ [default] +solc = '0.8.14' via_ir = true src = 'contracts' out = 'out' -libs = ['node_modules'] +libs = ["node_modules", "lib"] test = 'test/foundry' remappings = [ 'ds-test=lib/ds-test/src/', 'forge-std=lib/forge-std/src/', + '@rari-capital/solmate/=lib/solmate/', + 'contracts/=contracts/', + 'murky/=lib/murky/src/', ] fuzz_runs = 5000 fuzz_max_global_rejects = 2_000_000 -fuzz_max_local_rejects = 10_000 -optimizer_runs = 15000 +optimizer_runs = 19_066 [reference] solc = '0.8.7' via_ir = false src = 'reference' out = 'reference-out' -# specify something so it doesn't try to compile the 0.8.13 files in test/foundry +# specify something so it doesn't try to compile the 0.8.14 files in test/foundry test = 'reference' [optimized] -solc = '0.8.13' out = 'optimized-out' [test] -solc = '0.8.13' via_ir = false src = 'test/foundry' @@ -35,11 +36,10 @@ out = 'optimized-out' via_ir = false fuzz_runs = 1000 -[local-ffi] +[local] via_ir = false -fuzz_runs = 5000 +fuzz_runs = 1000 src = 'reference' out = 'reference-out' -ffi = true # See more config options https://github.com/gakonst/foundry/tree/master/config diff --git a/hardhat-coverage.config.ts b/hardhat-coverage.config.ts index b7cfc49a6..bea7c4d82 100644 --- a/hardhat-coverage.config.ts +++ b/hardhat-coverage.config.ts @@ -15,7 +15,7 @@ const config: HardhatUserConfig = { solidity: { compilers: [ { - version: "0.8.13", + version: "0.8.14", settings: { viaIR: false, optimizer: { @@ -28,6 +28,7 @@ const config: HardhatUserConfig = { networks: { hardhat: { blockGasLimit: 30_000_000, + throwOnCallFailures: false, }, }, gasReporter: { diff --git a/hardhat-reference-coverage.config.ts b/hardhat-reference-coverage.config.ts index 053a8d2e2..1b4b935a9 100644 --- a/hardhat-reference-coverage.config.ts +++ b/hardhat-reference-coverage.config.ts @@ -29,6 +29,7 @@ const config: HardhatUserConfig = { hardhat: { blockGasLimit: 30_000_000, allowUnlimitedContractSize: true, + throwOnCallFailures: false, }, }, gasReporter: { diff --git a/hardhat-reference.config.ts b/hardhat-reference.config.ts index 525d1fb8a..5ae11e791 100644 --- a/hardhat-reference.config.ts +++ b/hardhat-reference.config.ts @@ -43,6 +43,7 @@ const config: HardhatUserConfig = { hardhat: { blockGasLimit: 30_000_000, allowUnlimitedContractSize: true, + throwOnCallFailures: false, }, }, gasReporter: { diff --git a/hardhat.config.ts b/hardhat.config.ts index a558e7b6b..2676323cf 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -6,10 +6,10 @@ import "@typechain/hardhat"; import "hardhat-gas-reporter"; import "solidity-coverage"; -dotenv.config(); - import { TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS } from "hardhat/builtin-tasks/task-names"; +dotenv.config(); + // Filter Reference Contracts subtask(TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS).setAction( async (_, __, runSuper) => { @@ -26,19 +26,19 @@ const config: HardhatUserConfig = { solidity: { compilers: [ { - version: "0.8.13", + version: "0.8.14", settings: { viaIR: true, optimizer: { enabled: true, - runs: 15000, + runs: 19066, }, }, }, ], overrides: { "contracts/conduit/Conduit.sol": { - version: "0.8.13", + version: "0.8.14", settings: { viaIR: true, optimizer: { @@ -48,7 +48,7 @@ const config: HardhatUserConfig = { }, }, "contracts/conduit/ConduitController.sol": { - version: "0.8.13", + version: "0.8.14", settings: { viaIR: true, optimizer: { @@ -62,6 +62,7 @@ const config: HardhatUserConfig = { networks: { hardhat: { blockGasLimit: 30_000_000, + throwOnCallFailures: false, }, }, gasReporter: { diff --git a/img/Seaport-banner.png b/img/Seaport-banner.png new file mode 100644 index 000000000..ab8fd3a40 Binary files /dev/null and b/img/Seaport-banner.png differ diff --git a/img/consideration-banner.png b/img/consideration-banner.png deleted file mode 100644 index 998045c81..000000000 Binary files a/img/consideration-banner.png and /dev/null differ diff --git a/lib/ds-test b/lib/ds-test index c7a36fb23..9310e879d 160000 --- a/lib/ds-test +++ b/lib/ds-test @@ -1 +1 @@ -Subproject commit c7a36fb236f298e04edf28e2fee385b80f53945f +Subproject commit 9310e879db8ba3ea6d5c6489a579118fd264a3f5 diff --git a/lib/forge-std b/lib/forge-std index 37a3fe48c..d72ef5823 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 37a3fe48c3a4d8239cda93445f0b5e76b1507436 +Subproject commit d72ef58231da19b23df3cc6fa91e623a0b728878 diff --git a/lib/murky b/lib/murky index b67a9828f..2caa3b01b 160000 --- a/lib/murky +++ b/lib/murky @@ -1 +1 @@ -Subproject commit b67a9828f7de767ed5f81b786f50e2f9ab735040 +Subproject commit 2caa3b01b888a03152cfebfec8acb24eb8036c16 diff --git a/lib/solmate b/lib/solmate index b6ae78e6f..eaaccf88a 160000 --- a/lib/solmate +++ b/lib/solmate @@ -1 +1 @@ -Subproject commit b6ae78e6ff490f8fec7695c7b65d06e5614f1b65 +Subproject commit eaaccf88ac5290299884437e1aee098a96583d54 diff --git a/package.json b/package.json index 4ce17ad08..c6b30f07a 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "name": "consideration", - "version": "0.0.1", - "description": "Consideration is a marketplace contract for safely and efficiently creating and fulfilling orders for ERC721 and ERC1155 items. Each order contains an arbitrary number of items that the offerer is willing to give (the \"offer\") along with an arbitrary number of items that must be received along with their respective receivers (the \"consideration\").", - "main": "contracts/Consideration.sol", + "name": "seaport", + "version": "1.1.0", + "description": "Seaport is a marketplace protocol for safely and efficiently buying and selling NFTs. Each listing contains an arbitrary number of items that the offerer is willing to give (the \"offer\") along with an arbitrary number of items that must be received along with their respective receivers (the \"consideration\").", + "main": "contracts/Seaport.sol", "author": "0age", "license": "MIT", - "private": true, + "private": false, "engines": { "node": ">=16.0.0" }, @@ -29,22 +29,23 @@ "dotenv": "^16.0.0", "eslint": "^8.6.0", "eslint-config-prettier": "^8.3.0", - "eslint-config-standard": "^16.0.3", + "eslint-config-standard": "^17.0.0", "eslint-plugin-import": "^2.25.4", - "eslint-plugin-node": "^11.1.0", + "eslint-plugin-n": "^15.2.0", "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-promise": "^6.0.0", "ethereum-waffle": "^3.4.0", "hardhat-gas-reporter": "^1.0.7", "husky": ">=6", + "lint-staged": ">=10", "prettier": "^2.5.1", "prettier-plugin-solidity": "^1.0.0-beta.19", + "scuffed-abi": "^1.0.4", "solhint": "^3.3.6", "solidity-coverage": "^0.7.0", "ts-node": "^10.4.0", "typechain": "^8.0.0", - "typescript": "^4.5.4", - "lint-staged": ">=10" + "typescript": "^4.5.4" }, "resolutions": { "async": ">=2.6.4", @@ -55,14 +56,14 @@ "yargs-parser": ">=5.0.1" }, "scripts": { - "build": "hardhat compile", - "build:ref": "hardhat compile --config hardhat-reference.config.ts", - "test": "hardhat test", - "test:ref": "REFERENCE=true hardhat test --config hardhat-reference.config.ts", - "profile": "REPORT_GAS=true hardhat test", - "coverage": "hardhat coverage --config hardhat-coverage.config.ts", - "coverage:ref": "REFERENCE=true hardhat coverage --config hardhat-reference-coverage.config.ts --solcoverjs ./.solcover-reference.js", - "lint:check": "prettier --check **.sol && prettier --check **.js && prettier --check **.ts && hardhat compile && npx solhint 'contracts/**/*.sol'", + "build": "hardhat compile --config ./hardhat.config.ts", + "build:ref": "hardhat compile --config ./hardhat-reference.config.ts", + "test": "hardhat test --config ./hardhat.config.ts", + "test:ref": "REFERENCE=true hardhat test --config ./hardhat-reference.config.ts", + "profile": "REPORT_GAS=true hardhat test --config ./hardhat.config.ts", + "coverage": "hardhat coverage --config ./hardhat-coverage.config.ts --solcoverjs ./config/.solcover.js", + "coverage:ref": "REFERENCE=true hardhat coverage --config ./hardhat-reference-coverage.config.ts --solcoverjs ./config/.solcover-reference.js", + "lint:check": "prettier --check **.sol && prettier --check **.js && prettier --check **.ts && hardhat compile --config ./hardhat.config.ts && npx solhint --config ./config/.solhint.json --ignore-path ./config/.solhintignore 'contracts/**/*.sol'", "lint:fix": "prettier --write **.sol && prettier --write **.js && prettier --write **.ts", "test:forge": "FOUNDRY_PROFILE=reference forge build; FOUNDRY_PROFILE=optimized forge build; FOUNDRY_PROFILE=test forge test -vvv", "test:lite": "FOUNDRY_PROFILE=reference forge build; FOUNDRY_PROFILE=lite forge test -vvv", @@ -72,5 +73,56 @@ "*.sol": "prettier --write", "*.js": "prettier --write", "*.ts": "prettier --write" - } + }, + "prettier": { + "overrides": [ + { + "files": "*.sol", + "options": { + "tabWidth": 4, + "printWidth": 80, + "bracketSpacing": true + } + } + ] + }, + "eslintConfig": { + "env": { + "browser": false, + "es2021": true, + "mocha": true, + "node": true + }, + "plugins": [ + "@typescript-eslint", + "import" + ], + "extends": [ + "standard", + "plugin:prettier/recommended", + "eslint:recommended", + "plugin:import/recommended", + "plugin:import/typescript" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 12 + }, + "rules": { + "node/no-unsupported-features/es-syntax": [ + "error", + { + "ignores": [ + "modules" + ] + } + ] + } + }, + "eslintIgnore": [ + "node_modules", + "artifacts", + "cache", + "coverage" + ] } diff --git a/reference/ReferenceConsideration.sol b/reference/ReferenceConsideration.sol index 20666f571..35ea3a61c 100644 --- a/reference/ReferenceConsideration.sol +++ b/reference/ReferenceConsideration.sol @@ -29,7 +29,7 @@ import { OrderToExecute, AccumulatorStruct } from "./lib/ReferenceConsiderationS * @author 0age * @custom:coauthor d1ll0n * @custom:coauthor transmissions11 - * @custom:version rc-1 + * @custom:version rc-1.1 * @notice Consideration is a generalized ETH/ERC20/ERC721/ERC1155 marketplace. * It minimizes external calls to the greatest extent possible and * provides lightweight methods for common routes as well as more @@ -127,7 +127,8 @@ contract ReferenceConsideration is fulfilled = _validateAndFulfillAdvancedOrder( _convertOrderToAdvanced(order), new CriteriaResolver[](0), // No criteria resolvers supplied. - fulfillerConduitKey + fulfillerConduitKey, + msg.sender ); } @@ -156,7 +157,7 @@ contract ReferenceConsideration is * contained in the merkle root held by the item * in question's criteria element. Note that an * empty criteria indicates that any - * (transferrable) token identifier on the token + * (transferable) token identifier on the token * in question is valid and that no associated * proof needs to be supplied. * @param fulfillerConduitKey A bytes32 value indicating what conduit, if @@ -164,6 +165,9 @@ contract ReferenceConsideration is * from. The zero hash signifies that no conduit * should be used (and direct approvals set on * Consideration). + * @param recipient The intended recipient for all received items, + * with `address(0)` indicating that the caller + * should receive the items. * * @return fulfilled A boolean indicating whether the order has been * fulfilled. @@ -171,7 +175,8 @@ contract ReferenceConsideration is function fulfillAdvancedOrder( AdvancedOrder calldata advancedOrder, CriteriaResolver[] calldata criteriaResolvers, - bytes32 fulfillerConduitKey + bytes32 fulfillerConduitKey, + address recipient ) external payable @@ -184,7 +189,8 @@ contract ReferenceConsideration is fulfilled = _validateAndFulfillAdvancedOrder( advancedOrder, criteriaResolvers, - fulfillerConduitKey + fulfillerConduitKey, + recipient == address(0) ? msg.sender : recipient ); } @@ -264,6 +270,7 @@ contract ReferenceConsideration is offerFulfillments, considerationFulfillments, fulfillerConduitKey, + msg.sender, maximumFulfilled ); } @@ -304,7 +311,7 @@ contract ReferenceConsideration is * is contained in the merkle root held by * the item in question's criteria element. * Note that an empty criteria indicates - * that any (transferrable) token + * that any (transferable) token * identifier on the token in question is * valid and that no associated proof needs * to be supplied. @@ -335,6 +342,7 @@ contract ReferenceConsideration is FulfillmentComponent[][] calldata offerFulfillments, FulfillmentComponent[][] calldata considerationFulfillments, bytes32 fulfillerConduitKey, + address recipient, uint256 maximumFulfilled ) external @@ -359,6 +367,7 @@ contract ReferenceConsideration is offerFulfillments, considerationFulfillments, fulfillerConduitKey, + recipient == address(0) ? msg.sender : recipient, maximumFulfilled ); } @@ -431,7 +440,7 @@ contract ReferenceConsideration is * offer or consideration, a token identifier, and * a proof that the supplied token identifier is * contained in the order's merkle root. Note that - * an empty root indicates that any (transferrable) + * an empty root indicates that any (transferable) * token identifier is valid and that no associated * proof needs to be supplied. * @param fulfillments An array of elements allocating offer components @@ -506,19 +515,19 @@ contract ReferenceConsideration is /** * @notice Cancel all orders from a given offerer with a given zone in bulk - * by incrementing a nonce. Note that only the offerer may increment - * the nonce. + * by incrementing a counter. Note that only the offerer may + * increment the counter. * - * @return newNonce The new nonce. + * @return newCounter The new counter. */ - function incrementNonce() + function incrementCounter() external override notEntered - returns (uint256 newNonce) + returns (uint256 newCounter) { - // Increment current nonce for the supplied offerer. - newNonce = _incrementNonce(); + // Increment current counter for the supplied offerer. + newCounter = _incrementCounter(); } /** @@ -534,7 +543,8 @@ contract ReferenceConsideration is override returns (bytes32 orderHash) { - // Derive order hash by supplying order parameters along with the nonce. + // Derive order hash by supplying order parameters along with the + // counter. // prettier-ignore orderHash = _deriveOrderHash( OrderParameters( @@ -550,7 +560,7 @@ contract ReferenceConsideration is order.conduitKey, order.consideration.length ), - order.nonce + order.counter ); } @@ -587,20 +597,20 @@ contract ReferenceConsideration is } /** - * @notice Retrieve the current nonce for a given offerer. + * @notice Retrieve the current counter for a given offerer. * * @param offerer The offerer in question. * - * @return nonce The current nonce. + * @return counter The current counter. */ - function getNonce(address offerer) + function getCounter(address offerer) external view override - returns (uint256 nonce) + returns (uint256 counter) { - // Return the nonce for the supplied offerer. - nonce = _getNonce(offerer); + // Return the counter for the supplied offerer. + counter = _getCounter(offerer); } /** diff --git a/reference/conduit/ReferenceConduit.sol b/reference/conduit/ReferenceConduit.sol index a6dbd2c8e..118d26c1f 100644 --- a/reference/conduit/ReferenceConduit.sol +++ b/reference/conduit/ReferenceConduit.sol @@ -33,40 +33,60 @@ contract ReferenceConduit is ConduitInterface, ReferenceTokenTransferrer { _controller = msg.sender; } + /** + * @notice Execute a sequence of ERC20/721/1155 transfers. Only a caller + * with an open channel can call this function. Note that channels + * are expected to implement reentrancy protection if desired, and + * that cross-channel reentrancy may be possible if the conduit has + * multiple open channels at once. Also note that channels are + * expected to implement checks against transferring any zero-amount + * items if that constraint is desired. + * + * @param transfers The ERC20/721/1155 transfers to perform. + * + * @return magicValue A magic value indicating that the transfers were + * performed successfully. + */ function execute(ConduitTransfer[] calldata transfers) external override returns (bytes4 magicValue) { if (!_channels[msg.sender]) { - revert ChannelClosed(); + revert ChannelClosed(msg.sender); } - uint256 totalStandardTransfers = transfers.length; - - // Iterate over each standard execution. - for (uint256 i = 0; i < totalStandardTransfers; i++) { - // Retrieve the transfer in question. - ConduitTransfer calldata standardTransfer = transfers[i]; - - // Perform the transfer. - _transfer(standardTransfer); - } + // Perform standard transfers + _performTransfers(transfers); return this.execute.selector; } + /** + * @notice Execute a sequence of batch 1155 transfers. Only a caller with an + * open channel can call this function. Note that channels are + * expected to implement reentrancy protection if desired, and that + * cross-channel reentrancy may be possible if the conduit has + * multiple open channels at once. Also note that channels are + * expected to implement checks against transferring any zero-amount + * items if that constraint is desired. + * + * @param batchTransfers The 1155 batch transfers to perform. + * + * @return magicValue A magic value indicating that the transfers were + * performed successfully. + */ function executeBatch1155( ConduitBatch1155Transfer[] calldata batchTransfers ) external override returns (bytes4 magicValue) { if (!_channels[msg.sender]) { - revert ChannelClosed(); + revert ChannelClosed(msg.sender); } uint256 totalBatchTransfers = batchTransfers.length; // Iterate over each batch transfer. - for (uint256 i = 0; i < totalBatchTransfers; i++) { + for (uint256 i = 0; i < totalBatchTransfers; ++i) { // Retrieve the batch transfer in question. ConduitBatch1155Transfer calldata batchTransfer = batchTransfers[i]; @@ -77,29 +97,36 @@ contract ReferenceConduit is ConduitInterface, ReferenceTokenTransferrer { return this.executeBatch1155.selector; } + /** + * @notice Execute a sequence of transfers, both single and batch 1155. Only + * a caller with an open channel can call this function. Note that + * channels are expected to implement reentrancy protection if + * desired, and that cross-channel reentrancy may be possible if the + * conduit has multiple open channels at once. Also note that + * channels are expected to implement checks against transferring + * any zero-amount items if that constraint is desired. + * + * @param standardTransfers The ERC20/721/1155 transfers to perform. + * @param batchTransfers The 1155 batch transfers to perform. + * + * @return magicValue A magic value indicating that the transfers were + * performed successfully. + */ function executeWithBatch1155( ConduitTransfer[] calldata standardTransfers, ConduitBatch1155Transfer[] calldata batchTransfers ) external override returns (bytes4 magicValue) { if (!_channels[msg.sender]) { - revert ChannelClosed(); + revert ChannelClosed(msg.sender); } - uint256 totalStandardTransfers = standardTransfers.length; - - // Iterate over each standard transfer. - for (uint256 i = 0; i < totalStandardTransfers; i++) { - // Retrieve the transfer in question. - ConduitTransfer calldata standardTransfer = standardTransfers[i]; - - // Perform the transfer. - _transfer(standardTransfer); - } + // Perform standard transfers + _performTransfers(standardTransfers); uint256 totalBatchTransfers = batchTransfers.length; // Iterate over each batch transfer. - for (uint256 i = 0; i < totalBatchTransfers; i++) { + for (uint256 i = 0; i < totalBatchTransfers; ++i) { // Retrieve the batch transfer in question. ConduitBatch1155Transfer calldata batchTransfer = batchTransfers[i]; @@ -110,25 +137,62 @@ contract ReferenceConduit is ConduitInterface, ReferenceTokenTransferrer { return this.executeWithBatch1155.selector; } + /** + * @notice Open or close a given channel. Only callable by the controller. + * + * @param channel The channel to open or close. + * @param isOpen The status of the channel (either open or closed). + */ function updateChannel(address channel, bool isOpen) external override { if (msg.sender != _controller) { revert InvalidController(); } + // Ensure that the channel does not already have the indicated status. + if (_channels[channel] == isOpen) { + revert ChannelStatusAlreadySet(channel, isOpen); + } + _channels[channel] = isOpen; emit ChannelUpdated(channel, isOpen); } /** - * @dev Internal function to transfer a given item. + * @dev Internal function to transfer a list of given ERC20/721/1155 items. + * Sufficient approvals must be set, either on the respective proxy or + * on this contract itself. + * + * @param transfers The tokens to be transferred. + */ + function _performTransfers(ConduitTransfer[] calldata transfers) internal { + uint256 totalStandardTransfers = transfers.length; + + // Iterate over each standard execution. + for (uint256 i = 0; i < totalStandardTransfers; ++i) { + // Retrieve the transfer in question. + ConduitTransfer calldata standardTransfer = transfers[i]; + + // Perform the transfer. + _transfer(standardTransfer); + } + } + + /** + * @dev Internal function to transfer a given ERC20/721/1155 item. Note that + * channels are expected to implement checks against transferring any + * zero-amount items if that constraint is desired. * - * @param item The item to transfer, including an amount and recipient. + * @param item The ERC20/721/1155 item to transfer, including an amount and + * recipient. */ function _transfer(ConduitTransfer calldata item) internal { - // If the item type indicates Ether or a native token... + // Perform the transfer based on the item's type. if (item.itemType == ConduitItemType.ERC20) { - // Transfer ERC20 token. + // Transfer ERC20 token. Note that item.identifier is ignored and + // therefore ERC20 transfer items are potentially malleable — this + // check should be performed by the calling channel if a constraint + // on item malleability is desired. _performERC20Transfer(item.token, item.from, item.to, item.amount); } else if (item.itemType == ConduitItemType.ERC721) { // Ensure that exactly one 721 item is being transferred. diff --git a/reference/conduit/ReferenceConduitController.sol b/reference/conduit/ReferenceConduitController.sol index e7af8c04e..5dadf32a0 100644 --- a/reference/conduit/ReferenceConduitController.sol +++ b/reference/conduit/ReferenceConduitController.sol @@ -61,6 +61,11 @@ contract ReferenceConduitController is ConduitControllerInterface { override returns (address conduit) { + // Ensure that an initial owner has been supplied. + if (initialOwner == address(0)) { + revert InvalidInitialOwner(); + } + // If the first 20 bytes of the conduit key do not match the caller... if (address(uint160(bytes20(conduitKey))) != msg.sender) { // Revert with an error indicating that the creator is invalid. @@ -92,11 +97,14 @@ contract ReferenceConduitController is ConduitControllerInterface { // Deploy the conduit via CREATE2 using the conduit key as the salt. new ReferenceConduit{ salt: conduitKey }(); + // Initialize storage variable referencing conduit properties. + ConduitProperties storage conduitProperties = _conduits[conduit]; + // Set the supplied initial owner as the owner of the conduit. - _conduits[conduit].owner = initialOwner; + conduitProperties.owner = initialOwner; // Set conduit key used to deploy the conduit to enable reverse lookup. - _conduits[conduit].key = conduitKey; + conduitProperties.key = conduitKey; // Emit an event indicating that the conduit has been deployed. emit NewConduit(conduit, conduitKey); @@ -189,6 +197,7 @@ contract ReferenceConduitController is ConduitControllerInterface { * function. * * @param conduit The conduit for which to initiate ownership transfer. + * @param newPotentialOwner The new potential owner of the conduit. */ function transferOwnership(address conduit, address newPotentialOwner) external @@ -202,8 +211,13 @@ contract ReferenceConduitController is ConduitControllerInterface { revert NewPotentialOwnerIsZeroAddress(conduit); } + // Ensure the new potential owner is not already set. + if (newPotentialOwner == _conduits[conduit].potentialOwner) { + revert NewPotentialOwnerAlreadySet(conduit, newPotentialOwner); + } + // Emit an event indicating that the potential owner has been updated. - emit PotentialOwnerUpdated(conduit, newPotentialOwner); + emit PotentialOwnerUpdated(newPotentialOwner); // Set the new potential owner as the potential owner of the conduit. _conduits[conduit].potentialOwner = newPotentialOwner; @@ -220,8 +234,13 @@ contract ReferenceConduitController is ConduitControllerInterface { // Ensure the caller is the current owner of the conduit in question. _assertCallerIsConduitOwner(conduit); + // Ensure that ownership transfer is currently possible. + if (_conduits[conduit].potentialOwner == address(0)) { + revert NoPotentialOwnerCurrentlySet(conduit); + } + // Emit an event indicating that the potential owner has been cleared. - emit PotentialOwnerUpdated(conduit, address(0)); + emit PotentialOwnerUpdated(address(0)); // Clear the current new potential owner from the conduit. delete _conduits[conduit].potentialOwner; @@ -245,7 +264,7 @@ contract ReferenceConduitController is ConduitControllerInterface { } // Emit an event indicating that the potential owner has been cleared. - emit PotentialOwnerUpdated(conduit, address(0)); + emit PotentialOwnerUpdated(address(0)); // Clear the current new potential owner from the conduit. delete _conduits[conduit].potentialOwner; diff --git a/reference/lib/ReferenceAmountDeriver.sol b/reference/lib/ReferenceAmountDeriver.sol index da2c6c3cf..e0f6be5f9 100644 --- a/reference/lib/ReferenceAmountDeriver.sol +++ b/reference/lib/ReferenceAmountDeriver.sol @@ -9,24 +9,24 @@ import { import { FractionData } from "./ReferenceConsiderationStructs.sol"; /** - * @title AmountDeriver + * @title ReferenceAmountDeriver * @author 0age - * @notice AmountDeriver contains pure functions related to deriving item - * amounts based on partial fill quantity and on linear extrapolation - * based on current time when the start amount and end amount differ. + * @notice ReferenceAmountDeriver contains view and pure functions related to + * deriving item amounts based on partial fill quantity and on linear + * interpolation based on current time when the start amount and end + * amount differ. */ contract ReferenceAmountDeriver is AmountDerivationErrors { /** - * @dev Internal pure function to derive the current amount of a given item + * @dev Internal view function to derive the current amount of a given item * based on the current price, the starting price, and the ending * price. If the start and end prices differ, the current price will be - * extrapolated on a linear basis. + * interpolated on a linear basis. * * @param startAmount The starting amount of the item. * @param endAmount The ending amount of the item. - * @param elapsed The time elapsed since the order's start time. - * @param remaining The time left until the order's end time. - * @param duration The total duration of the order. + * @param startTime The starting time of the order. + * @param endTime The end time of the order. * @param roundUp A boolean indicating whether the resultant amount * should be rounded up or down. * @@ -35,34 +35,42 @@ contract ReferenceAmountDeriver is AmountDerivationErrors { function _locateCurrentAmount( uint256 startAmount, uint256 endAmount, - uint256 elapsed, - uint256 remaining, - uint256 duration, + uint256 startTime, + uint256 endTime, bool roundUp - ) internal pure returns (uint256) { + ) internal view returns (uint256) { // Only modify end amount if it doesn't already equal start amount. if (startAmount != endAmount) { // Leave extra amount to add for rounding at zero (i.e. round down). uint256 extraCeiling = 0; + // Derive the duration for the order and place it on the stack. + uint256 duration = endTime - startTime; + + // Derive time elapsed since the order started & place on stack. + uint256 elapsed = block.timestamp - startTime; + + // Derive time remaining until order expires and place on stack. + uint256 remaining = duration - elapsed; + // If rounding up, set rounding factor to one less than denominator. if (roundUp) { extraCeiling = duration - 1; } - // Aggregate new amounts weighted by time with rounding factor + // Aggregate new amounts weighted by time with rounding factor. uint256 totalBeforeDivision = ((startAmount * remaining) + (endAmount * elapsed) + extraCeiling); - // Division is performed without zero check as it cannot be zero. + // Divide totalBeforeDivision by duration to get the new amount. uint256 newAmount = totalBeforeDivision / duration; - // Return the current amount (expressed as endAmount internally). + // Return the current amount. return newAmount; } - // Return the original amount (now expressed as endAmount internally). + // Return the original amount. return endAmount; } @@ -101,13 +109,16 @@ contract ReferenceAmountDeriver is AmountDerivationErrors { } /** - * @dev Internal pure function to apply a fraction to a consideration + * @dev Internal view function to apply a fraction to a consideration * or offer item. * * @param startAmount The starting amount of the item. * @param endAmount The ending amount of the item. * @param fractionData A struct containing the data used to apply a * fraction to an order. + * @param roundUp A boolean indicating whether the resultant + * amount should be rounded up or down. + * * @return amount The received item to transfer with the final amount. */ function _applyFraction( @@ -115,7 +126,7 @@ contract ReferenceAmountDeriver is AmountDerivationErrors { uint256 endAmount, FractionData memory fractionData, bool roundUp - ) internal pure returns (uint256 amount) { + ) internal view returns (uint256 amount) { // If start amount equals end amount, apply fraction to end amount. if (startAmount == endAmount) { amount = _getFraction( @@ -124,7 +135,7 @@ contract ReferenceAmountDeriver is AmountDerivationErrors { endAmount ); } else { - // Otherwise, apply fraction to both to extrapolate final amount. + // Otherwise, apply fraction to both to interpolated final amount. amount = _locateCurrentAmount( _getFraction( fractionData.numerator, @@ -136,9 +147,8 @@ contract ReferenceAmountDeriver is AmountDerivationErrors { fractionData.denominator, endAmount ), - fractionData.elapsed, - fractionData.remaining, - fractionData.duration, + fractionData.startTime, + fractionData.endTime, roundUp ); } diff --git a/reference/lib/ReferenceAssertions.sol b/reference/lib/ReferenceAssertions.sol index 2772d454a..bc0188425 100644 --- a/reference/lib/ReferenceAssertions.sol +++ b/reference/lib/ReferenceAssertions.sol @@ -7,7 +7,7 @@ import { ReferenceGettersAndDerivers } from "./ReferenceGettersAndDerivers.sol"; import { TokenTransferrerErrors } from "contracts/interfaces/TokenTransferrerErrors.sol"; -import { ReferenceNonceManager } from "./ReferenceNonceManager.sol"; +import { ReferenceCounterManager } from "./ReferenceCounterManager.sol"; import "contracts/lib/ConsiderationConstants.sol"; @@ -19,7 +19,7 @@ import "contracts/lib/ConsiderationConstants.sol"; */ contract ReferenceAssertions is ReferenceGettersAndDerivers, - ReferenceNonceManager, + ReferenceCounterManager, TokenTransferrerErrors { /** @@ -38,28 +38,27 @@ contract ReferenceAssertions is * @dev Internal view function to to ensure that the supplied consideration * array length on a given set of order parameters is not less than the * original consideration array length for that order and to retrieve - * the current nonce for a given order's offerer and zone and use it to - * derive the order hash. + * the current counter for a given order's offerer and zone and use it + * to derive the order hash. * * @param orderParameters The parameters of the order to hash. * - * @return The hash. + * @return orderHash The order hash. */ - function _assertConsiderationLengthAndGetNoncedOrderHash( + function _assertConsiderationLengthAndGetOrderHash( OrderParameters memory orderParameters - ) internal view returns (bytes32) { + ) internal view returns (bytes32 orderHash) { // Ensure supplied consideration array length is not less than original. _assertConsiderationLengthIsNotLessThanOriginalConsiderationLength( orderParameters.consideration.length, orderParameters.totalOriginalConsiderationItems ); - // Derive and return order hash using current nonce for the offerer. - return - _deriveOrderHash( - orderParameters, - _getNonce(orderParameters.offerer) - ); + // Derive and return order hash using current counter for the offerer. + orderHash = _deriveOrderHash( + orderParameters, + _getCounter(orderParameters.offerer) + ); } /** diff --git a/reference/lib/ReferenceBasicOrderFulfiller.sol b/reference/lib/ReferenceBasicOrderFulfiller.sol index c77be7d06..995530ea0 100644 --- a/reference/lib/ReferenceBasicOrderFulfiller.sol +++ b/reference/lib/ReferenceBasicOrderFulfiller.sol @@ -57,8 +57,10 @@ contract ReferenceBasicOrderFulfiller is ReferenceOrderValidator { } /** - * @dev Creates a mapping of BasicOrderType Enums to BasicOrderRouteType Enums - * and BasicOrderType Enums to OrderType Enums + * @dev Creates a mapping of BasicOrderType Enums to BasicOrderRouteType + * Enums and BasicOrderType Enums to OrderType Enums. Note that this + * is wildly inefficient, but makes the logic easier to follow when + * performing the fulfillment. */ function createMappings() internal { // BasicOrderType to BasicOrderRouteType @@ -362,6 +364,23 @@ contract ReferenceBasicOrderFulfiller is ReferenceOrderValidator { conduitKey = parameters.offererConduitKey; } + // Check for dirtied unused parameters. + if ( + ((route == BasicOrderRouteType.ETH_TO_ERC721 || + route == BasicOrderRouteType.ETH_TO_ERC1155) && + (uint160(parameters.considerationToken) | + parameters.considerationIdentifier) != + 0) || + ((route == BasicOrderRouteType.ERC20_TO_ERC721 || + route == BasicOrderRouteType.ERC20_TO_ERC1155) && + parameters.considerationIdentifier != 0) || + ((route == BasicOrderRouteType.ERC721_TO_ERC20 || + route == BasicOrderRouteType.ERC1155_TO_ERC20) && + parameters.offerIdentifier != 0) + ) { + revert UnusedItemParameters(); + } + // Declare transfer accumulator that will collect transfers that can be // bundled into a single call to their associated conduit. AccumulatorStruct memory accumulatorStruct; @@ -500,14 +519,16 @@ contract ReferenceBasicOrderFulfiller is ReferenceOrderValidator { * hashes. * @param parameters The parameters of the basic order. * @param fulfillmentItemTypes The fulfillment's item type. + * + * @return orderHash The order hash. */ function _hashOrder( BasicFulfillmentHashes memory hashes, BasicOrderParameters calldata parameters, FulfillmentItemTypes memory fulfillmentItemTypes ) internal view returns (bytes32 orderHash) { - // Read offerer's current nonce from storage and place on the stack. - uint256 nonce = _getNonce(parameters.offerer); + // Read offerer's current counter from storage and place on the stack. + uint256 counter = _getCounter(parameters.offerer); // Hash the contents to get the orderHash orderHash = keccak256( @@ -523,7 +544,7 @@ contract ReferenceBasicOrderFulfiller is ReferenceOrderValidator { parameters.zoneHash, parameters.salt, parameters.offererConduitKey, - nonce + counter ) ); } @@ -563,14 +584,15 @@ contract ReferenceBasicOrderFulfiller is ReferenceOrderValidator { // Ensure supplied consideration array length is not less than original. _assertConsiderationLengthIsNotLessThanOriginalConsiderationLength( - parameters.additionalRecipients.length + 1, + parameters.additionalRecipients.length, parameters.totalOriginalAdditionalRecipients ); // Memory to store hashes. BasicFulfillmentHashes memory hashes; - // Store ItemType/Token parameters in a struct in memory to avoid stack issues. + // Store ItemType/Token parameters in a struct in memory to avoid stack + // issues. FulfillmentItemTypes memory fulfillmentItemTypes = FulfillmentItemTypes( orderType, receivedItemType, @@ -618,7 +640,8 @@ contract ReferenceBasicOrderFulfiller is ReferenceOrderValidator { ) ); - // Declare memory for additionalReceivedItem, additionalRecipientItem. + // Declare memory for additionalReceivedItem and + // additionalRecipientItem. ReceivedItem memory additionalReceivedItem; ConsiderationItem memory additionalRecipientItem; @@ -641,7 +664,7 @@ contract ReferenceBasicOrderFulfiller is ReferenceOrderValidator { for ( uint256 recipientCount = 0; recipientCount < parameters.additionalRecipients.length; - recipientCount++ + ++recipientCount ) { // Get the next additionalRecipient. AdditionalRecipient memory additionalRecipient = ( @@ -669,7 +692,8 @@ contract ReferenceBasicOrderFulfiller is ReferenceOrderValidator { continue; } - // Create a new consideration item for each additional recipient. + // Create a new consideration item for each additional + // recipient. additionalRecipientItem = ConsiderationItem( fulfillmentItemTypes.additionalRecipientsItemType, fulfillmentItemTypes.additionalRecipientsToken, @@ -712,7 +736,7 @@ contract ReferenceBasicOrderFulfiller is ReferenceOrderValidator { uint256 additionalTips = parameters .totalOriginalAdditionalRecipients; additionalTips < parameters.additionalRecipients.length; - additionalTips++ + ++additionalTips ) { // Get the next additionalRecipient. AdditionalRecipient memory additionalRecipient = ( @@ -760,7 +784,8 @@ contract ReferenceBasicOrderFulfiller is ReferenceOrderValidator { offerItem.token, offerItem.identifier, offerItem.amount, - offerItem.amount //Assembly uses OfferItem instead of SpentItem. + // Assembly uses OfferItem instead of SpentItem. + offerItem.amount ) ) ]; @@ -878,9 +903,10 @@ contract ReferenceBasicOrderFulfiller is ReferenceOrderValidator { * @param erc20Token The ERC20 token to transfer. * @param amount The amount of ERC20 tokens to transfer. * @param parameters The parameters of the order. - * @param fromOfferer Whether to decrement amount from the offered amount. - * @param accumulatorStruct A struct containing conduit transfer data and its - * corresponding conduitKey. + * @param fromOfferer Whether to decrement amount from the + * offered amount. + * @param accumulatorStruct A struct containing conduit transfer data + * and its corresponding conduitKey. */ function _transferERC20AndFinalize( address from, diff --git a/reference/lib/ReferenceConsiderationBase.sol b/reference/lib/ReferenceConsiderationBase.sol index 7bef56167..aeef28021 100644 --- a/reference/lib/ReferenceConsiderationBase.sol +++ b/reference/lib/ReferenceConsiderationBase.sol @@ -27,7 +27,7 @@ contract ReferenceConsiderationBase is { // Declare constants for name, version, and reentrancy sentinel values. string internal constant _NAME = "Consideration"; - string internal constant _VERSION = "rc.1"; + string internal constant _VERSION = "rc.1.1"; uint256 internal constant _NOT_ENTERED = 1; uint256 internal constant _ENTERED = 2; @@ -88,13 +88,18 @@ contract ReferenceConsiderationBase is * @dev Internal view function to derive the initial EIP-712 domain * separator. * - * @return The derived domain separator. + * @param _eip712DomainTypeHash The primary EIP-712 domain typehash. + * @param _nameHash The hash of the name of the contract. + * @param _versionHash The hash of the version string of the + * contract. + * + * @return domainSeparator The derived domain separator. */ function _deriveInitialDomainSeparator( bytes32 _eip712DomainTypeHash, bytes32 _nameHash, bytes32 _versionHash - ) internal view virtual returns (bytes32) { + ) internal view virtual returns (bytes32 domainSeparator) { return _deriveDomainSeparator( _eip712DomainTypeHash, @@ -198,7 +203,7 @@ contract ReferenceConsiderationBase is "bytes32 zoneHash,", "uint256 salt,", "bytes32 conduitKey,", - "uint256 nonce", + "uint256 counter", ")" ); diff --git a/reference/lib/ReferenceConsiderationStructs.sol b/reference/lib/ReferenceConsiderationStructs.sol index b9bc07ba3..c2ea41296 100644 --- a/reference/lib/ReferenceConsiderationStructs.sol +++ b/reference/lib/ReferenceConsiderationStructs.sol @@ -68,11 +68,9 @@ struct OrderToExecute { struct FractionData { uint256 numerator; // The portion of the order that should be filled. uint256 denominator; // The total size of the order - bytes32 offererConduitKey; // The offerer's conduit key. bytes32 fulfillerConduitKey; // The fulfiller's conduit key. - uint256 duration; // The total duration of the order. - uint256 elapsed; // The time elapsed since the order's start time. - uint256 remaining; // The time left until the order's end time. + uint256 startTime; // The start time of the order. + uint256 endTime; // The end time of the order. } /** diff --git a/reference/lib/ReferenceCounterManager.sol b/reference/lib/ReferenceCounterManager.sol new file mode 100644 index 000000000..500124f7e --- /dev/null +++ b/reference/lib/ReferenceCounterManager.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +// prettier-ignore +import { + ConsiderationEventsAndErrors +} from "contracts/interfaces/ConsiderationEventsAndErrors.sol"; + +import { ReferenceReentrancyGuard } from "./ReferenceReentrancyGuard.sol"; + +/** + * @title CounterManager + * @author 0age + * @notice CounterManager contains a storage mapping and related functionality + * for retrieving and incrementing a per-offerer counter. + */ +contract ReferenceCounterManager is + ConsiderationEventsAndErrors, + ReferenceReentrancyGuard +{ + // Only orders signed using an offerer's current counter are fulfillable. + mapping(address => uint256) private _counters; + + /** + * @dev Internal function to cancel all orders from a given offerer with a + * given zone in bulk by incrementing a counter. Note that only the + * offerer may increment the counter. + * + * @return newCounter The new counter. + */ + function _incrementCounter() + internal + notEntered + returns (uint256 newCounter) + { + // Increment current counter for the supplied offerer. + newCounter = ++_counters[msg.sender]; + + // Emit an event containing the new counter. + emit CounterIncremented(newCounter, msg.sender); + } + + /** + * @dev Internal view function to retrieve the current counter for a given + * offerer. + * + * @param offerer The offerer in question. + * + * @return currentCounter The current counter. + */ + function _getCounter(address offerer) + internal + view + returns (uint256 currentCounter) + { + // Return the counter for the supplied offerer. + currentCounter = _counters[offerer]; + } +} diff --git a/reference/lib/ReferenceCriteriaResolution.sol b/reference/lib/ReferenceCriteriaResolution.sol index 7ca6b4584..15c6e0d5b 100644 --- a/reference/lib/ReferenceCriteriaResolution.sol +++ b/reference/lib/ReferenceCriteriaResolution.sol @@ -41,7 +41,7 @@ contract ReferenceCriteriaResolution is CriteriaResolutionErrors { * identifier, and a proof that the supplied token * identifier is contained in the order's merkle * root. Note that a root of zero indicates that - * any transferrable token identifier is valid and + * any transferable token identifier is valid and * that no proof needs to be supplied. */ function _applyCriteriaResolvers( @@ -204,7 +204,7 @@ contract ReferenceCriteriaResolution is CriteriaResolutionErrors { * identifier, and a proof that the supplied token * identifier is contained in the order's merkle * root. Note that a root of zero indicates that - * any transferrable token identifier is valid and + * any transferable token identifier is valid and * that no proof needs to be supplied. */ function _applyCriteriaResolversAdvanced( @@ -371,14 +371,15 @@ contract ReferenceCriteriaResolution is CriteriaResolutionErrors { uint256 root, bytes32[] memory proof ) internal pure { - // Convert the supplied leaf element from uint256 to bytes32. - bytes32 computedHash = bytes32(leaf); + // Hash the supplied leaf to use as the initial proof element. + bytes32 computedHash = keccak256(abi.encodePacked(leaf)); // Iterate over each proof element. for (uint256 i = 0; i < proof.length; ++i) { // Retrieve the proof element. bytes32 proofElement = proof[i]; + // Sort and hash proof elements and update the computed hash. if (computedHash <= proofElement) { // Hash(current computed hash + current element of proof) computedHash = keccak256( diff --git a/reference/lib/ReferenceExecutor.sol b/reference/lib/ReferenceExecutor.sol index bad16e46a..41680aa07 100644 --- a/reference/lib/ReferenceExecutor.sol +++ b/reference/lib/ReferenceExecutor.sol @@ -48,14 +48,17 @@ contract ReferenceExecutor is ReferenceVerifiers, ReferenceTokenTransferrer { /** * @dev Internal function to transfer a given item. * - * @param item The item to transfer including an amount and recipient. - * @param offerer The account offering the item, i.e. the from address. - * @param conduitKey A bytes32 value indicating what corresponding conduit, - * if any, to source token approvals from. The zero hash - * signifies that no conduit should be used (and direct - * approvals set on Consideration) - * @param accumulatorStruct A struct containing conduit transfer data and its - * corresponding conduitKey. + * @param item The item to transfer including an amount + * and recipient. + * @param offerer The account offering the item, i.e. the + * from address. + * @param conduitKey A bytes32 value indicating what + * corresponding conduit, if any, to source + * token approvals from. The zero hash + * signifies that no conduit should be used + * (and direct approvals set on Consideration) + * @param accumulatorStruct A struct containing conduit transfer data + * and its corresponding conduitKey. */ function _transfer( ReceivedItem memory item, @@ -65,9 +68,19 @@ contract ReferenceExecutor is ReferenceVerifiers, ReferenceTokenTransferrer { ) internal { // If the item type indicates Ether or a native token... if (item.itemType == ItemType.NATIVE) { - // transfer the native tokens to the recipient. + // Ensure neither the token nor the identifier parameters are set. + if ((uint160(item.token) | item.identifier) != 0) { + revert UnusedItemParameters(); + } + + // Transfer the native tokens to the recipient. _transferEth(item.recipient, item.amount); } else if (item.itemType == ItemType.ERC20) { + // Ensure that no identifier is supplied. + if (item.identifier != 0) { + revert UnusedItemParameters(); + } + // Transfer ERC20 tokens from the offerer to the recipient. _transferERC20( item.token, @@ -132,12 +145,13 @@ contract ReferenceExecutor is ReferenceVerifiers, ReferenceTokenTransferrer { * @param from The originator of the transfer. * @param to The recipient of the transfer. * @param amount The amount to transfer. - * @param conduitKey A bytes32 value indicating what corresponding conduit, - * if any, to source token approvals from. The zero hash - * signifies that no conduit should be used (and direct - * approvals set on Consideration). - * @param accumulatorStruct A struct containing conduit transfer data and its - * corresponding conduitKey. + * @param conduitKey A bytes32 value indicating what + * corresponding conduit, if any, to source + * token approvals from. The zero hash + * signifies that no conduit should be used + * (and direct approvals set on Consideration) + * @param accumulatorStruct A struct containing conduit transfer data + * and its corresponding conduitKey. */ function _transferERC20( address token, @@ -162,7 +176,7 @@ contract ReferenceExecutor is ReferenceVerifiers, ReferenceTokenTransferrer { _insert( conduitKey, accumulatorStruct, - uint256(1), + ConduitItemType.ERC20, token, from, to, @@ -181,13 +195,15 @@ contract ReferenceExecutor is ReferenceVerifiers, ReferenceTokenTransferrer { * @param from The originator of the transfer. * @param to The recipient of the transfer. * @param identifier The tokenId to transfer. - * @param amount The "amount" (this value must be equal to one). - * @param conduitKey A bytes32 value indicating what corresponding conduit, - * if any, to source token approvals from. The zero hash - * signifies that no conduit should be used (and direct - * approvals set on Consideration). - * @param accumulatorStruct A struct containing conduit transfer data and its - * corresponding conduitKey. + * @param amount The "amount" (this value must be equal + * to one). + * @param conduitKey A bytes32 value indicating what + * corresponding conduit, if any, to source + * token approvals from. The zero hash + * signifies that no conduit should be used + * (and direct approvals set on Consideration) + * @param accumulatorStruct A struct containing conduit transfer data + * and its corresponding conduitKey. */ function _transferERC721( address token, @@ -215,7 +231,7 @@ contract ReferenceExecutor is ReferenceVerifiers, ReferenceTokenTransferrer { _insert( conduitKey, accumulatorStruct, - uint256(2), + ConduitItemType.ERC721, token, from, to, @@ -235,12 +251,13 @@ contract ReferenceExecutor is ReferenceVerifiers, ReferenceTokenTransferrer { * @param to The recipient of the transfer. * @param identifier The tokenId to transfer. * @param amount The amount to transfer. - * @param conduitKey A bytes32 value indicating what corresponding conduit, - * if any, to source token approvals from. The zero hash - * signifies that no conduit should be used (and direct - * approvals set on Consideration). - * @param accumulatorStruct A struct containing conduit transfer data and its - * corresponding conduitKey. + * @param conduitKey A bytes32 value indicating what + * corresponding conduit, if any, to source + * token approvals from. The zero hash + * signifies that no conduit should be used + * (and direct approvals set on Consideration) + * @param accumulatorStruct A struct containing conduit transfer data + * and its corresponding conduitKey. */ function _transferERC1155( address token, @@ -266,7 +283,7 @@ contract ReferenceExecutor is ReferenceVerifiers, ReferenceTokenTransferrer { _insert( conduitKey, accumulatorStruct, - uint256(3), + ConduitItemType.ERC1155, token, from, to, @@ -282,12 +299,13 @@ contract ReferenceExecutor is ReferenceVerifiers, ReferenceTokenTransferrer { * is "armed") and the supplied conduit key does not match the key held * by the accumulator. * - * @param accumulatorStruct A struct containing conduit transfer data and its - * corresponding conduitKey. - * @param conduitKey A bytes32 value indicating what corresponding conduit, - * if any, to source token approvals from. The zero hash - * signifies that no conduit should be used, with direct - * approvals set on this contract. + * @param accumulatorStruct A struct containing conduit transfer data + * and its corresponding conduitKey. + * @param conduitKey A bytes32 value indicating what corresponding + * conduit, if any, to source token approvals + * from. The zero hash signifies that no conduit + * should be used (and direct approvals set on + * Consideration) */ function _triggerIfArmedAndNotAccumulatable( AccumulatorStruct memory accumulatorStruct, @@ -304,8 +322,8 @@ contract ReferenceExecutor is ReferenceVerifiers, ReferenceTokenTransferrer { * the accumulator if the accumulator contains item transfers (i.e. it * is "armed"). * - * @param accumulatorStruct A struct containing conduit transfer data and its - * corresponding conduitKey. + * @param accumulatorStruct A struct containing conduit transfer data + * and its corresponding conduitKey. */ function _triggerIfArmed(AccumulatorStruct memory accumulatorStruct) internal @@ -324,8 +342,8 @@ contract ReferenceExecutor is ReferenceVerifiers, ReferenceTokenTransferrer { * a given conduit key, supplying all accumulated item transfers. The * accumulator will be "disarmed" and reset in the process. * - * @param accumulatorStruct A struct containing conduit transfer data and its - * corresponding conduitKey. + * @param accumulatorStruct A struct containing conduit transfer data + * and its corresponding conduitKey. */ function _trigger(AccumulatorStruct memory accumulatorStruct) internal { // Call the conduit with all the accumulated transfers. @@ -342,12 +360,13 @@ contract ReferenceExecutor is ReferenceVerifiers, ReferenceTokenTransferrer { * that collects a series of transfers to execute against a given * conduit in a single call. * - * @param conduitKey A bytes32 value indicating what corresponding conduit, - * if any, to source token approvals from. The zero hash - * signifies that no conduit should be used, with direct - * approvals set on this contract. - * @param accumulatorStruct A struct containing conduit transfer data and its - * corresponding conduitKey. + * @param conduitKey A bytes32 value indicating what + * corresponding conduit, if any, to source + * token approvals from. The zero hash + * signifies that no conduit should be used + * (and direct approvals set on Consideration) + * @param accumulatorStruct A struct containing conduit transfer data + * and its corresponding conduitKey. * @param itemType The type of the item to transfer. * @param token The token to transfer. * @param from The originator of the transfer. @@ -358,7 +377,7 @@ contract ReferenceExecutor is ReferenceVerifiers, ReferenceTokenTransferrer { function _insert( bytes32 conduitKey, AccumulatorStruct memory accumulatorStruct, - uint256 itemType, + ConduitItemType itemType, address token, address from, address to, @@ -396,7 +415,7 @@ contract ReferenceExecutor is ReferenceVerifiers, ReferenceTokenTransferrer { // Insert new transfer into array. newTransfers[currentTransferLength] = ConduitTransfer( - ConduitItemType(itemType), + itemType, token, from, to, @@ -420,8 +439,8 @@ contract ReferenceExecutor is ReferenceVerifiers, ReferenceTokenTransferrer { * the "salt" parameter supplied by the deployer (i.e. the * conduit controller) when deploying the given conduit. * - * @return conduit The address of the conduit associated with the given - * conduit key. + * @return conduit The address of the conduit associated with the given + * conduit key. */ function _getConduit(bytes32 conduitKey) internal @@ -433,7 +452,7 @@ contract ReferenceExecutor is ReferenceVerifiers, ReferenceTokenTransferrer { // If the conduit does not have runtime code (i.e. is not deployed)... if (conduit.code.length == 0) { - // Revert with an error indicating an invalud conduit. + // Revert with an error indicating an invalid conduit. revert InvalidConduit(conduitKey, conduit); } } diff --git a/reference/lib/ReferenceFulfillmentApplier.sol b/reference/lib/ReferenceFulfillmentApplier.sol index 94c21e34b..e2c105cac 100644 --- a/reference/lib/ReferenceFulfillmentApplier.sol +++ b/reference/lib/ReferenceFulfillmentApplier.sol @@ -34,7 +34,7 @@ import { */ contract ReferenceFulfillmentApplier is FulfillmentApplicationErrors { /** - * @dev Internal view function to match offer items to consideration items + * @dev Internal pure function to match offer items to consideration items * on a group of orders via a supplied fulfillment. * * @param ordersToExecute The orders to match. @@ -52,7 +52,7 @@ contract ReferenceFulfillmentApplier is FulfillmentApplicationErrors { OrderToExecute[] memory ordersToExecute, FulfillmentComponent[] calldata offerComponents, FulfillmentComponent[] calldata considerationComponents - ) internal view returns (Execution memory execution) { + ) internal pure returns (Execution memory execution) { // Ensure 1+ of both offer and consideration components are supplied. if ( offerComponents.length == 0 || considerationComponents.length == 0 @@ -60,6 +60,8 @@ contract ReferenceFulfillmentApplier is FulfillmentApplicationErrors { revert OfferAndConsiderationRequiredOnFulfillment(); } + // Recipient does not need to be specified because it will always be set + // to that of the consideration. // Validate and aggregate consideration items and store the result as a // ReceivedItem. ReceivedItem memory considerationItem = ( @@ -84,7 +86,8 @@ contract ReferenceFulfillmentApplier is FulfillmentApplicationErrors { ) = _aggregateValidFulfillmentOfferItems( ordersToExecute, offerComponents, - 0 + 0, + address(0) // unused ); // Ensure offer and consideration share types, tokens and identifiers. @@ -143,6 +146,8 @@ contract ReferenceFulfillmentApplier is FulfillmentApplicationErrors { * approvals from. The zero hash signifies that * no conduit should be used (and direct * approvals set on Consideration) + * @param recipient The intended recipient for all received + * items. * * @return execution The transfer performed as a result of the fulfillment. */ @@ -150,7 +155,8 @@ contract ReferenceFulfillmentApplier is FulfillmentApplicationErrors { OrderToExecute[] memory ordersToExecute, Side side, FulfillmentComponent[] memory fulfillmentComponents, - bytes32 fulfillerConduitKey + bytes32 fulfillerConduitKey, + address recipient ) internal view returns (Execution memory execution) { // Retrieve orders array length and place on the stack. uint256 totalOrders = ordersToExecute.length; @@ -210,7 +216,8 @@ contract ReferenceFulfillmentApplier is FulfillmentApplicationErrors { return _aggregateValidFulfillmentOfferItems( ordersToExecute, fulfillmentComponents, - nextComponentIndex - 1 + nextComponentIndex - 1, + recipient ); } else { // Otherwise, fulfillment components are consideration @@ -227,13 +234,15 @@ contract ReferenceFulfillmentApplier is FulfillmentApplicationErrors { } /** - * @dev Internal pure function to check the indicated offer item matches original item. + * @dev Internal pure function to check the indicated offer item + * matches original item. * * @param orderToExecute The order to compare. * @param offer The offer to compare * @param execution The aggregated offer item * - * @return invalidFulfillment A boolean indicating whether the fulfillment is invalid. + * @return invalidFulfillment A boolean indicating whether the + * fulfillment is invalid. */ function _checkMatchingOffer( OrderToExecute memory orderToExecute, @@ -265,8 +274,9 @@ contract ReferenceFulfillmentApplier is FulfillmentApplicationErrors { function _aggregateValidFulfillmentOfferItems( OrderToExecute[] memory ordersToExecute, FulfillmentComponent[] memory offerComponents, - uint256 startIndex - ) internal view returns (Execution memory execution) { + uint256 startIndex, + address recipient + ) internal pure returns (Execution memory execution) { // Get the order index and item index of the offer component. uint256 orderIndex = offerComponents[startIndex].orderIndex; uint256 itemIndex = offerComponents[startIndex].itemIndex; @@ -293,7 +303,7 @@ contract ReferenceFulfillmentApplier is FulfillmentApplicationErrors { offer.token, offer.identifier, offer.amount, - payable(msg.sender) + payable(recipient) ), orderToExecute.offerer, orderToExecute.conduitKey @@ -308,7 +318,8 @@ contract ReferenceFulfillmentApplier is FulfillmentApplicationErrors { i < offerComponents.length; ++i ) { - // Get the order index and item index of the offer component. + // Get the order index and item index of the offer + // component. orderIndex = offerComponents[i].orderIndex; itemIndex = offerComponents[i].itemIndex; @@ -328,20 +339,27 @@ contract ReferenceFulfillmentApplier is FulfillmentApplicationErrors { if (invalidFulfillment) { break; } - // Get the spent item based on the offer components item index. + // Get the spent item based on the offer components + // item index. offer = orderToExecute.spentItems[itemIndex]; // Update the Received Item Amount. execution.item.amount = execution.item.amount + offer.amount; - // Zero out amount on original offerItem to indicate it is spent, + // Zero out amount on original offerItem to indicate + // it is spent, offer.amount = 0; - // Ensure the indicated offer item matches original item. + // Ensure the indicated offer item matches original + // item. invalidFulfillment = _checkMatchingOffer( orderToExecute, offer, execution ); + // Break if invalid + if (invalidFulfillment) { + break; + } } } } @@ -397,12 +415,14 @@ contract ReferenceFulfillmentApplier is FulfillmentApplicationErrors { } /** - * @dev Internal pure function to check the indicated consideration item matches original item. + * @dev Internal pure function to check the indicated consideration item + * matches original item. * * @param consideration The consideration to compare * @param receivedItem The aggregated received item * - * @return invalidFulfillment A boolean indicating whether the fulfillment is invalid. + * @return invalidFulfillment A boolean indicating whether the fulfillment + * is invalid. */ function _checkMatchingConsideration( ReceivedItem memory consideration, @@ -481,7 +501,8 @@ contract ReferenceFulfillmentApplier is FulfillmentApplicationErrors { i < considerationComponents.length; ++i ) { - // Get the order index and item index of the consideration component. + // Get the order index and item index of the consideration + // component. potentialCandidate.orderIndex = considerationComponents[i] .orderIndex; potentialCandidate.itemIndex = considerationComponents[i] @@ -494,7 +515,8 @@ contract ReferenceFulfillmentApplier is FulfillmentApplicationErrors { if (potentialCandidate.invalidFulfillment) { break; } - // Get the order based on consideration components order index. + // Get the order based on consideration components order + // index. orderToExecute = ordersToExecute[ potentialCandidate.orderIndex ]; @@ -516,14 +538,20 @@ contract ReferenceFulfillmentApplier is FulfillmentApplicationErrors { receivedItem.amount = receivedItem.amount + consideration.amount; - // Zero out amount on original consideration item to indicate it is spent + // Zero out amount on original consideration item to + // indicate it is spent consideration.amount = 0; - // Ensure the indicated consideration item matches original item. + // Ensure the indicated consideration item matches + // original item. potentialCandidate .invalidFulfillment = _checkMatchingConsideration( consideration, receivedItem ); + // Break if invalid + if (potentialCandidate.invalidFulfillment) { + break; + } } } } diff --git a/reference/lib/ReferenceGettersAndDerivers.sol b/reference/lib/ReferenceGettersAndDerivers.sol index 561e97dd9..60461d6ba 100644 --- a/reference/lib/ReferenceGettersAndDerivers.sol +++ b/reference/lib/ReferenceGettersAndDerivers.sol @@ -52,7 +52,8 @@ contract ReferenceGettersAndDerivers is ReferenceConsiderationBase { } /** - * @dev Internal view function to derive the EIP-712 hash for a consideration item. + * @dev Internal view function to derive the EIP-712 hash for a + * consideration item. * * @param considerationItem The consideration item to hash. * @@ -84,13 +85,13 @@ contract ReferenceGettersAndDerivers is ReferenceConsiderationBase { * caller. * * @param orderParameters The parameters of the order to hash. - * @param nonce The nonce of the order to hash. + * @param counter The counter of the order to hash. * * @return orderHash The hash. */ function _deriveOrderHash( OrderParameters memory orderParameters, - uint256 nonce + uint256 counter ) internal view returns (bytes32 orderHash) { // Designate new memory regions for offer and consideration item hashes. bytes32[] memory offerHashes = new bytes32[]( @@ -134,7 +135,7 @@ contract ReferenceGettersAndDerivers is ReferenceConsiderationBase { orderParameters.zoneHash, orderParameters.salt, orderParameters.conduitKey, - nonce + counter ) ); } diff --git a/reference/lib/ReferenceNonceManager.sol b/reference/lib/ReferenceNonceManager.sol deleted file mode 100644 index d8ee6edcc..000000000 --- a/reference/lib/ReferenceNonceManager.sol +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.7; - -// prettier-ignore -import { - ConsiderationEventsAndErrors -} from "contracts/interfaces/ConsiderationEventsAndErrors.sol"; - -import { ReferenceReentrancyGuard } from "./ReferenceReentrancyGuard.sol"; - -/** - * @title NonceManager - * @author 0age - * @notice NonceManager contains a storage mapping and related functionality - * for retrieving and incrementing a per-offerer nonce. - */ -contract ReferenceNonceManager is - ConsiderationEventsAndErrors, - ReferenceReentrancyGuard -{ - // Only orders signed using an offerer's current nonce are fulfillable. - mapping(address => uint256) private _nonces; - - /** - * @dev Internal function to cancel all orders from a given offerer with a - * given zone in bulk by incrementing a nonce. Note that only the - * offerer may increment the nonce. - * - * @return newNonce The new nonce. - */ - function _incrementNonce() internal notEntered returns (uint256 newNonce) { - // Increment current nonce for the supplied offerer. - newNonce = ++_nonces[msg.sender]; - - // Emit an event containing the new nonce. - emit NonceIncremented(newNonce, msg.sender); - } - - /** - * @dev Internal view function to retrieve the current nonce for a given - * offerer. - * - * @param offerer The offerer in question. - * - * @return currentNonce The current nonce. - */ - function _getNonce(address offerer) - internal - view - returns (uint256 currentNonce) - { - // Return the nonce for the supplied offerer. - currentNonce = _nonces[offerer]; - } -} diff --git a/reference/lib/ReferenceOrderCombiner.sol b/reference/lib/ReferenceOrderCombiner.sol index 04de0baf9..570fb33e2 100644 --- a/reference/lib/ReferenceOrderCombiner.sol +++ b/reference/lib/ReferenceOrderCombiner.sol @@ -81,10 +81,11 @@ contract ReferenceOrderCombiner is * considered valid. * * @param ordersToExecute The orders to execute. This is an - * explicit version of advancedOrders without - * memory optimization, that provides - * an array of spentItems and receivedItems - * for fulfillment and event emission. + * explicit version of advancedOrders + * without memory optimization, that + * provides an array of spentItems and + * receivedItems for fulfillment and + * event emission. * * @param criteriaResolvers An array where each element contains a * reference to a specific offer or @@ -93,7 +94,7 @@ contract ReferenceOrderCombiner is * is contained in the merkle root held by * the item in question's criteria element. * Note that an empty criteria indicates - * that any (transferrable) token + * that any (transferable) token * identifier on the token in question is * valid and that no associated proof needs * to be supplied. @@ -109,14 +110,17 @@ contract ReferenceOrderCombiner is * approvals from. The zero hash signifies * that no conduit should be used (and * direct approvals set on Consideration). + * @param recipient The intended recipient for all received + * items. * @param maximumFulfilled The maximum number of orders to fulfill. * - * @return availableOrders An array of booleans indicating if each order - * with an index corresponding to the index of the - * returned boolean was fulfillable or not. - * @return executions An array of elements indicating the sequence of - * transfers performed as part of matching the given - * orders. + * @return availableOrders An array of booleans indicating if each + * order with an index corresponding to the + * index of the returned boolean was + * fulfillable or not. + * @return executions An array of elements indicating the + * sequence of transfers performed as part + * of matching the given orders. */ function _fulfillAvailableAdvancedOrders( AdvancedOrder[] memory advancedOrders, @@ -125,6 +129,7 @@ contract ReferenceOrderCombiner is FulfillmentComponent[][] calldata offerFulfillments, FulfillmentComponent[][] calldata considerationFulfillments, bytes32 fulfillerConduitKey, + address recipient, uint256 maximumFulfilled ) internal @@ -136,7 +141,8 @@ contract ReferenceOrderCombiner is ordersToExecute, criteriaResolvers, false, // Signifies that invalid orders should NOT revert. - maximumFulfilled + maximumFulfilled, + recipient ); // Execute transfers. @@ -144,7 +150,8 @@ contract ReferenceOrderCombiner is ordersToExecute, offerFulfillments, considerationFulfillments, - fulfillerConduitKey + fulfillerConduitKey, + recipient ); // Return order fulfillment details and executions. @@ -164,20 +171,22 @@ contract ReferenceOrderCombiner is * offer or consideration, a token identifier, and * a proof that the supplied token identifier is * contained in the order's merkle root. Note that - * a root of zero indicates that any transferrable + * a root of zero indicates that any transferable * token identifier is valid and that no proof * needs to be supplied. * @param revertOnInvalid A boolean indicating whether to revert on any * order being invalid; setting this to false will * instead cause the invalid order to be skipped. * @param maximumFulfilled The maximum number of orders to fulfill. + * @param recipient The intended recipient for all received items. */ function _validateOrdersAndPrepareToFulfill( AdvancedOrder[] memory advancedOrders, OrderToExecute[] memory ordersToExecute, CriteriaResolver[] memory criteriaResolvers, bool revertOnInvalid, - uint256 maximumFulfilled + uint256 maximumFulfilled, + address recipient ) internal { // Read length of orders array and place on the stack. uint256 totalOrders = advancedOrders.length; @@ -185,6 +194,10 @@ contract ReferenceOrderCombiner is // Track the order hash for each order being fulfilled. bytes32[] memory orderHashes = new bytes32[](totalOrders); + // Check if we are in a match function + bool nonMatchFn = msg.sig != 0x55944a42 && msg.sig != 0xa8174404; + bool anyNativeOfferItems; + // Iterate over each order. for (uint256 i = 0; i < totalOrders; ++i) { // Retrieve the current order. @@ -231,20 +244,17 @@ contract ReferenceOrderCombiner is // Otherwise, track the order hash in question. orderHashes[i] = orderHash; - // Decrement the number of fulfilled orders. - maximumFulfilled--; + // Skip underflow check as maximumFulfilled is nonzero. + unchecked { + // Decrement the number of fulfilled orders. + maximumFulfilled--; + } // Place the start time for the order on the stack. uint256 startTime = advancedOrder.parameters.startTime; - // Derive the duration for the order and place it on the stack. - uint256 duration = advancedOrder.parameters.endTime - startTime; - - // Derive time elapsed since the order started & place on stack. - uint256 elapsed = block.timestamp - startTime; - - // Derive time remaining until order expires and place on stack. - uint256 remaining = duration - elapsed; + // Place the end for the order on the stack. + uint256 endTime = advancedOrder.parameters.endTime; // Retrieve array of offer items for the order in question. OfferItem[] memory offer = advancedOrder.parameters.offer; @@ -254,6 +264,10 @@ contract ReferenceOrderCombiner is // Retrieve the offer item. OfferItem memory offerItem = offer[j]; + anyNativeOfferItems = + anyNativeOfferItems || + offerItem.itemType == ItemType.NATIVE; + // Apply order fill fraction to offer item end amount. uint256 endAmount = _getFraction( numerator, @@ -281,9 +295,8 @@ contract ReferenceOrderCombiner is offerItem.startAmount = _locateCurrentAmount( offerItem.startAmount, offerItem.endAmount, - elapsed, - remaining, - duration, + startTime, + endTime, false // Round down. ); @@ -331,9 +344,8 @@ contract ReferenceOrderCombiner is _locateCurrentAmount( considerationItem.startAmount, considerationItem.endAmount, - elapsed, - remaining, - duration, + startTime, + endTime, true // Round up. ) ); @@ -344,18 +356,14 @@ contract ReferenceOrderCombiner is } } + if (anyNativeOfferItems && nonMatchFn) { + revert InvalidNativeOfferItem(); + } + // Apply criteria resolvers to each order as applicable. _applyCriteriaResolvers(ordersToExecute, criteriaResolvers); - // Determine the fulfiller (revertOnInvalid ? address(0) : msg.sender). - address fulfiller; - if (revertOnInvalid) { - fulfiller = address(0); - } else { - fulfiller = msg.sender; - } // Emit an event for each order signifying that it has been fulfilled. - // Iterate over each order. for (uint256 i = 0; i < totalOrders; ++i) { // Do not emit an event if no order hash is present. @@ -371,7 +379,8 @@ contract ReferenceOrderCombiner is // Get the array of spentItems from the orderToExecute struct. SpentItem[] memory spentItems = ordersToExecute[i].spentItems; - // Get the array of spent receivedItems from the orderToExecute struct. + // Get the array of spent receivedItems from the + // orderToExecute struct. ReceivedItem[] memory receivedItems = ordersToExecute[i] .receivedItems; @@ -380,7 +389,7 @@ contract ReferenceOrderCombiner is orderHashes[i], orderParameters.offerer, orderParameters.zone, - fulfiller, + recipient, spentItems, receivedItems ); @@ -400,14 +409,15 @@ contract ReferenceOrderCombiner is * order formatting will cause the entire batch to fail. * * @param ordersToExecute The orders to execute. This is an - * explicit version of advancedOrders without - * memory optimization, that provides - * an array of spentItems and receivedItems - * for fulfillment and event emission. + * explicit version of advancedOrders + * without memory optimization, that + * provides an array of spentItems and + * receivedItems for fulfillment and + * event emission. * Note that both the offerer and the * fulfiller must first approve this - * contract (or the conduit if indicated by - * the order) to transfer any relevant + * contract (or the conduit if indicated + * by the order) to transfer any relevant * tokens on their behalf and that * contracts must implement * `onERC1155Received` in order to receive @@ -430,19 +440,23 @@ contract ReferenceOrderCombiner is * approvals from. The zero hash signifies * that no conduit should be used (and * direct approvals set on Consideration). + * @param recipient The intended recipient for all received + * items. * - * * @return availableOrders An array of booleans indicating if each order - * with an index corresponding to the index of the - * returned boolean was fulfillable or not. - * @return executions An array of elements indicating the sequence of - * transfers performed as part of matching the given - * orders. + * @return availableOrders An array of booleans indicating if each + * order with an index corresponding to the + * index of the returned boolean was + * fulfillable or not. + * @return executions An array of elements indicating the + * sequence of transfers performed as part + * of matching the given orders. */ function _executeAvailableFulfillments( OrderToExecute[] memory ordersToExecute, FulfillmentComponent[][] memory offerFulfillments, FulfillmentComponent[][] memory considerationFulfillments, - bytes32 fulfillerConduitKey + bytes32 fulfillerConduitKey, + address recipient ) internal returns (bool[] memory availableOrders, Execution[] memory executions) @@ -473,13 +487,17 @@ contract ReferenceOrderCombiner is ordersToExecute, Side.OFFER, components, - fulfillerConduitKey + fulfillerConduitKey, + recipient ); // If offerer and recipient on the execution are the same... if (execution.item.recipient == execution.offerer) { - // increment total filtered executions. - totalFilteredExecutions += 1; + // Executions start at 0, infeasible to increment > 2^256. + unchecked { + // Increment total filtered executions. + ++totalFilteredExecutions; + } } else { // Otherwise, assign the execution to the executions array. executions[i - totalFilteredExecutions] = execution; @@ -498,13 +516,17 @@ contract ReferenceOrderCombiner is ordersToExecute, Side.CONSIDERATION, components, - fulfillerConduitKey + fulfillerConduitKey, + recipient // unused ); // If offerer and recipient on the execution are the same... if (execution.item.recipient == execution.offerer) { - // increment total filtered executions. - totalFilteredExecutions += 1; + // Executions start at 0, infeasible to increment > 2^256. + unchecked { + // Increment total filtered executions. + ++totalFilteredExecutions; + } } else { // Otherwise, assign the execution to the executions array. executions[ @@ -679,7 +701,7 @@ contract ReferenceOrderCombiner is * offer or consideration, a token identifier, and * a proof that the supplied token identifier is * contained in the order's merkle root. Note that - * an empty root indicates that any (transferrable) + * an empty root indicates that any (transferable) * token identifier is valid and that no associated * proof needs to be supplied. * @param fulfillments An array of elements allocating offer components @@ -688,8 +710,8 @@ contract ReferenceOrderCombiner is * order for the match operation to be valid. * * @return executions An array of elements indicating the sequence of - * transfers performed as part of matching the given - * orders. + * transfers performed as part of matching the + * given orders. */ function _matchAdvancedOrders( AdvancedOrder[] memory advancedOrders, @@ -708,7 +730,8 @@ contract ReferenceOrderCombiner is ordersToExecute, criteriaResolvers, true, // Signifies that invalid orders should revert. - advancedOrders.length + advancedOrders.length, + address(0) ); // Fulfill the orders using the supplied fulfillments. @@ -759,8 +782,11 @@ contract ReferenceOrderCombiner is // If offerer and recipient on the execution are the same... if (execution.item.recipient == execution.offerer) { - // increment total filtered executions. - totalFilteredExecutions += 1; + // Executions start at 0, infeasible to increment > 2^256. + unchecked { + // Increment total filtered executions. + ++totalFilteredExecutions; + } } else { // Otherwise, assign the execution to the executions array. executions[i - totalFilteredExecutions] = execution; diff --git a/reference/lib/ReferenceOrderFulfiller.sol b/reference/lib/ReferenceOrderFulfiller.sol index 3285af82d..a0f424908 100644 --- a/reference/lib/ReferenceOrderFulfiller.sol +++ b/reference/lib/ReferenceOrderFulfiller.sol @@ -67,20 +67,22 @@ contract ReferenceOrderFulfiller is * that the supplied token identifier is * contained in the order's merkle root. Note * that a criteria of zero indicates that any - * (transferrable) token identifier is valid and + * (transferable) token identifier is valid and * that no proof needs to be supplied. * @param fulfillerConduitKey A bytes32 value indicating what conduit, if * any, to source the fulfiller's token approvals * from. The zero hash signifies that no conduit * should be used (and direct approvals set on * Consideration). + * @param recipient The intended recipient for all received items. * * @return A boolean indicating whether the order has been fulfilled. */ function _validateAndFulfillAdvancedOrder( AdvancedOrder memory advancedOrder, CriteriaResolver[] memory criteriaResolvers, - bytes32 fulfillerConduitKey + bytes32 fulfillerConduitKey, + address recipient ) internal returns (bool) { // Declare empty bytes32 array (unused, will remain empty). bytes32[] memory priorOrderHashes; @@ -108,8 +110,8 @@ contract ReferenceOrderFulfiller is orderParameters, fillNumerator, fillDenominator, - orderParameters.conduitKey, - fulfillerConduitKey + fulfillerConduitKey, + recipient ); // Emit an event signifying that the order has been fulfilled. @@ -117,7 +119,7 @@ contract ReferenceOrderFulfiller is orderHash, orderParameters.offerer, orderParameters.zone, - msg.sender, + recipient, orderToExecute.spentItems, orderToExecute.receivedItems ); @@ -134,49 +136,42 @@ contract ReferenceOrderFulfiller is * @param numerator A value indicating the portion of the order * that should be filled. * @param denominator A value indicating the total order size. - * @param offererConduitKey An address indicating what conduit, if any, to - * source the offerer's token approvals from. The - * zero hash signifies that no conduit should be - * used (and direct approvals set on - * Consideration). * @param fulfillerConduitKey A bytes32 value indicating what conduit, if * any, to source the fulfiller's token approvals * from. The zero hash signifies that no conduit * should be used (and direct approvals set on * Consideration). - * - * @return orderToExecute Returns the order of items that are being transferred. - * This will be used for the OrderFulfilled Event. + * @param recipient The intended recipient for all received items. + * @return orderToExecute Returns the order of items that are being + * transferred. This will be used for the + * OrderFulfilled Event. */ function _applyFractionsAndTransferEach( OrderParameters memory orderParameters, uint256 numerator, uint256 denominator, - bytes32 offererConduitKey, - bytes32 fulfillerConduitKey + bytes32 fulfillerConduitKey, + address recipient ) internal returns (OrderToExecute memory orderToExecute) { // Derive order duration, time elapsed, and time remaining. // Store in memory to avoid stack too deep issues. FractionData memory fractionData = FractionData( numerator, denominator, - offererConduitKey, fulfillerConduitKey, - (orderParameters.endTime - orderParameters.startTime), - (block.timestamp - orderParameters.startTime), - ((orderParameters.endTime - orderParameters.startTime) - - (block.timestamp - orderParameters.startTime)) + orderParameters.startTime, + orderParameters.endTime ); - // Put ether value supplied by the caller on the stack. - uint256 etherRemaining = msg.value; - // Create the accumulator struct. AccumulatorStruct memory accumulatorStruct; // Get the offerer of the order. address offerer = orderParameters.offerer; + // Get the conduitKey of the order + bytes32 conduitKey = orderParameters.conduitKey; + // Create the array to store the spent items for event. orderToExecute.spentItems = new SpentItem[]( orderParameters.offer.length @@ -188,6 +183,11 @@ contract ReferenceOrderFulfiller is for (uint256 i = 0; i < orderParameters.offer.length; ++i) { // Retrieve the offer item. OfferItem memory offerItem = orderParameters.offer[i]; + // Offer items for the native token can not be received + // outside of a match order function. + if (offerItem.itemType == ItemType.NATIVE) { + revert InvalidNativeOfferItem(); + } // Apply fill fraction to derive offer item amount to transfer. uint256 amount = _applyFraction( @@ -203,7 +203,7 @@ contract ReferenceOrderFulfiller is offerItem.token, offerItem.identifierOrCriteria, amount, - payable(msg.sender) + payable(recipient) ); // Create Spent Item for the OrderFulfilled event. @@ -214,23 +214,8 @@ contract ReferenceOrderFulfiller is amount ); - // Reduce available value if offer spent ETH or a native token. - if (receivedItem.itemType == ItemType.NATIVE) { - // Ensure that sufficient native tokens are still available. - if (amount > etherRemaining) { - revert InsufficientEtherSupplied(); - } - // Reduce ether remaining by amount. - etherRemaining -= amount; - } - - // Transfer the item from the offerer to the caller. - _transfer( - receivedItem, - offerer, - fractionData.offererConduitKey, - accumulatorStruct - ); + // Transfer the item from the offerer to the recipient. + _transfer(receivedItem, offerer, conduitKey, accumulatorStruct); } } @@ -239,6 +224,9 @@ contract ReferenceOrderFulfiller is orderParameters.consideration.length ); + // Put ether value supplied by the caller on the stack. + uint256 etherRemaining = msg.value; + // Declare a nested scope to minimize stack depth. { // Iterate over each consideration on the order. @@ -352,8 +340,8 @@ contract ReferenceOrderFulfiller is } /** - * @dev Internal pure function to convert an advanced order to an order to execute with - * numerator of 1. + * @dev Internal pure function to convert an advanced order to an order + * to execute with numerator of 1. * * @param advancedOrder The advanced order to convert. * @@ -428,8 +416,8 @@ contract ReferenceOrderFulfiller is } /** - * @dev Internal pure function to convert an array of advanced orders to an array of - * orders to execute. + * @dev Internal pure function to convert an array of advanced orders to + * an array of orders to execute. * * @param advancedOrders The advanced orders to convert. * diff --git a/reference/lib/ReferenceOrderValidator.sol b/reference/lib/ReferenceOrderValidator.sol index c641241cc..ce0734902 100644 --- a/reference/lib/ReferenceOrderValidator.sol +++ b/reference/lib/ReferenceOrderValidator.sol @@ -56,7 +56,7 @@ contract ReferenceOrderValidator is bytes memory signature ) internal { // Retrieve the order status for the given order hash. - OrderStatus memory orderStatus = _orderStatus[orderHash]; + OrderStatus storage orderStatus = _orderStatus[orderHash]; // Ensure order is fillable and is not cancelled. _verifyOrderStatus( @@ -72,10 +72,10 @@ contract ReferenceOrderValidator is } // Update order status as fully filled, packing struct values. - _orderStatus[orderHash].isValidated = true; - _orderStatus[orderHash].isCancelled = false; - _orderStatus[orderHash].numerator = 1; - _orderStatus[orderHash].denominator = 1; + orderStatus.isValidated = true; + orderStatus.isCancelled = false; + orderStatus.numerator = 1; + orderStatus.denominator = 1; } /** @@ -92,7 +92,7 @@ contract ReferenceOrderValidator is * identifier, and a proof that the supplied token * identifier is contained in the order's merkle * root. Note that a criteria of zero indicates - * that any (transferrable) token identifier is + * that any (transferable) token identifier is * valid and that no proof needs to be supplied. * @param revertOnInvalid A boolean indicating whether to revert if the * order is invalid due to the time or order status. @@ -152,10 +152,8 @@ contract ReferenceOrderValidator is revert PartialFillsNotEnabledForOrder(); } - // Retrieve current nonce and use it w/ parameters to derive order hash. - orderHash = _assertConsiderationLengthAndGetNoncedOrderHash( - orderParameters - ); + // Retrieve current counter and use it w/ parameters to get order hash. + orderHash = _assertConsiderationLengthAndGetOrderHash(orderParameters); // Ensure a valid submitter. _assertRestrictedAdvancedOrderValidity( @@ -170,7 +168,7 @@ contract ReferenceOrderValidator is ); // Retrieve the order status using the derived order hash. - OrderStatus memory orderStatus = _orderStatus[orderHash]; + OrderStatus storage orderStatus = _orderStatus[orderHash]; // Ensure order is fillable and is not cancelled. if ( @@ -195,8 +193,8 @@ contract ReferenceOrderValidator is } // Read filled amount as numerator and denominator and put on the stack. - uint256 filledNumerator = orderStatus.numerator; - uint256 filledDenominator = orderStatus.denominator; + uint256 filledNumerator = uint256(orderStatus.numerator); + uint256 filledDenominator = uint256(orderStatus.denominator); // If order currently has a non-zero denominator it is partially filled. if (filledDenominator != 0) { @@ -222,23 +220,71 @@ contract ReferenceOrderValidator is numerator = denominator - filledNumerator; } + // Increment the filled numerator by the new numerator. + filledNumerator += numerator; + + // Ensure fractional amounts are below max uint120. + if ( + filledNumerator > type(uint120).max || + denominator > type(uint120).max + ) { + // Derive greatest common divisor using euclidean algorithm. + uint256 scaleDown = _greatestCommonDivisor( + numerator, + _greatestCommonDivisor(filledNumerator, denominator) + ); + + // Note: this may not be necessary — need to validate. + uint256 safeScaleDown = scaleDown == 0 ? 1 : scaleDown; + + // Scale all fractional values down by gcd. + numerator = numerator / safeScaleDown; + filledNumerator = filledNumerator / safeScaleDown; + denominator = denominator / safeScaleDown; + + // Perform the overflow check a second time. + uint256 maxOverhead = type(uint256).max - type(uint120).max; + ((filledNumerator + maxOverhead) & (denominator + maxOverhead)); + } + // Update order status and fill amount, packing struct values. - _orderStatus[orderHash].isValidated = true; - _orderStatus[orderHash].isCancelled = false; - _orderStatus[orderHash].numerator = uint120( - filledNumerator + numerator - ); - _orderStatus[orderHash].denominator = uint120(denominator); + orderStatus.isValidated = true; + orderStatus.isCancelled = false; + orderStatus.numerator = uint120(filledNumerator); + orderStatus.denominator = uint120(denominator); } else { // Update order status and fill amount, packing struct values. - _orderStatus[orderHash].isValidated = true; - _orderStatus[orderHash].isCancelled = false; - _orderStatus[orderHash].numerator = uint120(numerator); - _orderStatus[orderHash].denominator = uint120(denominator); + orderStatus.isValidated = true; + orderStatus.isCancelled = false; + orderStatus.numerator = uint120(numerator); + orderStatus.denominator = uint120(denominator); } // Return order hash, new numerator and denominator. - return (orderHash, numerator, denominator); + return (orderHash, uint120(numerator), uint120(denominator)); + } + + /** + * @dev Internal function to derive the greatest common divisor of two + * values using the classical euclidian algorithm. + * + * @param a The first value. + * @param b The second value. + * + * @return greatestCommonDivisor The greatest common divisor. + */ + function _greatestCommonDivisor(uint256 a, uint256 b) + internal + pure + returns (uint256 greatestCommonDivisor) + { + while (b > 0) { + uint256 c = b; + b = a % c; + a = c; + } + + greatestCommonDivisor = a; } /** @@ -255,6 +301,8 @@ contract ReferenceOrderValidator is notEntered returns (bool) { + // Declare variables outside of the loop. + OrderStatus storage orderStatus; address offerer; address zone; @@ -274,7 +322,7 @@ contract ReferenceOrderValidator is revert InvalidCanceller(); } - // Derive order hash using the order parameters and the nonce. + // Derive order hash using the order parameters and the counter. bytes32 orderHash = _deriveOrderHash( OrderParameters( offerer, @@ -289,12 +337,15 @@ contract ReferenceOrderValidator is order.conduitKey, order.consideration.length ), - order.nonce + order.counter ); + // Retrieve the order status using the derived order hash. + orderStatus = _orderStatus[orderHash]; + // Update the order status as not valid and cancelled. - _orderStatus[orderHash].isValidated = false; - _orderStatus[orderHash].isCancelled = true; + orderStatus.isValidated = false; + orderStatus.isCancelled = true; // Emit an event signifying that the order has been cancelled. emit OrderCancelled(orderHash, offerer, zone); @@ -319,6 +370,7 @@ contract ReferenceOrderValidator is returns (bool) { // Declare variables outside of the loop. + OrderStatus storage orderStatus; bytes32 orderHash; address offerer; @@ -336,13 +388,13 @@ contract ReferenceOrderValidator is // Move offerer from memory to the stack. offerer = orderParameters.offerer; - // Get current nonce and use it w/ params to derive order hash. - orderHash = _assertConsiderationLengthAndGetNoncedOrderHash( + // Get current counter and use it w/ params to derive order hash. + orderHash = _assertConsiderationLengthAndGetOrderHash( orderParameters ); // Retrieve the order status using the derived order hash. - OrderStatus memory orderStatus = _orderStatus[orderHash]; + orderStatus = _orderStatus[orderHash]; // Ensure order is fillable and retrieve the filled amount. _verifyOrderStatus( @@ -358,7 +410,7 @@ contract ReferenceOrderValidator is _verifySignature(offerer, orderHash, order.signature); // Update order status to mark the order as valid. - _orderStatus[orderHash].isValidated = true; + orderStatus.isValidated = true; // Emit an event signifying the order has been validated. emit OrderValidated(orderHash, offerer, orderParameters.zone); @@ -396,7 +448,7 @@ contract ReferenceOrderValidator is ) { // Retrieve the order status using the order hash. - OrderStatus memory orderStatus = _orderStatus[orderHash]; + OrderStatus storage orderStatus = _orderStatus[orderHash]; // Return the fields on the order status. return ( diff --git a/reference/lib/ReferenceReentrancyGuard.sol b/reference/lib/ReferenceReentrancyGuard.sol index 50596c0ef..73fc0cb40 100644 --- a/reference/lib/ReferenceReentrancyGuard.sol +++ b/reference/lib/ReferenceReentrancyGuard.sol @@ -24,7 +24,8 @@ contract ReferenceReentrancyGuard is ReentrancyErrors { } /** - * @dev Modifier to set the reentrancy guard sentinel value for the duration of the call + * @dev Modifier to set the reentrancy guard sentinel value for the duration + * of the call. */ modifier nonReentrant() { _reentrancyGuard = _ENTERED; @@ -33,8 +34,8 @@ contract ReferenceReentrancyGuard is ReentrancyErrors { } /** - * @dev Modifier to check that the sentinel value for the reentrancy guard is not currently set - * by a previous call + * @dev Modifier to check that the sentinel value for the reentrancy guard + * is not currently set by a previous call. */ modifier notEntered() { if (_reentrancyGuard == _ENTERED) { diff --git a/reference/lib/ReferenceSignatureVerification.sol b/reference/lib/ReferenceSignatureVerification.sol index 2a2b91c4d..73729e658 100644 --- a/reference/lib/ReferenceSignatureVerification.sol +++ b/reference/lib/ReferenceSignatureVerification.sol @@ -39,8 +39,14 @@ contract ReferenceSignatureVerification is SignatureVerificationErrors { bytes32 s; uint8 v; - // If signature contains 64 bytes, parse as EIP-2098 signature. (r+s&v) - if (signature.length == 64) { + if (signer.code.length > 0) { + // If signer is a contract, try verification via EIP-1271. + _assertValidEIP1271Signature(signer, digest, signature); + + // Return early if the ERC-1271 signature check succeeded. + return; + } else if (signature.length == 64) { + // If signature contains 64 bytes, parse as EIP-2098 signature. (r+s&v) // Declare temporary vs that will be decomposed into s and v. bytes32 vs; @@ -58,12 +64,7 @@ contract ReferenceSignatureVerification is SignatureVerificationErrors { revert BadSignatureV(v); } } else { - // For all other signature lengths, try verification via EIP-1271. - // Attempt EIP-1271 static call to signer in case it's a contract. - _assertValidEIP1271Signature(signer, digest, signature); - - // Return early if the ERC-1271 signature check succeeded. - return; + revert InvalidSignature(); } // Attempt to recover signer using the digest and signature parameters. @@ -71,7 +72,7 @@ contract ReferenceSignatureVerification is SignatureVerificationErrors { // Disallow invalid signers. if (recoveredSigner == address(0)) { - revert InvalidSignature(); + revert InvalidSigner(); // Should a signer be recovered, but it doesn't match the signer... } else if (recoveredSigner != signer) { // Attempt EIP-1271 static call to signer in case it's a contract. diff --git a/reference/lib/ReferenceVerifiers.sol b/reference/lib/ReferenceVerifiers.sol index 3d5644d5a..5615eff38 100644 --- a/reference/lib/ReferenceVerifiers.sol +++ b/reference/lib/ReferenceVerifiers.sol @@ -90,7 +90,7 @@ contract ReferenceVerifiers is } /** - * @dev Internal pure function to validate that a given order is fillable + * @dev Internal view function to validate that a given order is fillable * and not cancelled based on the order status. * * @param orderHash The order hash. @@ -106,10 +106,10 @@ contract ReferenceVerifiers is */ function _verifyOrderStatus( bytes32 orderHash, - OrderStatus memory orderStatus, + OrderStatus storage orderStatus, bool onlyAllowUnused, bool revertOnInvalid - ) internal pure returns (bool valid) { + ) internal view returns (bool valid) { // Ensure that the order has not been cancelled. if (orderStatus.isCancelled) { // Only revert if revertOnInvalid has been supplied as true. @@ -121,14 +121,17 @@ contract ReferenceVerifiers is return false; } + // Read order status numerator from storage and place on stack. + uint256 orderStatusNumerator = orderStatus.numerator; + // If the order is not entirely unused... - if (orderStatus.numerator != 0) { + if (orderStatusNumerator != 0) { // ensure the order has not been partially filled when not allowed. if (onlyAllowUnused) { // Always revert on partial fills when onlyAllowUnused is true. revert OrderPartiallyFilled(orderHash); // Otherwise, ensure that order has not been entirely filled. - } else if (orderStatus.numerator >= orderStatus.denominator) { + } else if (orderStatusNumerator >= orderStatus.denominator) { // Only revert if revertOnInvalid has been supplied as true. if (revertOnInvalid) { revert OrderAlreadyFilled(orderHash); diff --git a/reference/lib/ReferenceZoneInteraction.sol b/reference/lib/ReferenceZoneInteraction.sol index e564ed6eb..dc9578f0b 100644 --- a/reference/lib/ReferenceZoneInteraction.sol +++ b/reference/lib/ReferenceZoneInteraction.sol @@ -70,7 +70,7 @@ contract ReferenceZoneInteraction is ZoneInteractionErrors { * identifier, and a proof that the supplied token * identifier is contained in the order's merkle * root. Note that a criteria of zero indicates - * that any (transferrable) token identifier is + * that any (transferable) token identifier is * valid and that no proof needs to be supplied. * @param priorOrderHashes The order hashes of each order supplied prior to * the current order as part of a "match" variety diff --git a/reference/shim/Shim.sol b/reference/shim/Shim.sol index 867b7f9ca..46fd7f5c7 100644 --- a/reference/shim/Shim.sol +++ b/reference/shim/Shim.sol @@ -12,6 +12,7 @@ import { TestERC20 } from "contracts/test/TestERC20.sol"; import { TestERC721 } from "contracts/test/TestERC721.sol"; import { TestERC1155 } from "contracts/test/TestERC1155.sol"; import { TestZone } from "contracts/test/TestZone.sol"; +import { TransferHelper } from "contracts/helpers/TransferHelper.sol"; // prettier-ignore import { ImmutableCreate2FactoryInterface diff --git a/remappings.txt b/remappings.txt index b88c94f2c..44817140a 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,5 +1,5 @@ -ds-test=lib/ds-test/src/ -forge-std=lib/forge-std/src/ -@rari-capital/solmate/=lib/solmate/ -contracts/=contracts/ -murky/=lib/murky/src/ \ No newline at end of file +murky/=./lib/murky/src/ +ds-test/=./lib/ds-test/src/ +solmate/=./lib/solmate/src/ +forge-std/=./lib/forge-std/src/ +@rari-capital/solmate=./lib/solmate/ \ No newline at end of file diff --git a/test/findings/AdditionalRecipientsOffByOne.spec.ts b/test/findings/AdditionalRecipientsOffByOne.spec.ts new file mode 100644 index 000000000..3a26f1110 --- /dev/null +++ b/test/findings/AdditionalRecipientsOffByOne.spec.ts @@ -0,0 +1,158 @@ +import { expect } from "chai"; +import { constants, Wallet } from "ethers"; +import { + ConsiderationInterface, + TestERC20, + TestERC721, +} from "../../typechain-types"; +import { buildOrderStatus, getBasicOrderParameters } from "../utils/encoding"; +import { seaportFixture, SeaportFixtures } from "../utils/fixtures"; +import { getWalletWithEther } from "../utils/impersonate"; +import { AdvancedOrder, ConsiderationItem } from "../utils/types"; +import { getScuffedContract } from "scuffed-abi"; +import { hexZeroPad } from "ethers/lib/utils"; +import { network } from "hardhat"; + +const IS_FIXED = true; + +describe("Additional recipients off by one error allows skipping second consideration", async () => { + let alice: Wallet; + let bob: Wallet; + let carol: Wallet; + let order: AdvancedOrder; + let orderHash: string; + let testERC20: TestERC20; + let testERC721: TestERC721; + let marketplaceContract: ConsiderationInterface; + let mintAndApprove721: SeaportFixtures["mintAndApprove721"]; + let mintAndApproveERC20: SeaportFixtures["mintAndApproveERC20"]; + let getTestItem20: SeaportFixtures["getTestItem20"]; + let getTestItem721: SeaportFixtures["getTestItem721"]; + let createOrder: SeaportFixtures["createOrder"]; + + after(async () => { + await network.provider.request({ + method: "hardhat_reset", + }); + }); + + before(async function () { + alice = await getWalletWithEther(); + bob = await getWalletWithEther(); + carol = await getWalletWithEther(); + ({ + mintAndApprove721, + mintAndApproveERC20, + marketplaceContract, + getTestItem20, + getTestItem721, + createOrder, + testERC20, + testERC721, + } = await seaportFixture(await getWalletWithEther())); + // ERC721 with ID = 1 + await mintAndApprove721(alice, marketplaceContract.address, 1); + // ERC20 with amount = 1100 + await mintAndApproveERC20(bob, marketplaceContract.address, 1100); + }); + + it("Alice offers to sell an NFT for 1000 DAI plus 100 in fees for Carol", async () => { + const offer = [getTestItem721(1, 1, 1)]; + + const consideration: ConsiderationItem[] = [ + getTestItem20(1000, 1000, alice.address), + getTestItem20(100, 100, carol.address), + ]; + + const results = await createOrder( + alice, + constants.AddressZero, // zone + offer, + consideration, + 0, // FULL_OPEN + [], // criteria + null, // timeFlag + alice, // signer + constants.HashZero, // zoneHash + constants.HashZero, // conduitKey + true // extraCheap + ); + order = results.order; + orderHash = results.orderHash; + + // OrderStatus is not validated + let orderStatus = await marketplaceContract.getOrderStatus(orderHash); + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(false, false, 0, 0) + ); + + // Bob validates the order + await expect(marketplaceContract.connect(bob).validate([order])) + .to.emit(marketplaceContract, "OrderValidated") + .withArgs(orderHash, alice.address, constants.AddressZero); + + // OrderStatus is validated + orderStatus = await marketplaceContract.getOrderStatus(orderHash); + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(true, false, 0, 0) + ); + }); + + describe("Bob attempts to fill the order without paying Carol", () => { + let maliciousCallData: string; + + before(async () => { + // True Parameters + const basicOrderParameters = getBasicOrderParameters( + 2, // ERC20ForERC721 + order + ); + + basicOrderParameters.additionalRecipients = []; + basicOrderParameters.signature = basicOrderParameters.signature + .slice(0, 66) + .concat(hexZeroPad("0x", 96).slice(2)); + const scuffedContract = getScuffedContract(marketplaceContract); + const scuffed = scuffedContract.fulfillBasicOrder({ + parameters: basicOrderParameters, + }); + scuffed.parameters.signature.length.replace(100); + scuffed.parameters.signature.tail.replace(carol.address); + + maliciousCallData = scuffed.encode(); + }); + + if (!IS_FIXED) { + it("Bob fulfills Alice's order using maliciously constructed calldata", async () => { + await expect( + bob.sendTransaction({ + to: marketplaceContract.address, + data: maliciousCallData, + }) + ).to.emit(marketplaceContract, "OrderFulfilled"); + }); + + it("Bob receives Alice's NFT, having paid 1000 DAI", async () => { + expect(await testERC721.ownerOf(1)).to.equal(bob.address); + expect(await testERC20.balanceOf(bob.address)).to.equal(100); + }); + + it("Alice receives 1000 DAI", async () => { + expect(await testERC20.balanceOf(alice.address)).to.equal(1000); + }); + + it("Carol does not receive her DAI", async () => { + expect(await testERC20.balanceOf(carol.address)).to.equal(0); + }); + } else { + it("Bob attempts to fulfill Alice's order with malicious calldata, but the transaction reverts", async () => { + await expect( + bob.sendTransaction({ + to: marketplaceContract.address, + data: maliciousCallData, + }) + ).to.be.revertedWith("MissingOriginalConsiderationItems"); + }); + } + }); +}); diff --git a/test/findings/CriteriaResolverUnhashedLeaves.spec.ts b/test/findings/CriteriaResolverUnhashedLeaves.spec.ts new file mode 100644 index 000000000..cf0ecc633 --- /dev/null +++ b/test/findings/CriteriaResolverUnhashedLeaves.spec.ts @@ -0,0 +1,130 @@ +import { expect } from "chai"; +import { BigNumber, constants, Wallet } from "ethers"; +import { network } from "hardhat"; +import { + ConsiderationInterface, + TestERC20, + TestERC721, +} from "../../typechain-types"; +import { buildResolver, toBN, toKey } from "../utils/encoding"; +import { seaportFixture, SeaportFixtures } from "../utils/fixtures"; +import { getWalletWithEther } from "../utils/impersonate"; +import { AdvancedOrder } from "../utils/types"; +const { merkleTree } = require("../utils/criteria"); + +const IS_FIXED = true; + +describe("Criteria resolver allows root hash to be given as a leaf", async () => { + let alice: Wallet; + let bob: Wallet; + let carol: Wallet; + let order: AdvancedOrder; + let testERC20: TestERC20; + let testERC721: TestERC721; + let marketplaceContract: ConsiderationInterface; + let set721ApprovalForAll: SeaportFixtures["set721ApprovalForAll"]; + let mintAndApproveERC20: SeaportFixtures["mintAndApproveERC20"]; + let getTestItem20: SeaportFixtures["getTestItem20"]; + let getTestItem721WithCriteria: SeaportFixtures["getTestItem721WithCriteria"]; + let createOrder: SeaportFixtures["createOrder"]; + let mint721s: SeaportFixtures["mint721s"]; + let tokenIds: BigNumber[]; + let root: string; + + after(async () => { + await network.provider.request({ + method: "hardhat_reset", + }); + }); + + before(async function () { + if (process.env.REFERENCE) { + this.skip(); + } + alice = await getWalletWithEther(); + bob = await getWalletWithEther(); + carol = await getWalletWithEther(); + ({ + mintAndApproveERC20, + marketplaceContract, + getTestItem20, + getTestItem721WithCriteria, + set721ApprovalForAll, + createOrder, + testERC20, + testERC721, + mint721s, + } = await seaportFixture(await getWalletWithEther())); + await mintAndApproveERC20(alice, marketplaceContract.address, 1000); + await set721ApprovalForAll(bob, marketplaceContract.address); + await set721ApprovalForAll(carol, marketplaceContract.address); + tokenIds = await mint721s(bob, 10); + ({ root } = merkleTree(tokenIds)); + }); + + it("Alice makes an offer to buy any of 10 NFTs with a particular trait for 1000 DAI", async () => { + const offer = [getTestItem20(1000, 1000)]; + const consideration = [ + getTestItem721WithCriteria(root, 1, 1, alice.address), + ]; + + const results = await createOrder( + alice, + constants.AddressZero, // zone + offer, + consideration, + 1, // FULL_OPEN + [], // criteria + null, // timeFlag + alice, // signer + constants.HashZero, // zoneHash + constants.HashZero, // conduitKey + true // extraCheap + ); + order = results.order; + }); + + describe("Carol, the collection owner, attempts to fill Alice's order with an NFT outside of Alice's criteria", async () => { + it("Carol mints a new NFT with its identifier set to the merkle tree's root hash", async () => { + await testERC721.mint(carol.address, root); + expect(tokenIds.filter((id) => id.eq(toBN(root))).length).to.eq(0); + }); + + if (!IS_FIXED) { + it("Carol fills Alice's order, giving the merkle root as the token ID and an empty proof", async () => { + const criteriaResolver = buildResolver(0, 1, 0, toBN(root), []); + await marketplaceContract + .connect(carol) + .fulfillAdvancedOrder( + order, + [criteriaResolver], + toKey(false), + carol.address + ); + }); + + it("Carol receives 1000 DAI from Alice", async () => { + expect(await testERC20.balanceOf(alice.address)).to.eq(0); + expect(await testERC20.balanceOf(carol.address)).to.eq(1000); + }); + + it("Alice receives the merkle root identified token from Carol", async () => { + expect(await testERC721.ownerOf(root)).to.equal(alice.address); + }); + } else { + it("Carol's attempt to fill Alice's order with the merkle root as the token ID reverts", async () => { + const criteriaResolver = buildResolver(0, 1, 0, toBN(root), []); + await expect( + marketplaceContract + .connect(carol) + .fulfillAdvancedOrder( + order, + [criteriaResolver], + toKey(false), + carol.address + ) + ).to.be.revertedWith("InvalidProof"); + }); + } + }); +}); diff --git a/test/findings/FulfillmentOverflowWithMissingItems.spec.ts b/test/findings/FulfillmentOverflowWithMissingItems.spec.ts new file mode 100644 index 000000000..d009539a6 --- /dev/null +++ b/test/findings/FulfillmentOverflowWithMissingItems.spec.ts @@ -0,0 +1,139 @@ +import { expect } from "chai"; +import { constants, Wallet } from "ethers"; +import { network } from "hardhat"; +import { + ConsiderationInterface, + TestERC20, + TestERC721, +} from "../../typechain-types"; +import { toFulfillment } from "../utils/encoding"; +import { seaportFixture, SeaportFixtures } from "../utils/fixtures"; +import { getWalletWithEther } from "../utils/impersonate"; +import { AdvancedOrder, OfferItem } from "../utils/types"; + +const IS_FIXED = true; + +describe("Fulfillment applier allows overflow when a missing item is provided", async () => { + let alice: Wallet; + let bob: Wallet; + let order: AdvancedOrder; + let maliciousOrder: AdvancedOrder; + let testERC20: TestERC20; + let testERC721: TestERC721; + let marketplaceContract: ConsiderationInterface; + let mintAndApprove721: SeaportFixtures["mintAndApprove721"]; + let mintAndApproveERC20: SeaportFixtures["mintAndApproveERC20"]; + let getTestItem20: SeaportFixtures["getTestItem20"]; + let getTestItem721: SeaportFixtures["getTestItem721"]; + let createOrder: SeaportFixtures["createOrder"]; + + after(async () => { + await network.provider.request({ + method: "hardhat_reset", + }); + }); + + before(async function () { + if (process.env.REFERENCE) { + this.skip(); + } + alice = await getWalletWithEther(); + bob = await getWalletWithEther(); + ({ + mintAndApprove721, + mintAndApproveERC20, + marketplaceContract, + getTestItem20, + getTestItem721, + createOrder, + testERC20, + testERC721, + } = await seaportFixture(await getWalletWithEther())); + // ERC721 with ID = 1 + await mintAndApprove721(alice, marketplaceContract.address, 1); + // ERC20 with amount = 1100 + await mintAndApproveERC20(bob, marketplaceContract.address, 1); + }); + + it("Alice offers to sell an NFT for 1000 DAI", async () => { + const offer = [getTestItem721(1, 1, 1)]; + const consideration = [getTestItem20(1000, 1000, alice.address)]; + + const results = await createOrder( + alice, + constants.AddressZero, // zone + offer, + consideration, + 0, // FULL_OPEN + [], // criteria + null, // timeFlag + alice, // signer + constants.HashZero, // zoneHash + constants.HashZero, // conduitKey + true // extraCheap + ); + order = results.order; + }); + + it("Bob constructs a malicious order with one empty consideration and one which will overflow Alice's", async () => { + const offer: OfferItem[] = [getTestItem20(1, 1)]; + const consideration = [ + getTestItem721(1, 1, 1, bob.address), + getTestItem20(0, 0, alice.address), + getTestItem20( + constants.MaxUint256.sub(998), + constants.MaxUint256.sub(998), + alice.address + ), + ]; + const results = await createOrder( + bob, + constants.AddressZero, + offer, + consideration, + 1 + ); + maliciousOrder = results.order; + }); + + describe("Bob attempts to match Alice's order with his malicious order", () => { + const fulfillments = [ + toFulfillment([[0, 0]], [[1, 0]]), + toFulfillment( + [[1, 0]], + [ + [0, 0], + [1, 1], + [1, 2], + ] + ), + ]; + + if (!IS_FIXED) { + it("Bob is able to match Alice's order with his malicious one", async () => { + await marketplaceContract + .connect(bob) + .matchAdvancedOrders([order, maliciousOrder], [], fulfillments); + }); + + it("Bob receives Alice's NFT, having paid 1 DAI", async () => { + expect(await testERC721.ownerOf(1)).to.equal(bob.address); + expect(await testERC20.balanceOf(bob.address)).to.equal(0); + }); + + it("Alice receives 1 DAI", async () => { + expect(await testERC20.balanceOf(alice.address)).to.equal(1); + }); + } else { + it("The transaction reverts", async () => { + await expect( + marketplaceContract + .connect(bob) + .matchAdvancedOrders([order, maliciousOrder], [], fulfillments) + ).to.be.revertedWith( + "panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)" + ); + }); + } + }); +}); diff --git a/test/findings/PartialFillFractionOverflow.spec.ts b/test/findings/PartialFillFractionOverflow.spec.ts new file mode 100644 index 000000000..5da4d3785 --- /dev/null +++ b/test/findings/PartialFillFractionOverflow.spec.ts @@ -0,0 +1,180 @@ +import { expect } from "chai"; +import { constants, Wallet } from "ethers"; +import { network } from "hardhat"; +import { + ConsiderationInterface, + TestERC1155, + TestERC20, +} from "../../typechain-types"; +import { buildOrderStatus, toBN, toKey } from "../utils/encoding"; +import { seaportFixture, SeaportFixtures } from "../utils/fixtures"; +import { getWalletWithEther } from "../utils/impersonate"; +import { AdvancedOrder, ConsiderationItem } from "../utils/types"; + +const IS_FIXED = true; + +describe("Partial fill fractions can overflow to reset an order", async () => { + let alice: Wallet; + let bob: Wallet; + let carol: Wallet; + let order: AdvancedOrder; + let orderHash: string; + let testERC20: TestERC20; + let testERC1155: TestERC1155; + let marketplaceContract: ConsiderationInterface; + let mintAndApprove1155: SeaportFixtures["mintAndApprove1155"]; + let mintAndApproveERC20: SeaportFixtures["mintAndApproveERC20"]; + let getTestItem20: SeaportFixtures["getTestItem20"]; + let getTestItem1155: SeaportFixtures["getTestItem1155"]; + let createOrder: SeaportFixtures["createOrder"]; + + after(async () => { + await network.provider.request({ + method: "hardhat_reset", + }); + }); + + before(async function () { + if (process.env.REFERENCE) { + this.skip(); + } + alice = await getWalletWithEther(); + bob = await getWalletWithEther(); + carol = await getWalletWithEther(); + ({ + mintAndApprove1155, + mintAndApproveERC20, + marketplaceContract, + getTestItem20, + getTestItem1155, + createOrder, + testERC20, + testERC1155, + } = await seaportFixture(await getWalletWithEther())); + await mintAndApprove1155(alice, marketplaceContract.address, 1, 1, 10); + await mintAndApproveERC20(bob, marketplaceContract.address, 500); + await mintAndApproveERC20(carol, marketplaceContract.address, 4500); + }); + + it("Alice has ten 1155 tokens she has approved Seaport to spend", async () => { + expect(await testERC1155.balanceOf(alice.address, 1)).to.eq(10); + }); + + it("Alice creates a partially fillable order to sell two 1155 tokens for 1000 DAI", async () => { + const offer = [getTestItem1155(1, 2, 2)]; + + const consideration: ConsiderationItem[] = [ + getTestItem20(1000, 1000, alice.address), + ]; + + const results = await createOrder( + alice, + constants.AddressZero, // zone + offer, + consideration, + 1, // PARTIAL_OPEN + [], // criteria + null, // timeFlag + alice, // signer + constants.HashZero, // zoneHash + constants.HashZero, // conduitKey + true // extraCheap + ); + order = results.order; + orderHash = results.orderHash; + + // OrderStatus is not validated + let orderStatus = await marketplaceContract.getOrderStatus(orderHash); + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(false, false, 0, 0) + ); + + // Bob validates the order + await expect(marketplaceContract.connect(bob).validate([order])) + .to.emit(marketplaceContract, "OrderValidated") + .withArgs(orderHash, alice.address, constants.AddressZero); + + // OrderStatus is validated + orderStatus = await marketplaceContract.getOrderStatus(orderHash); + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(true, false, 0, 0) + ); + }); + + describe("Bob partially fills 1/2 of Alice's order", () => { + it("Bob receives one 1155 token", async () => { + order.numerator = 1; + order.denominator = 2; + await marketplaceContract + .connect(bob) + .fulfillAdvancedOrder(order, [], toKey(false), bob.address); + expect(await testERC1155.balanceOf(bob.address, 1)).to.eq(1); + }); + + it("Alice receives 500 DAI", async () => { + expect(await testERC20.balanceOf(alice.address)).to.eq(500); + }); + }); + + describe("Carol attempts to fill the order multiple times", () => { + it("Carol fills the order with a fraction that overflows", async () => { + order.numerator = toBN(2).pow(118); + order.denominator = toBN(2).pow(119); + await marketplaceContract + .connect(carol) + .fulfillAdvancedOrder(order, [], toKey(false), carol.address); + }); + + it("Carol receives one 1155 token from Alice", async () => { + expect(await testERC1155.balanceOf(carol.address, 1)).to.eq(1); + expect(await testERC1155.balanceOf(alice.address, 1)).to.eq(8); + }); + + it("Carol pays Alice 500 DAI", async () => { + expect(await testERC20.balanceOf(carol.address)).to.eq(4000); + expect(await testERC20.balanceOf(alice.address)).to.eq(1000); + }); + + if (!IS_FIXED) { + it("Alice's order is reset and marked as 0% filled", async () => { + const orderStatus = await marketplaceContract.getOrderStatus(orderHash); + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(true, false, toBN(0), toBN(0)) + ); + }); + + it("Carol fills the same order multiple times", async () => { + for (let i = 0; i < 4; i++) { + order.numerator = toBN(2).pow(1); + order.denominator = toBN(2).pow(2); + await marketplaceContract + .connect(carol) + .fulfillAdvancedOrder(order, [], toKey(false), carol.address); + order.numerator = toBN(2).pow(118); + order.denominator = toBN(2).pow(119); + await marketplaceContract + .connect(carol) + .fulfillAdvancedOrder(order, [], toKey(false), carol.address); + } + }); + + it("Carol receives Alice's remaining eight 1155 tokens", async () => { + expect(await testERC1155.balanceOf(carol.address, 1)).to.eq(9); + expect(await testERC1155.balanceOf(alice.address, 1)).to.eq(0); + }); + + it("Alice receives 4000 DAI", async () => { + expect(await testERC20.balanceOf(carol.address)).to.eq(0); + expect(await testERC20.balanceOf(alice.address)).to.eq(5000); + }); + } else { + it("Alice's order is marked as completely filled", async () => { + const orderStatus = await marketplaceContract.getOrderStatus(orderHash); + + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(true, false, toBN(2), toBN(2)) + ); + }); + } + }); +}); diff --git a/test/foundry/CeilEquivalenceTest.t.sol b/test/foundry/CeilEquivalenceTest.t.sol new file mode 100644 index 000000000..168233ac6 --- /dev/null +++ b/test/foundry/CeilEquivalenceTest.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.13; + +contract CeilEquivalenceTest { + function testCeilEquivalence(uint256 numerator, uint256 denominator) + public + pure + { + // There is intermediate overflow for the unoptimized ceil + // but for the sake of this test we'll ignore those cases. + numerator %= type(uint128).max; + denominator %= type(uint128).max; + denominator++; // Ignore zero. + + uint256 optimized; + assembly { + optimized := mul( + add(div(sub(numerator, 1), denominator), 1), + iszero(iszero(numerator)) + ) + } + + uint256 unoptimized; + assembly { + unoptimized := div(add(numerator, sub(denominator, 1)), denominator) + } + + assert(optimized == unoptimized); + } +} diff --git a/test/foundry/FulfillAdvancedOrder.t.sol b/test/foundry/FulfillAdvancedOrder.t.sol index 143318053..d606d6f46 100644 --- a/test/foundry/FulfillAdvancedOrder.t.sol +++ b/test/foundry/FulfillAdvancedOrder.t.sol @@ -1,21 +1,23 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; -import { OrderType, BasicOrderType, ItemType, Side } from "../../contracts/lib/ConsiderationEnums.sol"; -import { AdditionalRecipient } from "../../contracts/lib/ConsiderationStructs.sol"; +import { OneWord } from "../../contracts/lib/ConsiderationConstants.sol"; +import { OrderType, ItemType } from "../../contracts/lib/ConsiderationEnums.sol"; import { ConsiderationInterface } from "../../contracts/interfaces/ConsiderationInterface.sol"; -import { AdvancedOrder, OfferItem, OrderParameters, ConsiderationItem, OrderComponents, BasicOrderParameters, CriteriaResolver } from "../../contracts/lib/ConsiderationStructs.sol"; +import { AdvancedOrder, OrderParameters, OrderComponents, CriteriaResolver } from "../../contracts/lib/ConsiderationStructs.sol"; import { BaseOrderTest } from "./utils/BaseOrderTest.sol"; -import { TestERC721 } from "../../contracts/test/TestERC721.sol"; -import { TestERC1155 } from "../../contracts/test/TestERC1155.sol"; -import { TestERC20 } from "../../contracts/test/TestERC20.sol"; -import { ProxyRegistry } from "./interfaces/ProxyRegistry.sol"; -import { OwnableDelegateProxy } from "./interfaces/OwnableDelegateProxy.sol"; -import { Merkle } from "murky/Merkle.sol"; +import { ERC1155Recipient } from "./utils/ERC1155Recipient.sol"; +import { ConsiderationEventsAndErrors } from "../../contracts/interfaces/ConsiderationEventsAndErrors.sol"; +import { ArithmeticUtil } from "./utils/ArithmeticUtil.sol"; contract FulfillAdvancedOrder is BaseOrderTest { - // todo: add numer/denom, swap if numer > denom + using ArithmeticUtil for uint256; + using ArithmeticUtil for uint128; + using ArithmeticUtil for uint120; + using ArithmeticUtil for uint8; + + FuzzInputs empty; struct FuzzInputs { uint256 tokenId; address zone; @@ -23,6 +25,7 @@ contract FulfillAdvancedOrder is BaseOrderTest { uint256 salt; uint16 offerAmt; // uint16 fulfillAmt; + address recipient; uint120[3] paymentAmounts; bool useConduit; uint8 numer; @@ -32,130 +35,372 @@ contract FulfillAdvancedOrder is BaseOrderTest { struct Context { ConsiderationInterface consideration; FuzzInputs args; + uint256 tokenAmount; + uint256 warpAmount; } modifier validateInputs(FuzzInputs memory args) { + vm.assume(args.offerAmt > 0); vm.assume( args.paymentAmounts[0] > 0 && args.paymentAmounts[1] > 0 && args.paymentAmounts[2] > 0 ); vm.assume( - uint256(args.paymentAmounts[0]) + - uint256(args.paymentAmounts[1]) + - uint256(args.paymentAmounts[2]) <= - 2**120 - 1 + args.paymentAmounts[0].add(args.paymentAmounts[1]).add( + args.paymentAmounts[2] + ) <= 2**120 - 1 ); - vm.assume(args.offerAmt > 0); - vm.assume(args.numer <= args.denom); - vm.assume(args.numer > 0); _; } - function testAdvancedPartial1155(FuzzInputs memory args) public { - _advancedPartial1155(Context(consideration, args)); - _advancedPartial1155(Context(referenceConsideration, args)); + modifier validateNumerDenom(FuzzInputs memory args) { + vm.assume(args.numer > 0 && args.denom > 0); + if (args.numer > args.denom) { + uint8 temp = args.denom; + args.denom = args.numer; + args.numer = temp; + } + vm.assume( + args.paymentAmounts[0] > 0 && + args.paymentAmounts[1] > 0 && + args.paymentAmounts[2] > 0 + ); + vm.assume( + args.paymentAmounts[0].add(args.paymentAmounts[1]).add( + args.paymentAmounts[2] + ) <= 2**120 - 1 + ); + _; } - function testAdvancedPartial1155Static() public { - test1155_1.mint(alice, 1, 10); + function test(function(Context memory) external fn, Context memory context) + internal + { + try fn(context) {} catch (bytes memory reason) { + assertPass(reason); + } + } - _configureERC1155OfferItem(1, uint256(10)); - _configureEthConsiderationItem(alice, uint256(10)); - _configureEthConsiderationItem(payable(address(0)), uint256(10)); - _configureEthConsiderationItem(cal, uint256(10)); - uint256 nonce = referenceConsideration.getNonce(alice); - OrderComponents memory orderComponents = OrderComponents( - alice, - address(0), - offerItems, - considerationItems, - OrderType.PARTIAL_OPEN, - block.timestamp, - block.timestamp + 1, - bytes32(0), - 0, - bytes32(0), - nonce + function testNoNativeOffersFulfillAdvanced(uint8[8] memory itemTypes) + public + { + uint256 tokenId; + for (uint256 i; i < 8; i++) { + ItemType itemType = ItemType(itemTypes[i] % 4); + if (itemType == ItemType.NATIVE) { + addEthOfferItem(1); + } else if (itemType == ItemType.ERC20) { + addErc20OfferItem(1); + } else if (itemType == ItemType.ERC1155) { + test1155_1.mint(alice, tokenId, 1); + addErc1155OfferItem(tokenId, 1); + } else { + test721_1.mint(alice, tokenId); + addErc721OfferItem(tokenId); + } + tokenId++; + } + addEthOfferItem(1); + + addEthConsiderationItem(alice, 1); + + test( + this.noNativeOfferItemsFulfillAdvanced, + Context(consideration, empty, 0, 0) ); - bytes32 orderHash = referenceConsideration.getOrderHash( - orderComponents + test( + this.noNativeOfferItemsFulfillAdvanced, + Context(referenceConsideration, empty, 0, 0) ); + } + function noNativeOfferItemsFulfillAdvanced(Context memory context) + external + stateless + { + configureOrderParameters(alice); + uint256 counter = context.consideration.getCounter(alice); + _configureOrderComponents(counter); + bytes32 orderHash = context.consideration.getOrderHash( + baseOrderComponents + ); bytes memory signature = signOrder( - referenceConsideration, + context.consideration, alicePk, orderHash ); - OrderParameters memory orderParameters = OrderParameters( + vm.expectRevert(abi.encodeWithSignature("InvalidNativeOfferItem()")); + context.consideration.fulfillAdvancedOrder( + AdvancedOrder(baseOrderParameters, 1, 1, signature, ""), + new CriteriaResolver[](0), + bytes32(0), + address(0) + ); + } + + function testAdvancedPartialAscendingOfferAmount1155( + FuzzInputs memory args, + uint128 tokenAmount, + uint256 warpAmount + ) public validateInputs(args) { + vm.assume(tokenAmount > 0); + + test( + this.advancedPartialAscendingOfferAmount1155, + Context( + referenceConsideration, + args, + tokenAmount, + warpAmount % 1000 + ) + ); + test( + this.advancedPartialAscendingOfferAmount1155, + Context(consideration, args, tokenAmount, warpAmount % 1000) + ); + } + + function advancedPartialAscendingOfferAmount1155(Context memory context) + external + stateless + { + uint256 sumOfPaymentAmounts = (context.args.paymentAmounts[0].mul(2)) + .add(context.args.paymentAmounts[1].mul(2)) + .add(context.args.paymentAmounts[2].mul(2)); + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test1155_1.mint( alice, - address(0), + context.args.tokenId, + context.tokenAmount.mul(4) + ); + + addOfferItem( + ItemType.ERC1155, + context.args.tokenId, + context.tokenAmount.mul(2), + context.tokenAmount.mul(4) + ); + // set endAmount to 2 * startAmount + addEthConsiderationItem(alice, context.args.paymentAmounts[0].mul(2)); + addEthConsiderationItem(alice, context.args.paymentAmounts[1].mul(2)); + addEthConsiderationItem(alice, context.args.paymentAmounts[2].mul(2)); + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, offerItems, considerationItems, OrderType.PARTIAL_OPEN, block.timestamp, - block.timestamp + 1, - bytes32(0), - 0, - bytes32(0), - 3 + block.timestamp + 1000, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length ); - uint256 value = 30; - referenceConsideration.fulfillAdvancedOrder{ value: value }( - AdvancedOrder(orderParameters, 1, 1, signature, ""), - new CriteriaResolver[](0), - bytes32(0) + OrderComponents memory orderComponents = getOrderComponents( + orderParameters, + context.consideration.getCounter(alice) ); + + bytes32 orderHash = context.consideration.getOrderHash(orderComponents); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + delete offerItems; + delete considerationItems; + + AdvancedOrder memory advancedOrder = AdvancedOrder( + orderParameters, + 1, + 2, + signature, + "" + ); + + uint256 startTime = block.timestamp; + vm.warp(block.timestamp + context.warpAmount); + // calculate current amount of order based on warpAmount, round down since it's an offer + // and divide by two to fulfill half of the order + uint256 currentAmount = _locateCurrentAmount( + context.tokenAmount * 2, + context.tokenAmount * 4, + startTime, + startTime + 1000, + false // don't round up offers + ) / 2; + // set transaction value to sum of eth consideration items (including endAmount of considerationItem[0]) + vm.expectEmit(false, true, true, true, address(test1155_1)); + emit TransferSingle( + address(0), + alice, + address(this), + context.args.tokenId, + currentAmount + ); + context.consideration.fulfillAdvancedOrder{ + value: sumOfPaymentAmounts + }(advancedOrder, new CriteriaResolver[](0), bytes32(0), address(0)); + (, , uint256 totalFilled, uint256 totalSize) = context + .consideration + .getOrderStatus(orderHash); + assertEq(totalFilled, 1); + assertEq(totalSize, 2); } - function _advancedPartial1155(Context memory context) - internal - validateInputs(context.args) - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - // vm.assume(context.args.fulfillAmt > 0); - // vm.assume( - // context.args.offerAmt > context.args.fulfillAmt - // ); - // todo: swap fulfillment and tokenAmount if we exceed global rejects with above assume - // if (context.args.offerAmt < context.args.fulfillAmt) { - // uint256 temp = context.args.fulfillAmt; - // context.args.fulfillAmt = context.args.offerAmt; - // context.args.offerAmt = temp; - // } + function testAdvancedPartialAscendingConsiderationAmount1155( + FuzzInputs memory inputs, + uint128 tokenAmount + ) public validateInputs(inputs) { + vm.assume(tokenAmount > 0); + test( + this.advancedPartialAscendingConsiderationAmount1155, + Context(referenceConsideration, inputs, tokenAmount, 0) + ); + test( + this.advancedPartialAscendingConsiderationAmount1155, + Context(consideration, inputs, tokenAmount, 0) + ); + } + function advancedPartialAscendingConsiderationAmount1155( + Context memory context + ) external stateless { bytes32 conduitKey = context.args.useConduit ? conduitKeyOne : bytes32(0); - // mint offerAmt tokens test1155_1.mint( alice, context.args.tokenId, - context.args.denom // mint 256x as many + context.tokenAmount.mul(1000) ); - _configureERC1155OfferItem( + addOfferItem( + ItemType.ERC1155, context.args.tokenId, - uint256(context.args.denom) + context.tokenAmount.mul(1000) ); - _configureEthConsiderationItem( + // set endAmount to 2 * startAmount + addEthConsiderationItem( alice, - context.args.paymentAmounts[0] * uint256(context.args.denom) + context.args.paymentAmounts[0].mul(2), + context.args.paymentAmounts[0].mul(4) ); - _configureEthConsiderationItem( - payable(context.args.zone), - context.args.paymentAmounts[1] * uint256(context.args.denom) + addEthConsiderationItem(alice, context.args.paymentAmounts[1].mul(2)); + addEthConsiderationItem(alice, context.args.paymentAmounts[2].mul(2)); + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.PARTIAL_OPEN, + block.timestamp, + block.timestamp + 1000, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length + ); + + OrderComponents memory orderComponents = getOrderComponents( + orderParameters, + context.consideration.getCounter(alice) + ); + + bytes32 orderHash = context.consideration.getOrderHash(orderComponents); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + delete offerItems; + delete considerationItems; + + AdvancedOrder memory advancedOrder = AdvancedOrder( + orderParameters, + 1, + 2, + signature, + "" ); - _configureEthConsiderationItem( - cal, - context.args.paymentAmounts[2] * uint256(context.args.denom) + + // set blockTimestamp to right before endTime and set insufficient value for transaction + vm.warp(block.timestamp + 999); + vm.expectRevert( + ConsiderationEventsAndErrors.InsufficientEtherSupplied.selector ); + context.consideration.fulfillAdvancedOrder{ + value: context + .args + .paymentAmounts[0] + .add(context.args.paymentAmounts[1]) + .add(context.args.paymentAmounts[2]) + }(advancedOrder, new CriteriaResolver[](0), bytes32(0), address(0)); + + uint256 sumOfPaymentAmounts = (context.args.paymentAmounts[0].mul(4)) + .add((context.args.paymentAmounts[1].mul(2))) + .add((context.args.paymentAmounts[2].mul(2))); + + // set transaction value to sum of eth consideration items (including endAmount of considerationItem[0]) + context.consideration.fulfillAdvancedOrder{ + value: sumOfPaymentAmounts + }(advancedOrder, new CriteriaResolver[](0), bytes32(0), address(0)); + + (, , uint256 totalFilled, uint256 totalSize) = context + .consideration + .getOrderStatus(orderHash); + assertEq(totalFilled, 1); + assertEq(totalSize, 2); + } + + function testSingleAdvanced1155( + FuzzInputs memory inputs, + uint128 tokenAmount + ) + public + validateInputs(inputs) + validateNumerDenom(inputs) + onlyPayable(inputs.zone) + only1155Receiver(inputs.recipient) + { + vm.assume(tokenAmount > 0); + + test( + this.singleAdvanced1155, + Context(consideration, inputs, tokenAmount, 0) + ); + test( + this.singleAdvanced1155, + Context(referenceConsideration, inputs, tokenAmount, 0) + ); + } + function singleAdvanced1155(Context memory context) external stateless { + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test1155_1.mint(alice, context.args.tokenId, context.tokenAmount); + + addErc1155OfferItem(context.args.tokenId, context.tokenAmount); + addEthConsiderationItem(payable(0), 10); + addEthConsiderationItem(alice, 10); + addEthConsiderationItem(bob, 10); + uint256 counter = referenceConsideration.getCounter(alice); OrderComponents memory orderComponents = OrderComponents( alice, context.args.zone, @@ -167,7 +412,66 @@ contract FulfillAdvancedOrder is BaseOrderTest { context.args.zoneHash, context.args.salt, conduitKey, - context.consideration.getNonce(alice) + counter + ); + bytes32 orderHash = consideration.getOrderHash(orderComponents); + + bytes memory signature = signOrder(consideration, alicePk, orderHash); + + OrderParameters memory orderParameters = OrderParameters( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.PARTIAL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + 3 + ); + uint256 value = 30; + + consideration.fulfillAdvancedOrder{ value: value }( + AdvancedOrder(orderParameters, 1, 1, signature, ""), + new CriteriaResolver[](0), + bytes32(0), + context.args.recipient + ); + + assertEq( + context.tokenAmount, + test1155_1.balanceOf(context.args.recipient, context.args.tokenId) + ); + } + + function testPartialFulfillEthTo1155DenominatorOverflow() public { + test( + this.partialFulfillEthTo1155DenominatorOverflow, + Context(consideration, empty, 0, 0) + ); + test( + this.partialFulfillEthTo1155DenominatorOverflow, + Context(referenceConsideration, empty, 0, 0) + ); + } + + function partialFulfillEthTo1155DenominatorOverflow(Context memory context) + external + stateless + { + // mint 100 tokens + test1155_1.mint(alice, 1, 100); + + addErc1155OfferItem(1, 100); + addEthConsiderationItem(alice, 100); + + _configureOrderParameters(alice, address(0), bytes32(0), 0, false); + baseOrderParameters.orderType = OrderType.PARTIAL_OPEN; + OrderComponents memory orderComponents = getOrderComponents( + baseOrderParameters, + context.consideration.getCounter(alice) ); bytes32 orderHash = context.consideration.getOrderHash(orderComponents); @@ -190,39 +494,24 @@ contract FulfillAdvancedOrder is BaseOrderTest { assertEq(totalSize, 0); } - OrderParameters memory orderParameters = OrderParameters( - alice, - context.args.zone, - offerItems, - considerationItems, - OrderType.PARTIAL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - considerationItems.length + // Create an order to fulfill half of the original offer. + context.consideration.fulfillAdvancedOrder{ value: 50 }( + AdvancedOrder(baseOrderParameters, 2**118, 2**119, signature, ""), + new CriteriaResolver[](0), + bytes32(0), + address(0) ); - uint256 value = uint128(context.args.numer) * - (uint128(context.args.paymentAmounts[0]) + - context.args.paymentAmounts[1] + - context.args.paymentAmounts[2]); - emit log_named_uint("numer", context.args.numer); - emit log_named_uint("denom", context.args.denom); - emit log_named_uint("value", value); - // uint120 numer = uint120(context.args.offerAmt) * 2; - context.consideration.fulfillAdvancedOrder{ value: value }( - AdvancedOrder( - orderParameters, - context.args.numer, - context.args.denom, - signature, - "" - ), + + // Create a second order to fulfill one-tenth of the original offer. + // The denominator will overflow when combined with that of the first order. + context.consideration.fulfillAdvancedOrder{ value: 10 }( + AdvancedOrder(baseOrderParameters, 1, 10, signature, ""), new CriteriaResolver[](0), - conduitKey + bytes32(0), + address(0) ); + // Assert six-tenths of the order has been fulfilled. { ( bool isValidated, @@ -232,8 +521,471 @@ contract FulfillAdvancedOrder is BaseOrderTest { ) = context.consideration.getOrderStatus(orderHash); assertTrue(isValidated); assertFalse(isCancelled); - assertEq(totalFilled, context.args.numer); - assertEq(totalSize, context.args.denom); + assertEq(totalFilled, 6); + + assertEq(totalSize, 10); + assertEq(60, test1155_1.balanceOf(address(this), 1)); } } + + function testPartialFulfillEthTo1155DenominatorOverflowToZero() public { + test( + this.partialFulfillEthTo1155DenominatorOverflowToZero, + Context(consideration, empty, 0, 0) + ); + test( + this.partialFulfillEthTo1155DenominatorOverflowToZero, + Context(referenceConsideration, empty, 0, 0) + ); + } + + function partialFulfillEthTo1155DenominatorOverflowToZero( + Context memory context + ) external stateless { + // mint 100 tokens + test1155_1.mint(alice, 1, 100); + + addErc1155OfferItem(1, 100); + addEthConsiderationItem(alice, 100); + + _configureOrderParameters(alice, address(0), bytes32(0), 0, false); + baseOrderParameters.orderType = OrderType.PARTIAL_OPEN; + OrderComponents memory orderComponents = getOrderComponents( + baseOrderParameters, + context.consideration.getCounter(alice) + ); + bytes32 orderHash = context.consideration.getOrderHash(orderComponents); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + { + ( + bool isValidated, + bool isCancelled, + uint256 totalFilled, + uint256 totalSize + ) = context.consideration.getOrderStatus(orderHash); + assertFalse(isValidated); + assertFalse(isCancelled); + assertEq(totalFilled, 0); + assertEq(totalSize, 0); + } + + AdvancedOrder memory advancedOrder = AdvancedOrder( + baseOrderParameters, + 2**119, + 2**119, + signature, + "" + ); + + // set denominator to 2 ** 120 + assembly { + mstore(add(0x40, advancedOrder), shl(120, 1)) + } + + bytes4 fulfillAdvancedOrderSelector = consideration + .fulfillAdvancedOrder + .selector; + bytes memory fulfillAdvancedOrderCalldata = abi.encodeWithSelector( + fulfillAdvancedOrderSelector, + advancedOrder, + new CriteriaResolver[](0), + bytes32(0), + address(0) + ); + + address considerationAddress = address(consideration); + uint256 calldataLength = fulfillAdvancedOrderCalldata.length; + bool success; + + assembly { + // Call fulfillBasicOrders + success := call( + gas(), + considerationAddress, + 50, + // The fn signature and calldata starts after the + // first OneWord bytes, as those initial bytes just + // contain the length of fulfillAdvancedOrderCalldata + add(fulfillAdvancedOrderCalldata, OneWord), + calldataLength, + // Store output at empty storage location, + // identified using "free memory pointer". + mload(0x40), + OneWord + ) + } + vm.expectRevert(abi.encodeWithSignature("BadFraction()")); + } + + function testPartialFulfillEthTo1155NumeratorOverflowToZero() public { + test( + this.partialFulfillEthTo1155NumeratorOverflowToZero, + Context(consideration, empty, 0, 0) + ); + test( + this.partialFulfillEthTo1155NumeratorOverflowToZero, + Context(referenceConsideration, empty, 0, 0) + ); + } + + function partialFulfillEthTo1155NumeratorOverflowToZero( + Context memory context + ) external stateless { + // mint 100 tokens + test1155_1.mint(alice, 1, 100); + + addErc1155OfferItem(1, 100); + addEthConsiderationItem(alice, 100); + + _configureOrderParameters(alice, address(0), bytes32(0), 0, false); + baseOrderParameters.orderType = OrderType.PARTIAL_OPEN; + OrderComponents memory orderComponents = getOrderComponents( + baseOrderParameters, + context.consideration.getCounter(alice) + ); + bytes32 orderHash = context.consideration.getOrderHash(orderComponents); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + { + ( + bool isValidated, + bool isCancelled, + uint256 totalFilled, + uint256 totalSize + ) = context.consideration.getOrderStatus(orderHash); + assertFalse(isValidated); + assertFalse(isCancelled); + assertEq(totalFilled, 0); + assertEq(totalSize, 0); + } + + AdvancedOrder memory advancedOrder = AdvancedOrder( + baseOrderParameters, + 2**119, + 2**119, + signature, + "" + ); + + // set numerator to 2 ** 120 + assembly { + mstore(add(0x20, advancedOrder), shl(120, 1)) + } + + bytes4 fulfillAdvancedOrderSelector = consideration + .fulfillAdvancedOrder + .selector; + bytes memory fulfillAdvancedOrderCalldata = abi.encodeWithSelector( + fulfillAdvancedOrderSelector, + advancedOrder, + new CriteriaResolver[](0), + bytes32(0), + address(0) + ); + + address considerationAddress = address(consideration); + uint256 calldataLength = fulfillAdvancedOrderCalldata.length; + bool success; + + assembly { + // Call fulfillBasicOrders + success := call( + gas(), + considerationAddress, + 50, + // The fn signature and calldata starts after the + // first OneWord bytes, as those initial bytes just + // contain the length of fulfillAdvancedOrderCalldata + add(fulfillAdvancedOrderCalldata, OneWord), + calldataLength, + // Store output at empty storage location, + // identified using "free memory pointer". + mload(0x40), + OneWord + ) + } + vm.expectRevert(abi.encodeWithSignature("BadFraction()")); + } + + function testPartialFulfillEthTo1155NumeratorDenominatorOverflowToZero() + public + { + test( + this.partialFulfillEthTo1155NumeratorDenominatorOverflowToZero, + Context(consideration, empty, 0, 0) + ); + test( + this.partialFulfillEthTo1155NumeratorDenominatorOverflowToZero, + Context(referenceConsideration, empty, 0, 0) + ); + } + + function partialFulfillEthTo1155NumeratorDenominatorOverflowToZero( + Context memory context + ) external stateless { + // mint 100 tokens + test1155_1.mint(alice, 1, 100); + + addErc1155OfferItem(1, 100); + addEthConsiderationItem(alice, 100); + + _configureOrderParameters(alice, address(0), bytes32(0), 0, false); + baseOrderParameters.orderType = OrderType.PARTIAL_OPEN; + OrderComponents memory orderComponents = getOrderComponents( + baseOrderParameters, + context.consideration.getCounter(alice) + ); + bytes32 orderHash = context.consideration.getOrderHash(orderComponents); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + { + ( + bool isValidated, + bool isCancelled, + uint256 totalFilled, + uint256 totalSize + ) = context.consideration.getOrderStatus(orderHash); + assertFalse(isValidated); + assertFalse(isCancelled); + assertEq(totalFilled, 0); + assertEq(totalSize, 0); + } + + AdvancedOrder memory advancedOrder = AdvancedOrder( + baseOrderParameters, + 2**119, + 2**119, + signature, + "" + ); + + // set both numerator and denominator to 2 ** 120 + assembly { + mstore(add(0x20, advancedOrder), shl(120, 1)) + mstore(add(0x40, advancedOrder), shl(120, 1)) + } + + bytes4 fulfillAdvancedOrderSelector = consideration + .fulfillAdvancedOrder + .selector; + bytes memory fulfillAdvancedOrderCalldata = abi.encodeWithSelector( + fulfillAdvancedOrderSelector, + advancedOrder, + new CriteriaResolver[](0), + bytes32(0), + address(0) + ); + + address considerationAddress = address(consideration); + uint256 calldataLength = fulfillAdvancedOrderCalldata.length; + bool success; + + assembly { + // Call fulfillBasicOrders + success := call( + gas(), + considerationAddress, + 50, + // The fn signature and calldata starts after the + // first OneWord bytes, as those initial bytes just + // contain the length of fulfillAdvancedOrderCalldata + add(fulfillAdvancedOrderCalldata, OneWord), + calldataLength, + // Store output at empty storage location, + // identified using "free memory pointer". + mload(0x40), + OneWord + ) + } + vm.expectRevert(abi.encodeWithSignature("BadFraction()")); + } + + function testPartialFulfillEthTo1155NumeratorSetToZero() public { + test( + this.partialFulfillEthTo1155NumeratorSetToZero, + Context(consideration, empty, 0, 0) + ); + test( + this.partialFulfillEthTo1155NumeratorSetToZero, + Context(referenceConsideration, empty, 0, 0) + ); + } + + function partialFulfillEthTo1155NumeratorSetToZero(Context memory context) + external + stateless + { + // mint 100 tokens + test1155_1.mint(alice, 1, 100); + + addErc1155OfferItem(1, 100); + addEthConsiderationItem(alice, 100); + + _configureOrderParameters(alice, address(0), bytes32(0), 0, false); + baseOrderParameters.orderType = OrderType.PARTIAL_OPEN; + OrderComponents memory orderComponents = getOrderComponents( + baseOrderParameters, + context.consideration.getCounter(alice) + ); + bytes32 orderHash = context.consideration.getOrderHash(orderComponents); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + { + ( + bool isValidated, + bool isCancelled, + uint256 totalFilled, + uint256 totalSize + ) = context.consideration.getOrderStatus(orderHash); + assertFalse(isValidated); + assertFalse(isCancelled); + assertEq(totalFilled, 0); + assertEq(totalSize, 0); + } + + vm.expectRevert(abi.encodeWithSignature("BadFraction()")); + // Call fulfillAdvancedOrder with an order with a numerator of 0. + context.consideration.fulfillAdvancedOrder{ value: 50 }( + AdvancedOrder(baseOrderParameters, 0, 2, signature, ""), + new CriteriaResolver[](0), + bytes32(0), + address(0) + ); + } + + function testPartialFulfillEthTo1155DenominatorSetToZero() public { + test( + this.partialFulfillEthTo1155DenominatorSetToZero, + Context(consideration, empty, 0, 0) + ); + test( + this.partialFulfillEthTo1155DenominatorSetToZero, + Context(referenceConsideration, empty, 0, 0) + ); + } + + function partialFulfillEthTo1155DenominatorSetToZero(Context memory context) + external + stateless + { + // mint 100 tokens + test1155_1.mint(alice, 1, 100); + + addErc1155OfferItem(1, 100); + addEthConsiderationItem(alice, 100); + + _configureOrderParameters(alice, address(0), bytes32(0), 0, false); + baseOrderParameters.orderType = OrderType.PARTIAL_OPEN; + OrderComponents memory orderComponents = getOrderComponents( + baseOrderParameters, + context.consideration.getCounter(alice) + ); + bytes32 orderHash = context.consideration.getOrderHash(orderComponents); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + { + ( + bool isValidated, + bool isCancelled, + uint256 totalFilled, + uint256 totalSize + ) = context.consideration.getOrderStatus(orderHash); + assertFalse(isValidated); + assertFalse(isCancelled); + assertEq(totalFilled, 0); + assertEq(totalSize, 0); + } + + vm.expectRevert(abi.encodeWithSignature("BadFraction()")); + // Call fulfillAdvancedOrder with an order with a denominator of 0. + context.consideration.fulfillAdvancedOrder{ value: 50 }( + AdvancedOrder(baseOrderParameters, 1, 0, signature, ""), + new CriteriaResolver[](0), + bytes32(0), + address(0) + ); + } + + function testpartialFulfillEthTo1155NumeratorDenominatorSetToZero() public { + test( + this.partialFulfillEthTo1155NumeratorDenominatorSetToZero, + Context(consideration, empty, 0, 0) + ); + test( + this.partialFulfillEthTo1155NumeratorDenominatorSetToZero, + Context(referenceConsideration, empty, 0, 0) + ); + } + + function partialFulfillEthTo1155NumeratorDenominatorSetToZero( + Context memory context + ) external stateless { + // mint 100 tokens + test1155_1.mint(alice, 1, 100); + + addErc1155OfferItem(1, 100); + addEthConsiderationItem(alice, 100); + + _configureOrderParameters(alice, address(0), bytes32(0), 0, false); + baseOrderParameters.orderType = OrderType.PARTIAL_OPEN; + OrderComponents memory orderComponents = getOrderComponents( + baseOrderParameters, + context.consideration.getCounter(alice) + ); + bytes32 orderHash = context.consideration.getOrderHash(orderComponents); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + { + ( + bool isValidated, + bool isCancelled, + uint256 totalFilled, + uint256 totalSize + ) = context.consideration.getOrderStatus(orderHash); + assertFalse(isValidated); + assertFalse(isCancelled); + assertEq(totalFilled, 0); + assertEq(totalSize, 0); + } + + vm.expectRevert(abi.encodeWithSignature("BadFraction()")); + // Call fulfillAdvancedOrder with an order with a numerator and denominator of 0. + context.consideration.fulfillAdvancedOrder{ value: 50 }( + AdvancedOrder(baseOrderParameters, 0, 0, signature, ""), + new CriteriaResolver[](0), + bytes32(0), + address(0) + ); + } } diff --git a/test/foundry/FulfillAdvancedOrderCriteria.t.sol b/test/foundry/FulfillAdvancedOrderCriteria.t.sol new file mode 100644 index 000000000..e8207b1b2 --- /dev/null +++ b/test/foundry/FulfillAdvancedOrderCriteria.t.sol @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.13; + +import { BaseOrderTest } from "./utils/BaseOrderTest.sol"; +import { Merkle } from "murky/Merkle.sol"; +import { ConsiderationInterface } from "../../contracts/interfaces/ConsiderationInterface.sol"; +import { CriteriaResolver, OfferItem, OrderComponents, AdvancedOrder } from "../../contracts/lib/ConsiderationStructs.sol"; +import { ItemType, Side } from "../../contracts/lib/ConsiderationEnums.sol"; + +contract FulfillAdvancedOrderCriteria is BaseOrderTest { + Merkle merkle = new Merkle(); + FuzzArgs empty; + + struct FuzzArgs { + uint256[8] identifiers; + uint8 index; + } + + struct Context { + ConsiderationInterface consideration; + FuzzArgs args; + } + + function test(function(Context memory) external fn, Context memory context) + internal + { + try fn(context) {} catch (bytes memory reason) { + assertPass(reason); + } + } + + function testFulfillAdvancedOrderWithCriteria(FuzzArgs memory args) public { + test( + this.fulfillAdvancedOrderWithCriteria, + Context(consideration, args) + ); + test( + this.fulfillAdvancedOrderWithCriteria, + Context(referenceConsideration, args) + ); + } + + function testFulfillAdvancedOrderWithCriteriaPreimage(FuzzArgs memory args) + public + { + test( + this.fulfillAdvancedOrderWithCriteriaPreimage, + Context(consideration, args) + ); + test( + this.fulfillAdvancedOrderWithCriteriaPreimage, + Context(referenceConsideration, args) + ); + } + + function prepareCriteriaOfferOrder(Context memory context) + internal + returns ( + bytes32[] memory hashedIdentifiers, + AdvancedOrder memory advancedOrder + ) + { + // create a new array to store bytes32 hashes of identifiers + hashedIdentifiers = new bytes32[](context.args.identifiers.length); + for (uint256 i = 0; i < context.args.identifiers.length; i++) { + // try to mint each identifier; fuzzer may include duplicates + try test721_1.mint(alice, context.args.identifiers[i]) {} catch ( + bytes memory + ) {} + // hash identifier and store to generate proof + hashedIdentifiers[i] = keccak256( + abi.encode(context.args.identifiers[i]) + ); + } + + bytes32 root = merkle.getRoot(hashedIdentifiers); + + addOfferItem721Criteria(address(test721_1), uint256(root)); + addEthConsiderationItem(alice, 1); + _configureOrderParameters(alice, address(0), bytes32(0), 0, false); + + OrderComponents memory orderComponents = getOrderComponents( + baseOrderParameters, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + advancedOrder = AdvancedOrder(baseOrderParameters, 1, 1, signature, ""); + } + + function fulfillAdvancedOrderWithCriteria(Context memory context) + external + stateless + { + // pick a "random" index in the array of identifiers; use that identifier + context.args.index = context.args.index % 8; + uint256 identifier = context.args.identifiers[context.args.index]; + + ( + bytes32[] memory hashedIdentifiers, + AdvancedOrder memory advancedOrder + ) = prepareCriteriaOfferOrder(context); + // create resolver for identifier including proof for token at index + CriteriaResolver memory resolver = CriteriaResolver( + 0, + Side.OFFER, + 0, + identifier, + merkle.getProof(hashedIdentifiers, context.args.index) + ); + CriteriaResolver[] memory resolvers = new CriteriaResolver[](1); + resolvers[0] = resolver; + + context.consideration.fulfillAdvancedOrder{ value: 1 }( + advancedOrder, + resolvers, + bytes32(0), + address(0) + ); + + assertEq(address(this), test721_1.ownerOf(identifier)); + } + + function fulfillAdvancedOrderWithCriteriaPreimage(Context memory context) + external + stateless + { + context.args.index = context.args.index % 8; + ( + bytes32[] memory hashedIdentifiers, + AdvancedOrder memory advancedOrder + ) = prepareCriteriaOfferOrder(context); + + // grab a random proof + bytes32[] memory proof = merkle.getProof( + hashedIdentifiers, + context.args.index + ); + // copy all but the first element of the proof to a new array + bytes32[] memory truncatedProof = new bytes32[](proof.length - 1); + for (uint256 i = 0; i < truncatedProof.length - 1; i++) { + truncatedProof[i] = proof[i + 1]; + } + // use the first element as a new token identifier + uint256 preimageIdentifier = uint256(proof[0]); + // try to mint preimageIdentifier; there's a chance it's already minted + try test721_1.mint(alice, preimageIdentifier) {} catch (bytes memory) {} + + // create criteria resolver including first hash as identifier + CriteriaResolver memory resolver = CriteriaResolver( + 0, + Side.OFFER, + 0, + preimageIdentifier, + truncatedProof + ); + CriteriaResolver[] memory resolvers = new CriteriaResolver[](1); + resolvers[0] = resolver; + + vm.expectRevert(abi.encodeWithSignature("InvalidProof()")); + context.consideration.fulfillAdvancedOrder{ value: 1 }( + advancedOrder, + resolvers, + bytes32(0), + address(0) + ); + } + + function addOfferItem721Criteria(address token, uint256 identifierHash) + internal + { + addOfferItem721Criteria(token, identifierHash, 1, 1); + } + + function addOfferItem721Criteria( + address token, + uint256 identifierHash, + uint256 amount + ) internal { + addOfferItem721Criteria(token, identifierHash, amount, amount); + } + + function addOfferItem721Criteria( + address token, + uint256 identifierHash, + uint256 startAmount, + uint256 endAmount + ) internal { + offerItems.push( + OfferItem( + ItemType.ERC721_WITH_CRITERIA, + token, + identifierHash, + startAmount, + endAmount + ) + ); + } +} diff --git a/test/foundry/FulfillAvailableAdvancedOrder.t.sol b/test/foundry/FulfillAvailableAdvancedOrder.t.sol index a1390cda2..ebed17425 100644 --- a/test/foundry/FulfillAvailableAdvancedOrder.t.sol +++ b/test/foundry/FulfillAvailableAdvancedOrder.t.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { OrderType, BasicOrderType, ItemType, Side } from "../../contracts/lib/ConsiderationEnums.sol"; import { AdditionalRecipient } from "../../contracts/lib/ConsiderationStructs.sol"; @@ -12,78 +12,255 @@ import { TestERC1155 } from "../../contracts/test/TestERC1155.sol"; import { TestERC20 } from "../../contracts/test/TestERC20.sol"; import { ProxyRegistry } from "./interfaces/ProxyRegistry.sol"; import { OwnableDelegateProxy } from "./interfaces/OwnableDelegateProxy.sol"; +import { ERC1155Recipient } from "./utils/ERC1155Recipient.sol"; import { stdError } from "forge-std/Test.sol"; +import { ArithmeticUtil } from "./utils/ArithmeticUtil.sol"; contract FulfillAvailableAdvancedOrder is BaseOrderTest { + using ArithmeticUtil for uint256; + using ArithmeticUtil for uint128; + using ArithmeticUtil for uint80; + + FuzzInputs empty; + struct FuzzInputs { address zone; uint256 id; bytes32 zoneHash; uint256 salt; + address recipient; uint128[3] paymentAmts; bool useConduit; + uint80 amount; + uint80 numer; + uint80 denom; } struct Context { ConsiderationInterface consideration; FuzzInputs args; + ItemType itemType; + } + + modifier validateInputs(FuzzInputs memory inputs) { + vm.assume(inputs.amount > 0); + vm.assume( + inputs.paymentAmts[0] > 0 && + inputs.paymentAmts[1] > 0 && + inputs.paymentAmts[2] > 0 + ); + vm.assume( + inputs.paymentAmts[0].add(inputs.paymentAmts[1]).add( + inputs.paymentAmts[2] + ) <= 2**128 - 1 + ); + _; + } + + modifier validateNumerDenom(FuzzInputs memory inputs) { + vm.assume(inputs.amount > 0 && inputs.numer > 0 && inputs.denom > 0); + if (inputs.numer > inputs.denom) { + uint80 temp = inputs.numer; + inputs.numer = inputs.denom; + inputs.denom = temp; + } + vm.assume( + inputs.paymentAmts[0].mul(inputs.denom) + + inputs.paymentAmts[1].mul(inputs.denom) + + inputs.paymentAmts[2].mul(inputs.denom) <= + 2**128 - 1 + ); + _; + } + + function test(function(Context memory) external fn, Context memory context) + internal + { + try fn(context) {} catch (bytes memory reason) { + assertPass(reason); + } + } + + function testNoNativeOffersFulfillAvailableAdvanced( + uint8[8] memory itemTypes + ) public { + uint256 tokenId; + for (uint256 i; i < 8; i++) { + ItemType itemType = ItemType(itemTypes[i] % 4); + if (itemType == ItemType.NATIVE) { + addEthOfferItem(1); + } else if (itemType == ItemType.ERC20) { + addErc20OfferItem(1); + } else if (itemType == ItemType.ERC1155) { + test1155_1.mint(alice, tokenId, 1); + addErc1155OfferItem(tokenId, 1); + } else { + test721_1.mint(alice, tokenId); + addErc721OfferItem(tokenId); + } + tokenId++; + offerComponents.push(FulfillmentComponent(1, i)); + } + addEthOfferItem(1); + + addEthConsiderationItem(alice, 1); + considerationComponents.push(FulfillmentComponent(1, 0)); + + test( + this.noNativeOfferItemsFulfillAvailableAdvanced, + Context(consideration, empty, ItemType(0)) + ); + test( + this.noNativeOfferItemsFulfillAvailableAdvanced, + Context(referenceConsideration, empty, ItemType(0)) + ); + } + + function noNativeOfferItemsFulfillAvailableAdvanced(Context memory context) + external + stateless + { + configureOrderParameters(alice); + uint256 counter = context.consideration.getCounter(alice); + _configureOrderComponents(counter); + bytes32 orderHash = context.consideration.getOrderHash( + baseOrderComponents + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + AdvancedOrder[] memory orders = new AdvancedOrder[](2); + orders[1] = AdvancedOrder(baseOrderParameters, 1, 1, signature, ""); + offerComponentsArray.push(offerComponents); + considerationComponentsArray.push(considerationComponents); + + delete offerItems; + delete considerationItems; + delete offerComponents; + delete considerationComponents; + + token1.mint(alice, 100); + addErc20OfferItem(100); + addEthConsiderationItem(alice, 1); + configureOrderParameters(alice); + counter = context.consideration.getCounter(alice); + _configureOrderComponents(counter); + bytes32 orderHash2 = context.consideration.getOrderHash( + baseOrderComponents + ); + bytes memory signature2 = signOrder( + context.consideration, + alicePk, + orderHash2 + ); + offerComponents.push(FulfillmentComponent(0, 0)); + considerationComponents.push(FulfillmentComponent(0, 0)); + offerComponentsArray.push(offerComponents); + considerationComponentsArray.push(considerationComponents); + + orders[0] = AdvancedOrder(baseOrderParameters, 1, 1, signature2, ""); + + vm.expectRevert(abi.encodeWithSignature("InvalidNativeOfferItem()")); + context.consideration.fulfillAvailableAdvancedOrders{ value: 2 }( + orders, + new CriteriaResolver[](0), + offerComponentsArray, + considerationComponentsArray, + bytes32(0), + address(0), + 2 + ); } function testFulfillAvailableAdvancedOrderOverflow() public { - for (uint256 i; i < 4; i++) { + for (uint256 i; i < 4; ++i) { // skip 721s if (i == 2) { continue; } - _testFulfillAvailableAdvancedOrdersOverflow( - consideration, - ItemType(i) + test( + this.fulfillAvailableAdvancedOrdersOverflow, + Context(consideration, empty, ItemType(i)) ); - _testFulfillAvailableAdvancedOrdersOverflow( - referenceConsideration, - ItemType(i) + test( + this.fulfillAvailableAdvancedOrdersOverflow, + Context(referenceConsideration, empty, ItemType(i)) ); } } - function testFulfillSingleOrderViaFulfillAvailableAdvancedOrdersEthToSingleErc721( + function testFulfillAvailableAdvancedOrderMissingItemAmount() public { + for (uint256 i; i < 4; ++i) { + // skip 721s + if (i == 2) { + continue; + } + test( + this.fulfillAvailableAdvancedOrdersMissingItemAmount, + Context(consideration, empty, ItemType(i)) + ); + test( + this.fulfillAvailableAdvancedOrdersMissingItemAmount, + Context(referenceConsideration, empty, ItemType(i)) + ); + } + } + + function testFulfillSingleOrderViaFulfillAvailableAdvancedOrdersEthToErc1155( FuzzInputs memory args - ) public { - _testFulfillSingleOrderViaFulfillAvailableAdvancedOrdersEthToSingleErc721( - Context(referenceConsideration, args) + ) + public + validateInputs(args) + onlyPayable(args.zone) + only1155Receiver(args.zone) + onlyPayable(args.recipient) + only1155Receiver(args.recipient) + { + test( + this + .fulfillSingleOrderViaFulfillAvailableAdvancedOrdersEthToErc1155, + Context(referenceConsideration, args, ItemType(0)) ); - _testFulfillSingleOrderViaFulfillAvailableAdvancedOrdersEthToSingleErc721( - Context(consideration, args) + test( + this + .fulfillSingleOrderViaFulfillAvailableAdvancedOrdersEthToErc1155, + Context(consideration, args, ItemType(0)) ); } function testPartialFulfillSingleOrderViaFulfillAvailableAdvancedOrdersEthToErc1155( - FuzzInputs memory args, - uint80 amount, - uint80 numerator, - uint80 denominator - ) public { - _testPartialFulfillSingleOrderViaFulfillAvailableAdvancedOrdersEthToErc1155( - Context(referenceConsideration, args), - amount, - numerator, - denominator - ); - _testPartialFulfillSingleOrderViaFulfillAvailableAdvancedOrdersEthToErc1155( - Context(consideration, args), - amount, - numerator, - denominator + FuzzInputs memory args + ) + public + validateInputs(args) + onlyPayable(args.zone) + only1155Receiver(args.zone) + onlyPayable(args.recipient) + only1155Receiver(args.recipient) + validateNumerDenom(args) + { + test( + this + .partialFulfillSingleOrderViaFulfillAvailableAdvancedOrdersEthToErc1155, + Context(referenceConsideration, args, ItemType(0)) + ); + test( + this + .partialFulfillSingleOrderViaFulfillAvailableAdvancedOrdersEthToErc1155, + Context(consideration, args, ItemType(0)) ); } - function _testFulfillAvailableAdvancedOrdersOverflow( - ConsiderationInterface _consideration, - ItemType itemType - ) internal resetTokenBalancesBetweenRuns { + function fulfillAvailableAdvancedOrdersOverflow(Context memory context) + external + stateless + { test721_1.mint(alice, 1); - _configureERC721OfferItem(1); - _configureConsiderationItem(alice, itemType, 1, 100); + addErc721OfferItem(1); + addConsiderationItem(alice, context.itemType, 1, 100); OrderParameters memory orderParameters = OrderParameters( address(alice), @@ -101,21 +278,21 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { OrderComponents memory firstOrderComponents = getOrderComponents( orderParameters, - _consideration.getNonce(alice) + context.consideration.getCounter(alice) ); bytes memory signature = signOrder( - _consideration, + context.consideration, alicePk, - _consideration.getOrderHash(firstOrderComponents) + context.consideration.getOrderHash(firstOrderComponents) ); delete offerItems; delete considerationItems; test721_1.mint(bob, 2); - _configureERC721OfferItem(2); - // try to overflow the aggregated amount of eth sent to alice - _configureConsiderationItem(alice, itemType, 1, MAX_INT); + addErc721OfferItem(2); + // try to overflow the aggregated amount sent to alice + addConsiderationItem(alice, context.itemType, 1, MAX_INT); OrderParameters memory secondOrderParameters = OrderParameters( address(bob), @@ -133,12 +310,12 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { OrderComponents memory secondOrderComponents = getOrderComponents( secondOrderParameters, - _consideration.getNonce(bob) + context.consideration.getCounter(bob) ); bytes memory secondSignature = signOrder( - _consideration, + context.consideration, bobPk, - _consideration.getOrderHash(secondOrderComponents) + context.consideration.getOrderHash(secondOrderComponents) ); AdvancedOrder[] memory advancedOrders = new AdvancedOrder[](2); @@ -162,59 +339,151 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { delete offerComponents; offerComponents.push(FulfillmentComponent(1, 0)); offerComponentsArray.push(offerComponents); - resetOfferComponents(); + delete offerComponents; - // agregate eth considerations together + // aggregate eth considerations together considerationComponents.push(FulfillmentComponent(0, 0)); considerationComponents.push(FulfillmentComponent(1, 0)); considerationComponentsArray.push(considerationComponents); - resetConsiderationComponents(); + delete considerationComponents; CriteriaResolver[] memory criteriaResolvers; vm.expectRevert(stdError.arithmeticError); - _consideration.fulfillAvailableAdvancedOrders{ value: 99 }( + context.consideration.fulfillAvailableAdvancedOrders{ value: 99 }( advancedOrders, criteriaResolvers, offerComponentsArray, considerationComponentsArray, bytes32(0), + address(0), 100 ); } - function _testFulfillSingleOrderViaFulfillAvailableAdvancedOrdersEthToSingleErc721( + function fulfillAvailableAdvancedOrdersMissingItemAmount( Context memory context - ) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 + ) external stateless { + test721_1.mint(alice, 1); + addErc721OfferItem(1); + addConsiderationItem(alice, context.itemType, 1, 100); + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + address(0), + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + bytes32(0), + 0, + bytes32(0), + considerationItems.length ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) <= - 2**128 - 1 + + OrderComponents memory firstOrderComponents = getOrderComponents( + orderParameters, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(firstOrderComponents) + ); + + delete offerItems; + delete considerationItems; + + test721_1.mint(bob, 2); + addErc721OfferItem(2); + // try to overflow the aggregated amount sent to alice + addConsiderationItem(alice, context.itemType, 1, MAX_INT); + addConsiderationItem(alice, context.itemType, 1, 0); + + OrderParameters memory secondOrderParameters = OrderParameters( + address(bob), + address(0), + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + bytes32(0), + 0, + bytes32(0), + considerationItems.length + ); + + OrderComponents memory secondOrderComponents = getOrderComponents( + secondOrderParameters, + context.consideration.getCounter(bob) + ); + bytes memory secondSignature = signOrder( + context.consideration, + bobPk, + context.consideration.getOrderHash(secondOrderComponents) + ); + + AdvancedOrder[] memory advancedOrders = new AdvancedOrder[](2); + advancedOrders[0] = AdvancedOrder( + orderParameters, + uint120(1), + uint120(1), + signature, + "0x" + ); + advancedOrders[1] = AdvancedOrder( + secondOrderParameters, + uint120(1), + uint120(1), + secondSignature, + "0x" ); + offerComponents.push(FulfillmentComponent(0, 0)); + offerComponentsArray.push(offerComponents); + delete offerComponents; + offerComponents.push(FulfillmentComponent(1, 0)); + offerComponentsArray.push(offerComponents); + delete offerComponents; + + // aggregate eth considerations together + considerationComponents.push(FulfillmentComponent(0, 0)); + considerationComponents.push(FulfillmentComponent(1, 0)); + considerationComponents.push(FulfillmentComponent(1, 1)); + considerationComponentsArray.push(considerationComponents); + delete considerationComponents; + + CriteriaResolver[] memory criteriaResolvers; + + vm.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x11)); + context.consideration.fulfillAvailableAdvancedOrders{ value: 99 }( + advancedOrders, + criteriaResolvers, + offerComponentsArray, + considerationComponentsArray, + bytes32(0), + address(0), + 100 + ); + } + + function fulfillSingleOrderViaFulfillAvailableAdvancedOrdersEthToErc1155( + Context memory context + ) external stateless { bytes32 conduitKey = context.args.useConduit ? conduitKeyOne : bytes32(0); - test721_1.mint(alice, context.args.id); + test1155_1.mint(alice, context.args.id, context.args.amount); offerItems.push( OfferItem( - ItemType.ERC721, - address(test721_1), + ItemType.ERC1155, + address(test1155_1), context.args.id, - 1, - 1 + context.args.amount, + context.args.amount ) ); considerationItems.push( @@ -222,8 +491,8 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { ItemType.NATIVE, address(0), 0, - uint256(context.args.paymentAmts[0]), - uint256(context.args.paymentAmts[0]), + 10, + 10, payable(alice) ) ); @@ -232,8 +501,8 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { ItemType.NATIVE, address(0), 0, - uint256(context.args.paymentAmts[1]), - uint256(context.args.paymentAmts[1]), + 10, + 10, payable(context.args.zone) ) ); @@ -242,8 +511,8 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { ItemType.NATIVE, address(0), 0, - uint256(context.args.paymentAmts[2]), - uint256(context.args.paymentAmts[2]), + 10, + 10, payable(cal) ) ); @@ -258,7 +527,7 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { context.args.zoneHash, context.args.salt, conduitKey, - context.consideration.getNonce(alice) + context.consideration.getCounter(alice) ); bytes memory signature = signOrder( context.consideration, @@ -268,19 +537,19 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { offerComponents.push(FulfillmentComponent(0, 0)); offerComponentsArray.push(offerComponents); - resetOfferComponents(); + delete offerComponents; considerationComponents.push(FulfillmentComponent(0, 0)); considerationComponentsArray.push(considerationComponents); - resetConsiderationComponents(); + delete considerationComponents; considerationComponents.push(FulfillmentComponent(0, 1)); considerationComponentsArray.push(considerationComponents); - resetConsiderationComponents(); + delete considerationComponents; considerationComponents.push(FulfillmentComponent(0, 2)); considerationComponentsArray.push(considerationComponents); - resetConsiderationComponents(); + delete considerationComponents; assertTrue(considerationComponentsArray.length == 3); @@ -307,67 +576,42 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { "0x" ); - CriteriaResolver[] memory criteriaResolvers; - - context.consideration.fulfillAvailableAdvancedOrders{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] - }( + context.consideration.fulfillAvailableAdvancedOrders{ value: 30 }( advancedOrders, - criteriaResolvers, + new CriteriaResolver[](0), offerComponentsArray, considerationComponentsArray, conduitKey, + bob, 100 ); - } - function _testPartialFulfillSingleOrderViaFulfillAvailableAdvancedOrdersEthToErc1155( - Context memory context, - uint80 amount, - uint80 numerator, - uint80 denominator - ) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - vm.assume( - amount > 0 && - numerator > 0 && - denominator > 0 && - numerator < denominator - ); - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) * - denominator + - uint256(context.args.paymentAmts[1]) * - denominator + - uint256(context.args.paymentAmts[2]) * - denominator <= - 2**128 - 1 + assertEq( + test1155_1.balanceOf(bob, context.args.id), + context.args.amount ); + } + function partialFulfillSingleOrderViaFulfillAvailableAdvancedOrdersEthToErc1155( + Context memory context + ) external stateless { bytes32 conduitKey = context.args.useConduit ? conduitKeyOne : bytes32(0); - test1155_1.mint(alice, context.args.id, uint256(amount) * denominator); + test1155_1.mint( + alice, + context.args.id, + context.args.amount.mul(context.args.denom) + ); offerItems.push( OfferItem( ItemType.ERC1155, address(test1155_1), context.args.id, - uint256(amount) * denominator, - uint256(amount) * denominator + context.args.amount.mul(context.args.denom), + context.args.amount.mul(context.args.denom) ) ); considerationItems.push( @@ -375,8 +619,8 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { ItemType.NATIVE, address(0), 0, - uint256(context.args.paymentAmts[0]) * denominator, - uint256(context.args.paymentAmts[0]) * denominator, + context.args.paymentAmts[0].mul(context.args.denom), + context.args.paymentAmts[0].mul(context.args.denom), payable(alice) ) ); @@ -385,8 +629,8 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { ItemType.NATIVE, address(0), 0, - uint256(context.args.paymentAmts[1]) * denominator, - uint256(context.args.paymentAmts[1]) * denominator, + context.args.paymentAmts[1].mul(context.args.denom), + context.args.paymentAmts[1].mul(context.args.denom), payable(context.args.zone) ) ); @@ -395,8 +639,8 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { ItemType.NATIVE, address(0), 0, - uint256(context.args.paymentAmts[2]) * denominator, - uint256(context.args.paymentAmts[2]) * denominator, + context.args.paymentAmts[2].mul(context.args.denom), + context.args.paymentAmts[2].mul(context.args.denom), payable(cal) ) ); @@ -412,7 +656,7 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { context.args.zoneHash, context.args.salt, conduitKey, - context.consideration.getNonce(alice) + context.consideration.getCounter(alice) ); bytes memory signature = signOrder( context.consideration, @@ -422,19 +666,19 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { offerComponents.push(FulfillmentComponent(0, 0)); offerComponentsArray.push(offerComponents); - resetOfferComponents(); + delete offerComponents; considerationComponents.push(FulfillmentComponent(0, 0)); considerationComponentsArray.push(considerationComponents); - resetConsiderationComponents(); + delete considerationComponents; considerationComponents.push(FulfillmentComponent(0, 1)); considerationComponentsArray.push(considerationComponents); - resetConsiderationComponents(); + delete considerationComponents; considerationComponents.push(FulfillmentComponent(0, 2)); considerationComponentsArray.push(considerationComponents); - resetConsiderationComponents(); + delete considerationComponents; assertTrue(considerationComponentsArray.length == 3); @@ -455,8 +699,8 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { AdvancedOrder[] memory advancedOrders = new AdvancedOrder[](1); advancedOrders[0] = AdvancedOrder( orderParameters, - numerator, - denominator, + context.args.numer, + context.args.denom, signature, "0x" ); @@ -464,7 +708,7 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { CriteriaResolver[] memory criteriaResolvers; uint256 value = (context.args.paymentAmts[0] + context.args.paymentAmts[1] + - context.args.paymentAmts[2]) * uint256(denominator); + context.args.paymentAmts[2]).mul(context.args.denom); context.consideration.fulfillAvailableAdvancedOrders{ value: value }( advancedOrders, @@ -472,6 +716,7 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { offerComponentsArray, considerationComponentsArray, conduitKey, + address(0), 100 ); @@ -479,7 +724,210 @@ contract FulfillAvailableAdvancedOrder is BaseOrderTest { (, , uint256 totalFilled, uint256 totalSize) = context .consideration .getOrderStatus(orderHash); - assertEq(totalFilled, uint256(numerator)); - assertEq(totalSize, uint256(denominator)); + assertEq(totalFilled, context.args.numer); + assertEq(totalSize, context.args.denom); + } + + function testPartialFulfillDenominatorOverflowEthToErc1155() public { + test( + this.partialFulfillDenominatorOverflowEthToErc1155, + Context(referenceConsideration, empty, ItemType(0)) + ); + test( + this.partialFulfillDenominatorOverflowEthToErc1155, + Context(consideration, empty, ItemType(0)) + ); + } + + function partialFulfillDenominatorOverflowEthToErc1155( + Context memory context + ) external stateless { + // Mint 100 tokens to alice. + test1155_1.mint(alice, 1, 100); + + addErc1155OfferItem(1, 100); + addEthConsiderationItem(alice, 100); + + _configureOrderParameters(alice, address(0), bytes32(0), 0, false); + baseOrderParameters.orderType = OrderType.PARTIAL_OPEN; + OrderComponents memory orderComponents = getOrderComponents( + baseOrderParameters, + context.consideration.getCounter(alice) + ); + bytes32 orderHash = context.consideration.getOrderHash(orderComponents); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + { + ( + bool isValidated, + bool isCancelled, + uint256 totalFilled, + uint256 totalSize + ) = context.consideration.getOrderStatus(orderHash); + assertFalse(isValidated); + assertFalse(isCancelled); + assertEq(totalFilled, 0); + assertEq(totalSize, 0); + } + + // Aggregate the orders in an AdvancedOrder array. + AdvancedOrder[] memory orders = new AdvancedOrder[](2); + orders[0] = AdvancedOrder( + baseOrderParameters, + 2**118, + 2**119, + signature, + "" + ); + orders[1] = AdvancedOrder(baseOrderParameters, 1, 10, signature, ""); + + // Aggregate the erc1155 offers together. + offerComponents.push(FulfillmentComponent(0, 0)); + offerComponents.push(FulfillmentComponent(1, 0)); + offerComponentsArray.push(offerComponents); + delete offerComponents; + + // Aggregate the eth considerations together. + considerationComponents.push(FulfillmentComponent(0, 0)); + considerationComponents.push(FulfillmentComponent(1, 0)); + considerationComponentsArray.push(considerationComponents); + delete considerationComponents; + + // Pass in the AdvancedOrder array and fulfill both partially-fulfillable orders. + context.consideration.fulfillAvailableAdvancedOrders{ value: 60 }( + orders, + new CriteriaResolver[](0), + offerComponentsArray, + considerationComponentsArray, + bytes32(0), + address(0), + 100 + ); + + // Assert six-tenths of the offer has been fulfilled. + { + ( + bool isValidated, + bool isCancelled, + uint256 totalFilled, + uint256 totalSize + ) = context.consideration.getOrderStatus(orderHash); + assertTrue(isValidated); + assertFalse(isCancelled); + assertEq(totalFilled, 6); + + assertEq(totalSize, 10); + assertEq(60, test1155_1.balanceOf(address(this), 1)); + } + } + + function testPartialFulfillDenominatorOverflowEthToErc1155NonAggregated() + public + { + test( + this.partialFulfillDenominatorOverflowEthToErc1155NonAggregated, + Context(referenceConsideration, empty, ItemType(0)) + ); + test( + this.partialFulfillDenominatorOverflowEthToErc1155NonAggregated, + Context(consideration, empty, ItemType(0)) + ); + } + + function partialFulfillDenominatorOverflowEthToErc1155NonAggregated( + Context memory context + ) external stateless { + // Mint 100 tokens to alice. + test1155_1.mint(alice, 1, 100); + + addErc1155OfferItem(1, 100); + addEthConsiderationItem(alice, 100); + + _configureOrderParameters(alice, address(0), bytes32(0), 0, false); + baseOrderParameters.orderType = OrderType.PARTIAL_OPEN; + OrderComponents memory orderComponents = getOrderComponents( + baseOrderParameters, + context.consideration.getCounter(alice) + ); + bytes32 orderHash = context.consideration.getOrderHash(orderComponents); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + { + ( + bool isValidated, + bool isCancelled, + uint256 totalFilled, + uint256 totalSize + ) = context.consideration.getOrderStatus(orderHash); + assertFalse(isValidated); + assertFalse(isCancelled); + assertEq(totalFilled, 0); + assertEq(totalSize, 0); + } + + // Aggregate the orders in an AdvancedOrder array. + AdvancedOrder[] memory orders = new AdvancedOrder[](2); + orders[0] = AdvancedOrder( + baseOrderParameters, + 2**118, + 2**119, + signature, + "" + ); + orders[1] = AdvancedOrder(baseOrderParameters, 1, 10, signature, ""); + + // Add the offer components of the two orders separately to the offer components array. + // This results in two separate transfers of erc1155 tokens as opposed to one aggregated transfer. + offerComponents.push(FulfillmentComponent(0, 0)); + offerComponentsArray.push(offerComponents); + delete offerComponents; + offerComponents.push(FulfillmentComponent(1, 0)); + offerComponentsArray.push(offerComponents); + delete offerComponents; + + // Add the consideration components corresponding to the offer components. + considerationComponents.push(FulfillmentComponent(0, 0)); + considerationComponentsArray.push(considerationComponents); + delete considerationComponents; + considerationComponents.push(FulfillmentComponent(1, 0)); + considerationComponentsArray.push(considerationComponents); + delete considerationComponents; + + // Pass in the AdvancedOrder array and fulfill both partially-fulfillable orders. + context.consideration.fulfillAvailableAdvancedOrders{ value: 60 }( + orders, + new CriteriaResolver[](0), + offerComponentsArray, + considerationComponentsArray, + bytes32(0), + address(0), + 100 + ); + + // Assert six-tenths of the offer has been fulfilled. + { + ( + bool isValidated, + bool isCancelled, + uint256 totalFilled, + uint256 totalSize + ) = context.consideration.getOrderStatus(orderHash); + assertTrue(isValidated); + assertFalse(isCancelled); + assertEq(totalFilled, 6); + + assertEq(totalSize, 10); + assertEq(60, test1155_1.balanceOf(address(this), 1)); + } } } diff --git a/test/foundry/FulfillAvailableAdvancedOrderCriteria.t.sol b/test/foundry/FulfillAvailableAdvancedOrderCriteria.t.sol new file mode 100644 index 000000000..79f7abe38 --- /dev/null +++ b/test/foundry/FulfillAvailableAdvancedOrderCriteria.t.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.13; + +import { BaseOrderTest } from "./utils/BaseOrderTest.sol"; +import { Merkle } from "murky/Merkle.sol"; +import { ConsiderationInterface } from "../../contracts/interfaces/ConsiderationInterface.sol"; +import { CriteriaResolver, OfferItem, OrderComponents, AdvancedOrder, FulfillmentComponent } from "../../contracts/lib/ConsiderationStructs.sol"; +import { ItemType, Side } from "../../contracts/lib/ConsiderationEnums.sol"; + +contract FulfillAdvancedOrderCriteria is BaseOrderTest { + Merkle merkle = new Merkle(); + FuzzArgs empty; + + struct FuzzArgs { + uint256[8] identifiers; + uint8 index; + } + + struct Context { + ConsiderationInterface consideration; + FuzzArgs args; + } + + function test(function(Context memory) external fn, Context memory context) + internal + { + try fn(context) {} catch (bytes memory reason) { + assertPass(reason); + } + } + + function prepareCriteriaOfferOrder(Context memory context) + internal + returns ( + bytes32[] memory hashedIdentifiers, + AdvancedOrder memory advancedOrder + ) + { + // create a new array to store bytes32 hashes of identifiers + hashedIdentifiers = new bytes32[](context.args.identifiers.length); + for (uint256 i = 0; i < context.args.identifiers.length; i++) { + // try to mint each identifier; fuzzer may include duplicates + try test721_1.mint(alice, context.args.identifiers[i]) {} catch ( + bytes memory + ) {} + // hash identifier and store to generate proof + hashedIdentifiers[i] = keccak256( + abi.encode(context.args.identifiers[i]) + ); + } + + bytes32 root = merkle.getRoot(hashedIdentifiers); + + addOfferItem721Criteria(address(test721_1), uint256(root)); + addEthConsiderationItem(alice, 1); + _configureOrderParameters(alice, address(0), bytes32(0), 0, false); + + OrderComponents memory orderComponents = getOrderComponents( + baseOrderParameters, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + advancedOrder = AdvancedOrder(baseOrderParameters, 1, 1, signature, ""); + } + + function addOfferItem721Criteria(address token, uint256 identifierHash) + internal + { + addOfferItem721Criteria(token, identifierHash, 1, 1); + } + + function addOfferItem721Criteria( + address token, + uint256 identifierHash, + uint256 amount + ) internal { + addOfferItem721Criteria(token, identifierHash, amount, amount); + } + + function addOfferItem721Criteria( + address token, + uint256 identifierHash, + uint256 startAmount, + uint256 endAmount + ) internal { + offerItems.push( + OfferItem( + ItemType.ERC721_WITH_CRITERIA, + token, + identifierHash, + startAmount, + endAmount + ) + ); + } + + function testFulfillAvailableAdvancedOrdersWithCriteria( + FuzzArgs memory args + ) public { + test( + this.fulfillAvailableAdvancedOrdersWithCriteria, + Context(consideration, args) + ); + test( + this.fulfillAvailableAdvancedOrdersWithCriteria, + Context(referenceConsideration, args) + ); + } + + function fulfillAvailableAdvancedOrdersWithCriteria(Context memory context) + external + stateless + { + // pick a "random" index in the array of identifiers; use that identifier + context.args.index = context.args.index % 8; + uint256 identifier = context.args.identifiers[context.args.index]; + + ( + bytes32[] memory hashedIdentifiers, + AdvancedOrder memory advancedOrder + ) = prepareCriteriaOfferOrder(context); + + // add advancedOrder to an AdvancedOrder array + AdvancedOrder[] memory advancedOrders = new AdvancedOrder[](1); + advancedOrders[0] = advancedOrder; + + // create resolver for identifier including proof for token at index + CriteriaResolver memory resolver = CriteriaResolver( + 0, + Side.OFFER, + 0, + identifier, + merkle.getProof(hashedIdentifiers, context.args.index) + ); + CriteriaResolver[] memory resolvers = new CriteriaResolver[](1); + resolvers[0] = resolver; + + // add erc721 offer to offerComponentsArray + offerComponents.push(FulfillmentComponent(0, 0)); + offerComponentsArray.push(offerComponents); + resetOfferComponents(); + + // add eth consideration to considerationComponentsArray + considerationComponents.push(FulfillmentComponent(0, 0)); + considerationComponentsArray.push(considerationComponents); + resetConsiderationComponents(); + + context.consideration.fulfillAvailableAdvancedOrders{ value: 1 }( + advancedOrders, + resolvers, + offerComponentsArray, + considerationComponentsArray, + bytes32(0), + address(0), + 100 + ); + + assertEq(address(this), test721_1.ownerOf(identifier)); + } +} diff --git a/test/foundry/FulfillBasicOrderTest.sol b/test/foundry/FulfillBasicOrderTest.sol deleted file mode 100644 index a16bceaf1..000000000 --- a/test/foundry/FulfillBasicOrderTest.sol +++ /dev/null @@ -1,289 +0,0 @@ -// SPDX-License-Identifier: MIT -//Author: CupOJoseph - -pragma solidity 0.8.13; - -import { OrderType, BasicOrderType, ItemType, Side } from "../../contracts/lib/ConsiderationEnums.sol"; -import { AdditionalRecipient } from "../../contracts/lib/ConsiderationStructs.sol"; -import { ConsiderationInterface } from "../../contracts/interfaces/ConsiderationInterface.sol"; -import { OfferItem, ConsiderationItem, OrderComponents, BasicOrderParameters } from "../../contracts/lib/ConsiderationStructs.sol"; -import { BaseOrderTest } from "./utils/BaseOrderTest.sol"; - -import { TestERC721 } from "../../contracts/test/TestERC721.sol"; - -import { TestERC1155 } from "../../contracts/test/TestERC1155.sol"; - -import { TestERC20 } from "../../contracts/test/TestERC20.sol"; - -contract FulfillBasicOrderTest is BaseOrderTest { - BasicOrderParameters basicOrderParameters; - OrderComponents orderComponents; - - struct FuzzInputsCommon { - address zone; - uint256 tokenId; - uint128 paymentAmount; - bytes32 zoneHash; - uint256 salt; - } - struct Context { - ConsiderationInterface consideration; - FuzzInputsCommon args; - uint128 tokenAmount; - } - - function testBasicEthTo721(FuzzInputsCommon memory inputs) public { - _configureERC721OfferItem(inputs.tokenId); - _configureEthConsiderationItem(alice, inputs.paymentAmount); - _configureBasicOrderParametersEthTo721(inputs); - - _testBasicEthTo721_new(Context(consideration, inputs, 0)); - _configureERC721OfferItem(inputs.tokenId); - _configureEthConsiderationItem(alice, inputs.paymentAmount); - _testBasicEthTo721_new(Context(referenceConsideration, inputs, 0)); - } - - function testBasicErc20To721(FuzzInputsCommon memory inputs) public { - _configureERC721OfferItem(inputs.tokenId); - _configureErc20ConsiderationItem(alice, inputs.paymentAmount); - _configureBasicOrderParametersErc20To721(inputs); - - _testBasicErc20To721_new(Context(consideration, inputs, 0)); - _configureERC721OfferItem(inputs.tokenId); - _configureErc20ConsiderationItem(alice, inputs.paymentAmount); - _testBasicErc20To721_new(Context(referenceConsideration, inputs, 0)); - } - - function testBasicEthTo1155( - FuzzInputsCommon memory inputs, - uint128 tokenAmount - ) public { - _configureERC1155OfferItem(inputs.tokenId, tokenAmount); - _configureEthConsiderationItem(alice, inputs.paymentAmount); - - _configureBasicOrderParametersEthTo1155(inputs, tokenAmount); - _testBasicEthTo1155_new( - Context(referenceConsideration, inputs, tokenAmount) - ); - _configureERC1155OfferItem(inputs.tokenId, tokenAmount); - _configureEthConsiderationItem(alice, inputs.paymentAmount); - _testBasicEthTo1155_new(Context(consideration, inputs, tokenAmount)); - } - - function testBasicErc20To1155( - FuzzInputsCommon memory inputs, - uint128 tokenAmount - ) public { - _configureERC1155OfferItem(inputs.tokenId, tokenAmount); - _configureErc20ConsiderationItem(alice, inputs.paymentAmount); - - _configureBasicOrderParametersErc20To1155(inputs, tokenAmount); - _testBasicErc20To1155_new( - Context(referenceConsideration, inputs, tokenAmount) - ); - _configureERC1155OfferItem(inputs.tokenId, tokenAmount); - _configureErc20ConsiderationItem(alice, inputs.paymentAmount); - _testBasicErc20To1155_new(Context(consideration, inputs, tokenAmount)); - } - - function _testBasicErc20To1155_new(Context memory context) - internal - resetTokenBalancesBetweenRuns - { - vm.assume(context.args.paymentAmount > 0); - vm.assume(context.tokenAmount > 0); - - test1155_1.mint(alice, context.args.tokenId, context.tokenAmount); - - _configureOrderComponents( - context.args.zone, - context.args.zoneHash, - context.args.salt, - bytes32(0) - ); - uint256 nonce = context.consideration.getNonce(alice); - orderComponents.nonce = nonce; - bytes32 orderHash = context.consideration.getOrderHash(orderComponents); - bytes memory signature = signOrder( - context.consideration, - alicePk, - orderHash - ); - - basicOrderParameters.signature = signature; - context.consideration.fulfillBasicOrder(basicOrderParameters); - } - - function _testBasicEthTo1155_new(Context memory context) - internal - resetTokenBalancesBetweenRuns - { - vm.assume(context.args.paymentAmount > 0); - vm.assume(context.tokenAmount > 0); - - test1155_1.mint(alice, context.args.tokenId, context.tokenAmount); - - _configureOrderComponents( - context.args.zone, - context.args.zoneHash, - context.args.salt, - bytes32(0) - ); - uint256 nonce = context.consideration.getNonce(alice); - orderComponents.nonce = nonce; - bytes32 orderHash = context.consideration.getOrderHash(orderComponents); - bytes memory signature = signOrder( - context.consideration, - alicePk, - orderHash - ); - - basicOrderParameters.signature = signature; - context.consideration.fulfillBasicOrder{ - value: context.args.paymentAmount - }(basicOrderParameters); - } - - function _testBasicEthTo721_new(Context memory context) - internal - resetTokenBalancesBetweenRuns - { - vm.assume(context.args.paymentAmount > 0); - - test721_1.mint(alice, context.args.tokenId); - - _configureOrderComponents( - context.args.zone, - context.args.zoneHash, - context.args.salt, - bytes32(0) - ); - uint256 nonce = context.consideration.getNonce(alice); - orderComponents.nonce = nonce; - bytes32 orderHash = context.consideration.getOrderHash(orderComponents); - bytes memory signature = signOrder( - context.consideration, - alicePk, - orderHash - ); - - basicOrderParameters.signature = signature; - context.consideration.fulfillBasicOrder{ - value: context.args.paymentAmount - }(basicOrderParameters); - } - - function _testBasicErc20To721_new(Context memory context) - internal - resetTokenBalancesBetweenRuns - { - vm.assume(context.args.paymentAmount > 0); - - test721_1.mint(alice, context.args.tokenId); - - _configureOrderComponents( - context.args.zone, - context.args.zoneHash, - context.args.salt, - bytes32(0) - ); - uint256 nonce = context.consideration.getNonce(alice); - orderComponents.nonce = nonce; - bytes32 orderHash = context.consideration.getOrderHash(orderComponents); - bytes memory signature = signOrder( - context.consideration, - alicePk, - orderHash - ); - - basicOrderParameters.signature = signature; - context.consideration.fulfillBasicOrder(basicOrderParameters); - } - - function _configureBasicOrderParametersEthTo721( - FuzzInputsCommon memory args - ) internal { - basicOrderParameters.considerationToken = address(0); - basicOrderParameters.considerationIdentifier = 0; - basicOrderParameters.considerationAmount = args.paymentAmount; - basicOrderParameters.offerer = payable(alice); - basicOrderParameters.zone = args.zone; - basicOrderParameters.offerToken = address(test721_1); - basicOrderParameters.offerIdentifier = args.tokenId; - basicOrderParameters.offerAmount = 1; - basicOrderParameters.basicOrderType = BasicOrderType - .ETH_TO_ERC721_FULL_OPEN; - basicOrderParameters.startTime = block.timestamp; - basicOrderParameters.endTime = block.timestamp + 100; - basicOrderParameters.zoneHash = args.zoneHash; - basicOrderParameters.salt = args.salt; - basicOrderParameters.offererConduitKey = bytes32(0); - basicOrderParameters.fulfillerConduitKey = bytes32(0); - basicOrderParameters.totalOriginalAdditionalRecipients = 0; - // additional recipients should always be empty - // don't do signature; - } - - function _configureBasicOrderParametersEthTo1155( - FuzzInputsCommon memory args, - uint128 amount - ) internal { - basicOrderParameters.considerationToken = address(0); - basicOrderParameters.considerationIdentifier = 0; - basicOrderParameters.considerationAmount = args.paymentAmount; - basicOrderParameters.offerer = payable(alice); - basicOrderParameters.zone = args.zone; - basicOrderParameters.offerToken = address(test1155_1); - basicOrderParameters.offerIdentifier = args.tokenId; - basicOrderParameters.offerAmount = amount; - basicOrderParameters.basicOrderType = BasicOrderType - .ETH_TO_ERC1155_FULL_OPEN; - basicOrderParameters.startTime = block.timestamp; - basicOrderParameters.endTime = block.timestamp + 100; - basicOrderParameters.zoneHash = args.zoneHash; - basicOrderParameters.salt = args.salt; - basicOrderParameters.offererConduitKey = bytes32(0); - basicOrderParameters.fulfillerConduitKey = bytes32(0); - basicOrderParameters.totalOriginalAdditionalRecipients = 0; - // additional recipients should always be empty - // don't do signature; - } - - function _configureBasicOrderParametersErc20To1155( - FuzzInputsCommon memory args, - uint128 amount - ) internal { - _configureBasicOrderParametersEthTo1155(args, amount); - basicOrderParameters.considerationToken = address(token1); - basicOrderParameters.basicOrderType = BasicOrderType - .ERC20_TO_ERC1155_FULL_OPEN; - } - - function _configureBasicOrderParametersErc20To721( - FuzzInputsCommon memory args - ) internal { - _configureBasicOrderParametersEthTo721(args); - basicOrderParameters.considerationToken = address(token1); - basicOrderParameters.basicOrderType = BasicOrderType - .ERC20_TO_ERC721_FULL_OPEN; - } - - function _configureOrderComponents( - address zone, - bytes32 zoneHash, - uint256 salt, - bytes32 conduitKey - ) internal { - orderComponents.offerer = alice; - orderComponents.zone = zone; - orderComponents.offer = offerItems; - orderComponents.consideration = considerationItems; - orderComponents.orderType = OrderType.FULL_OPEN; - orderComponents.startTime = block.timestamp; - orderComponents.endTime = block.timestamp + 100; - orderComponents.zoneHash = zoneHash; - orderComponents.salt = salt; - orderComponents.conduitKey = conduitKey; - // don't set nonce - } -} diff --git a/test/foundry/FulfillBasicOrderTest.t.sol b/test/foundry/FulfillBasicOrderTest.t.sol new file mode 100644 index 000000000..c5373756b --- /dev/null +++ b/test/foundry/FulfillBasicOrderTest.t.sol @@ -0,0 +1,703 @@ +// SPDX-License-Identifier: MIT +//Author: CupOJoseph + +pragma solidity >=0.8.13; + +import { OrderType, BasicOrderType, ItemType, Side } from "../../contracts/lib/ConsiderationEnums.sol"; +import { AdditionalRecipient, Order } from "../../contracts/lib/ConsiderationStructs.sol"; +import { ConsiderationInterface } from "../../contracts/interfaces/ConsiderationInterface.sol"; +import { OfferItem, ConsiderationItem, OrderComponents, BasicOrderParameters } from "../../contracts/lib/ConsiderationStructs.sol"; +import { BaseOrderTest } from "./utils/BaseOrderTest.sol"; + +import { TestERC721 } from "../../contracts/test/TestERC721.sol"; + +import { TestERC1155 } from "../../contracts/test/TestERC1155.sol"; + +import { TestERC20 } from "../../contracts/test/TestERC20.sol"; +import { ArithmeticUtil } from "./utils/ArithmeticUtil.sol"; + +import { OrderParameters } from "./utils/reentrancy/ReentrantStructs.sol"; + +contract FulfillBasicOrderTest is BaseOrderTest { + using ArithmeticUtil for uint128; + + uint256 badIdentifier; + address badToken; + BasicOrderParameters basicOrderParameters; + + struct FuzzInputsCommon { + address zone; + uint256 tokenId; + uint128 paymentAmount; + bytes32 zoneHash; + uint256 salt; + } + struct Context { + ConsiderationInterface consideration; + FuzzInputsCommon args; + uint128 tokenAmount; + } + + modifier validateInputs(Context memory context) { + vm.assume(context.args.paymentAmount > 0); + _; + } + + modifier validateInputsWithAmount(Context memory context) { + vm.assume(context.args.paymentAmount > 0); + vm.assume(context.args.tokenId > 0); + vm.assume(context.tokenAmount > 0); + _; + } + + function test(function(Context memory) external fn, Context memory context) + internal + { + try fn(context) {} catch (bytes memory reason) { + assertPass(reason); + } + } + + function testBasicEthTo721(FuzzInputsCommon memory inputs) + public + validateInputs(Context(consideration, inputs, 0)) + { + addErc721OfferItem(inputs.tokenId); + addEthConsiderationItem(alice, inputs.paymentAmount); + _configureBasicOrderParametersEthTo721(inputs); + + test(this.basicEthTo721, Context(consideration, inputs, 0)); + test(this.basicEthTo721, Context(referenceConsideration, inputs, 0)); + } + + function testBasicErc20To721(FuzzInputsCommon memory inputs) + public + validateInputs(Context(consideration, inputs, 0)) + { + addErc721OfferItem(inputs.tokenId); + addErc20ConsiderationItem(alice, inputs.paymentAmount); + _configureBasicOrderParametersErc20To721(inputs); + + test(this.basicErc20To721, Context(consideration, inputs, 0)); + test(this.basicErc20To721, Context(referenceConsideration, inputs, 0)); + } + + function testBasicEthTo1155( + FuzzInputsCommon memory inputs, + uint128 tokenAmount + ) + public + validateInputsWithAmount(Context(consideration, inputs, tokenAmount)) + { + addErc1155OfferItem(inputs.tokenId, tokenAmount); + addEthConsiderationItem(alice, inputs.paymentAmount); + _configureBasicOrderParametersEthTo1155(inputs, tokenAmount); + + test(this.basicEthTo1155, Context(consideration, inputs, tokenAmount)); + test( + this.basicEthTo1155, + Context(referenceConsideration, inputs, tokenAmount) + ); + } + + function testBasicErc20To1155( + FuzzInputsCommon memory inputs, + uint128 tokenAmount + ) + public + validateInputsWithAmount(Context(consideration, inputs, tokenAmount)) + { + addErc1155OfferItem(inputs.tokenId, tokenAmount); + addErc20ConsiderationItem(alice, inputs.paymentAmount); + _configureBasicOrderParametersErc20To1155(inputs, tokenAmount); + + test( + this.basicErc20To1155, + Context(consideration, inputs, tokenAmount) + ); + test( + this.basicErc20To1155, + Context(referenceConsideration, inputs, tokenAmount) + ); + } + + function testFulfillBasicOrderRevertInvalidAdditionalRecipientsLength( + uint256 fuzzTotalRecipients, + uint256 fuzzAmountToSubtractFromTotalRecipients + ) public { + uint256 totalRecipients = fuzzTotalRecipients % 200; + // Set amount to subtract from total recipients + // to be at most totalRecipients. + uint256 amountToSubtractFromTotalRecipients = totalRecipients > 0 + ? fuzzAmountToSubtractFromTotalRecipients % totalRecipients + : 0; + + // Create basic order + ( + Order memory myOrder, + BasicOrderParameters memory _basicOrderParameters + ) = prepareBasicOrder(1); + + // Add additional recipients + _basicOrderParameters.additionalRecipients = new AdditionalRecipient[]( + totalRecipients + ); + for ( + uint256 i = 0; + i < _basicOrderParameters.additionalRecipients.length; + i++ + ) { + _basicOrderParameters.additionalRecipients[ + i + ] = AdditionalRecipient({ recipient: alice, amount: 1 }); + } + + // Get the calldata that will be passed into fulfillOrder. + bytes memory fulfillOrderCalldata = abi.encodeWithSelector( + consideration.fulfillBasicOrder.selector, + _basicOrderParameters + ); + + _performTestFulfillOrderRevertInvalidArrayLength( + consideration, + myOrder, + fulfillOrderCalldata, + // Order parameters starts at 0x44 relative to the start of the + // order calldata because the order calldata starts with 0x20 bytes + // for order calldata length, 0x04 bytes for selector, and 0x20 + // bytes until the start of order parameters. + 0x44, + 0x200, + _basicOrderParameters.additionalRecipients.length, + amountToSubtractFromTotalRecipients + ); + } + + function testRevertUnusedItemParametersAddressSetOnNativeConsideration( + FuzzInputsCommon memory inputs, + uint128 tokenAmount, + address _badToken + ) + public + validateInputsWithAmount(Context(consideration, inputs, tokenAmount)) + { + vm.assume(_badToken != address(0)); + badToken = _badToken; + + addErc1155OfferItem(inputs.tokenId, tokenAmount); + addEthConsiderationItem(alice, 100); + test( + this.revertUnusedItemParametersAddressSetOnNativeConsideration, + Context(consideration, inputs, tokenAmount) + ); + test( + this.revertUnusedItemParametersAddressSetOnNativeConsideration, + Context(referenceConsideration, inputs, tokenAmount) + ); + } + + function revertUnusedItemParametersAddressSetOnNativeConsideration( + Context memory context + ) external stateless { + test1155_1.mint(alice, context.args.tokenId, context.tokenAmount); + + considerationItems[0].token = badToken; + + _configureOrderParameters( + alice, + address(0), + bytes32(0), + globalSalt++, + false + ); + _configureOrderComponents(context.consideration.getCounter(alice)); + + bytes32 orderHash = context.consideration.getOrderHash( + baseOrderComponents + ); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + BasicOrderParameters + memory _basicOrderParameters = toBasicOrderParameters( + baseOrderComponents, + BasicOrderType.ETH_TO_ERC1155_FULL_OPEN, + signature + ); + + vm.expectRevert(abi.encodeWithSignature("UnusedItemParameters()")); + context.consideration.fulfillBasicOrder{ value: 100 }( + _basicOrderParameters + ); + } + + function testRevertUnusedItemParametersIdentifierSetOnErc20Offer( + FuzzInputsCommon memory inputs, + uint128 tokenAmount, + uint256 _badIdentifier + ) + public + validateInputsWithAmount(Context(consideration, inputs, tokenAmount)) + { + vm.assume(_badIdentifier != 0); + badIdentifier = _badIdentifier; + + addErc20OfferItem(100); + addErc721ConsiderationItem(alice, inputs.tokenId); + + test( + this.revertUnusedItemParametersIdentifierSetOnErc20Offer, + Context(consideration, inputs, tokenAmount) + ); + test( + this.revertUnusedItemParametersIdentifierSetOnErc20Offer, + Context(referenceConsideration, inputs, tokenAmount) + ); + } + + function revertUnusedItemParametersIdentifierSetOnErc20Offer( + Context memory context + ) external stateless { + test721_1.mint(address(this), context.args.tokenId); + + offerItems[0].identifierOrCriteria = 69; + + _configureOrderParameters( + alice, + address(0), + bytes32(0), + globalSalt++, + false + ); + _configureOrderComponents(context.consideration.getCounter(alice)); + + bytes32 orderHash = context.consideration.getOrderHash( + baseOrderComponents + ); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + BasicOrderParameters + memory _basicOrderParameters = toBasicOrderParameters( + baseOrderComponents, + BasicOrderType.ERC721_TO_ERC20_FULL_OPEN, + signature + ); + + vm.expectRevert(abi.encodeWithSignature("UnusedItemParameters()")); + context.consideration.fulfillBasicOrder(_basicOrderParameters); + } + + function testRevertUnusedItemParametersIdentifierSetOnNativeConsideration( + FuzzInputsCommon memory inputs, + uint128 tokenAmount, + uint256 _badIdentifier + ) + public + validateInputsWithAmount(Context(consideration, inputs, tokenAmount)) + { + vm.assume(_badIdentifier != 0); + badIdentifier = _badIdentifier; + + addErc1155OfferItem(inputs.tokenId, tokenAmount); + addEthConsiderationItem(alice, 100); + test( + this.revertUnusedItemParametersIdentifierSetOnNativeConsideration, + Context(consideration, inputs, tokenAmount) + ); + test( + this.revertUnusedItemParametersIdentifierSetOnNativeConsideration, + Context(referenceConsideration, inputs, tokenAmount) + ); + } + + function revertUnusedItemParametersIdentifierSetOnNativeConsideration( + Context memory context + ) external stateless { + test1155_1.mint(alice, context.args.tokenId, context.tokenAmount); + + considerationItems[0].identifierOrCriteria = badIdentifier; + + _configureOrderParameters( + alice, + address(0), + bytes32(0), + globalSalt++, + false + ); + _configureOrderComponents(context.consideration.getCounter(alice)); + + bytes32 orderHash = context.consideration.getOrderHash( + baseOrderComponents + ); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + BasicOrderParameters + memory _basicOrderParameters = toBasicOrderParameters( + baseOrderComponents, + BasicOrderType.ETH_TO_ERC1155_FULL_OPEN, + signature + ); + + vm.expectRevert(abi.encodeWithSignature("UnusedItemParameters()")); + context.consideration.fulfillBasicOrder{ value: 100 }( + _basicOrderParameters + ); + } + + function testRevertUnusedItemParametersAddressAndIdentifierSetOnNativeConsideration( + FuzzInputsCommon memory inputs, + uint128 tokenAmount, + uint256 _badIdentifier, + address _badToken + ) + public + validateInputsWithAmount(Context(consideration, inputs, tokenAmount)) + { + vm.assume(_badIdentifier != 0 || _badToken != address(0)); + badIdentifier = _badIdentifier; + badToken = _badToken; + + addErc1155OfferItem(inputs.tokenId, tokenAmount); + addEthConsiderationItem(alice, 100); + test( + this + .revertUnusedItemParametersAddressAndIdentifierSetOnNativeConsideration, + Context(consideration, inputs, tokenAmount) + ); + test( + this + .revertUnusedItemParametersAddressAndIdentifierSetOnNativeConsideration, + Context(referenceConsideration, inputs, tokenAmount) + ); + } + + function revertUnusedItemParametersAddressAndIdentifierSetOnNativeConsideration( + Context memory context + ) external stateless { + test1155_1.mint(alice, context.args.tokenId, context.tokenAmount); + + considerationItems[0].identifierOrCriteria = badIdentifier; + considerationItems[0].token = badToken; + + _configureOrderParameters( + alice, + address(0), + bytes32(0), + globalSalt++, + false + ); + _configureOrderComponents(context.consideration.getCounter(alice)); + + bytes32 orderHash = context.consideration.getOrderHash( + baseOrderComponents + ); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + BasicOrderParameters + memory _basicOrderParameters = toBasicOrderParameters( + baseOrderComponents, + BasicOrderType.ETH_TO_ERC1155_FULL_OPEN, + signature + ); + + vm.expectRevert(abi.encodeWithSignature("UnusedItemParameters()")); + context.consideration.fulfillBasicOrder{ value: 100 }( + _basicOrderParameters + ); + } + + function testRevertUnusedItemParametersIdentifierSetOnErc20Consideration( + FuzzInputsCommon memory inputs, + uint128 tokenAmount, + uint256 _badIdentifier + ) + public + validateInputsWithAmount(Context(consideration, inputs, tokenAmount)) + { + vm.assume(_badIdentifier != 0); + badIdentifier = _badIdentifier; + + addErc721OfferItem(inputs.tokenId); + addErc20ConsiderationItem(alice, 100); + test( + this.revertUnusedItemParametersIdentifierSetOnErc20Consideration, + Context(consideration, inputs, tokenAmount) + ); + test( + this.revertUnusedItemParametersIdentifierSetOnErc20Consideration, + Context(referenceConsideration, inputs, tokenAmount) + ); + } + + function revertUnusedItemParametersIdentifierSetOnErc20Consideration( + Context memory context + ) external stateless { + test721_1.mint(alice, context.args.tokenId); + + considerationItems[0].identifierOrCriteria = badIdentifier; + + _configureOrderParameters( + alice, + address(0), + bytes32(0), + globalSalt++, + false + ); + _configureOrderComponents(context.consideration.getCounter(alice)); + + bytes32 orderHash = context.consideration.getOrderHash( + baseOrderComponents + ); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + BasicOrderParameters + memory _basicOrderParameters = toBasicOrderParameters( + baseOrderComponents, + BasicOrderType.ERC20_TO_ERC721_FULL_OPEN, + signature + ); + + vm.expectRevert(abi.encodeWithSignature("UnusedItemParameters()")); + context.consideration.fulfillBasicOrder(_basicOrderParameters); + } + + function prepareBasicOrder(uint256 tokenId) + internal + returns ( + Order memory order, + BasicOrderParameters memory _basicOrderParameters + ) + { + (Order memory _order, , ) = _prepareOrder(tokenId, 1); + order = _order; + _basicOrderParameters = toBasicOrderParameters( + _order, + BasicOrderType.ERC20_TO_ERC1155_FULL_OPEN + ); + } + + function basicErc20To1155(Context memory context) external stateless { + test1155_1.mint(alice, context.args.tokenId, context.tokenAmount); + + _configureOrderComponents( + context.args.zone, + context.args.zoneHash, + context.args.salt, + bytes32(0) + ); + uint256 counter = context.consideration.getCounter(alice); + baseOrderComponents.counter = counter; + bytes32 orderHash = context.consideration.getOrderHash( + baseOrderComponents + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + basicOrderParameters.signature = signature; + context.consideration.fulfillBasicOrder(basicOrderParameters); + assertEq( + context.tokenAmount, + test1155_1.balanceOf(address(this), context.args.tokenId) + ); + } + + function basicEthTo1155(Context memory context) external stateless { + test1155_1.mint(alice, context.args.tokenId, context.tokenAmount); + + _configureOrderComponents( + context.args.zone, + context.args.zoneHash, + context.args.salt, + bytes32(0) + ); + uint256 counter = context.consideration.getCounter(alice); + baseOrderComponents.counter = counter; + bytes32 orderHash = context.consideration.getOrderHash( + baseOrderComponents + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + basicOrderParameters.signature = signature; + context.consideration.fulfillBasicOrder{ + value: context.args.paymentAmount + }(basicOrderParameters); + assertEq( + context.tokenAmount, + test1155_1.balanceOf(address(this), context.args.tokenId) + ); + } + + function basicEthTo721(Context memory context) external stateless { + test721_1.mint(alice, context.args.tokenId); + + _configureOrderComponents( + context.args.zone, + context.args.zoneHash, + context.args.salt, + bytes32(0) + ); + uint256 counter = context.consideration.getCounter(alice); + baseOrderComponents.counter = counter; + bytes32 orderHash = context.consideration.getOrderHash( + baseOrderComponents + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + basicOrderParameters.signature = signature; + context.consideration.fulfillBasicOrder{ + value: context.args.paymentAmount + }(basicOrderParameters); + assertEq(address(this), test721_1.ownerOf(context.args.tokenId)); + } + + function basicErc20To721(Context memory context) external stateless { + test721_1.mint(alice, context.args.tokenId); + + _configureOrderComponents( + context.args.zone, + context.args.zoneHash, + context.args.salt, + bytes32(0) + ); + uint256 counter = context.consideration.getCounter(alice); + baseOrderComponents.counter = counter; + bytes32 orderHash = context.consideration.getOrderHash( + baseOrderComponents + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + basicOrderParameters.signature = signature; + context.consideration.fulfillBasicOrder(basicOrderParameters); + assertEq( + context.args.paymentAmount.add(uint128(MAX_INT)), + token1.balanceOf(address(alice)) + ); + assertEq(address(this), test721_1.ownerOf(context.args.tokenId)); + } + + function _configureBasicOrderParametersEthTo721( + FuzzInputsCommon memory args + ) internal { + basicOrderParameters.considerationToken = address(0); + basicOrderParameters.considerationIdentifier = 0; + basicOrderParameters.considerationAmount = args.paymentAmount; + basicOrderParameters.offerer = payable(alice); + basicOrderParameters.zone = args.zone; + basicOrderParameters.offerToken = address(test721_1); + basicOrderParameters.offerIdentifier = args.tokenId; + basicOrderParameters.offerAmount = 1; + basicOrderParameters.basicOrderType = BasicOrderType + .ETH_TO_ERC721_FULL_OPEN; + basicOrderParameters.startTime = block.timestamp; + basicOrderParameters.endTime = block.timestamp + 100; + basicOrderParameters.zoneHash = args.zoneHash; + basicOrderParameters.salt = args.salt; + basicOrderParameters.offererConduitKey = bytes32(0); + basicOrderParameters.fulfillerConduitKey = bytes32(0); + basicOrderParameters.totalOriginalAdditionalRecipients = 0; + // additional recipients should always be empty + // don't do signature; + } + + function _configureBasicOrderParametersEthTo1155( + FuzzInputsCommon memory args, + uint128 amount + ) internal { + basicOrderParameters.considerationToken = address(0); + basicOrderParameters.considerationIdentifier = 0; + basicOrderParameters.considerationAmount = args.paymentAmount; + basicOrderParameters.offerer = payable(alice); + basicOrderParameters.zone = args.zone; + basicOrderParameters.offerToken = address(test1155_1); + basicOrderParameters.offerIdentifier = args.tokenId; + basicOrderParameters.offerAmount = amount; + basicOrderParameters.basicOrderType = BasicOrderType + .ETH_TO_ERC1155_FULL_OPEN; + basicOrderParameters.startTime = block.timestamp; + basicOrderParameters.endTime = block.timestamp + 100; + basicOrderParameters.zoneHash = args.zoneHash; + basicOrderParameters.salt = args.salt; + basicOrderParameters.offererConduitKey = bytes32(0); + basicOrderParameters.fulfillerConduitKey = bytes32(0); + basicOrderParameters.totalOriginalAdditionalRecipients = 0; + // additional recipients should always be empty + // don't do signature; + } + + function _configureBasicOrderParametersErc20To1155( + FuzzInputsCommon memory args, + uint128 amount + ) internal { + _configureBasicOrderParametersEthTo1155(args, amount); + basicOrderParameters.considerationToken = address(token1); + basicOrderParameters.basicOrderType = BasicOrderType + .ERC20_TO_ERC1155_FULL_OPEN; + } + + function _configureBasicOrderParametersErc20To721( + FuzzInputsCommon memory args + ) internal { + _configureBasicOrderParametersEthTo721(args); + basicOrderParameters.considerationToken = address(token1); + basicOrderParameters.basicOrderType = BasicOrderType + .ERC20_TO_ERC721_FULL_OPEN; + } + + function _configureOrderComponents( + address zone, + bytes32 zoneHash, + uint256 salt, + bytes32 conduitKey + ) internal { + baseOrderComponents.offerer = alice; + baseOrderComponents.zone = zone; + baseOrderComponents.offer = offerItems; + baseOrderComponents.consideration = considerationItems; + baseOrderComponents.orderType = OrderType.FULL_OPEN; + baseOrderComponents.startTime = block.timestamp; + baseOrderComponents.endTime = block.timestamp + 100; + baseOrderComponents.zoneHash = zoneHash; + baseOrderComponents.salt = salt; + baseOrderComponents.conduitKey = conduitKey; + // don't set counter + } +} diff --git a/test/foundry/FulfillOrderTest.sol b/test/foundry/FulfillOrderTest.sol deleted file mode 100644 index 4021c168a..000000000 --- a/test/foundry/FulfillOrderTest.sol +++ /dev/null @@ -1,1829 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.13; - -import { OrderType, BasicOrderType, ItemType, Side } from "../../contracts/lib/ConsiderationEnums.sol"; -import { AdditionalRecipient } from "../../contracts/lib/ConsiderationStructs.sol"; -import { ConsiderationInterface } from "../../contracts/interfaces/ConsiderationInterface.sol"; -import { Order, OfferItem, OrderParameters, ConsiderationItem, OrderComponents, BasicOrderParameters } from "../../contracts/lib/ConsiderationStructs.sol"; -import { BaseOrderTest } from "./utils/BaseOrderTest.sol"; -import { TestERC721 } from "../../contracts/test/TestERC721.sol"; -import { TestERC1155 } from "../../contracts/test/TestERC1155.sol"; -import { TestERC20 } from "../../contracts/test/TestERC20.sol"; -import { ProxyRegistry } from "./interfaces/ProxyRegistry.sol"; -import { OwnableDelegateProxy } from "./interfaces/OwnableDelegateProxy.sol"; - -contract FulfillOrderTest is BaseOrderTest { - struct FuzzInputsCommon { - address zone; - uint128 id; - bytes32 zoneHash; - uint256 salt; - uint128[3] paymentAmts; - bool useConduit; - } - - struct Context { - ConsiderationInterface consideration; - FuzzInputsCommon args; - uint256 erc1155amt; - uint128 tipAmt; - uint8 numTips; - } - - function testFulfillOrderEthToErc721(FuzzInputsCommon memory inputs) - public - { - _testFulfillOrderEthToErc721( - Context(referenceConsideration, inputs, 0, 0, 0) - ); - _testFulfillOrderEthToErc721(Context(consideration, inputs, 0, 0, 0)); - } - - function testFulfillOrderEthToErc1155( - FuzzInputsCommon memory inputs, - uint256 tokenAmount - ) public { - _testFulfillOrderEthToErc1155( - Context(referenceConsideration, inputs, tokenAmount, 0, 0) - ); - _testFulfillOrderEthToErc1155( - Context(consideration, inputs, tokenAmount, 0, 0) - ); - } - - function testFulfillOrderEthToErc721WithSingleTip( - FuzzInputsCommon memory inputs, - uint128 tipAmt - ) public { - _testFulfillOrderEthToErc721WithSingleEthTip( - Context(referenceConsideration, inputs, 0, tipAmt, 0) - ); - _testFulfillOrderEthToErc721WithSingleEthTip( - Context(consideration, inputs, 0, tipAmt, 0) - ); - } - - function testFulfillOrderEthToErc1155WithSingleTip( - FuzzInputsCommon memory inputs, - uint256 tokenAmt, - uint128 tipAmt - ) public { - _testFulfillOrderEthToErc1155WithSingleEthTip( - Context(referenceConsideration, inputs, tokenAmt, tipAmt, 0) - ); - _testFulfillOrderEthToErc1155WithSingleEthTip( - Context(consideration, inputs, tokenAmt, tipAmt, 0) - ); - } - - function testFulfillOrderEthToErc721WithMultipleTips( - FuzzInputsCommon memory inputs, - uint8 numTips - ) public { - _testFulfillOrderEthToErc721WithMultipleEthTips( - Context(referenceConsideration, inputs, 0, 0, numTips) - ); - _testFulfillOrderEthToErc721WithMultipleEthTips( - Context(consideration, inputs, 0, 0, numTips) - ); - } - - function testFulfillOrderEthToErc1155WithMultipleTips( - FuzzInputsCommon memory inputs, - uint256 tokenAmt, - uint8 numTips - ) public { - _testFulfillOrderEthToErc1155WithMultipleEthTips( - Context(referenceConsideration, inputs, tokenAmt, 0, numTips) - ); - _testFulfillOrderEthToErc1155WithMultipleEthTips( - Context(consideration, inputs, tokenAmt, 0, numTips) - ); - } - - function testFulfillOrderSingleErc20ToSingleErc1155( - FuzzInputsCommon memory inputs, - uint256 tokenAmt - ) public { - _testFulfillOrderSingleErc20ToSingleErc1155( - Context(referenceConsideration, inputs, tokenAmt, 0, 0) - ); - _testFulfillOrderSingleErc20ToSingleErc1155( - Context(consideration, inputs, tokenAmt, 0, 0) - ); - } - - function testFulfillOrderEthToErc721WithErc721Tips( - FuzzInputsCommon memory inputs, - uint8 numTips - ) public { - _testFulfillOrderEthToErc721WithErc721Tips( - Context(referenceConsideration, inputs, 0, 0, numTips) - ); - _testFulfillOrderEthToErc721WithErc721Tips( - Context(consideration, inputs, 0, 0, numTips) - ); - } - - function testFulfillOrderEthToErc1155WithErc721Tips( - FuzzInputsCommon memory inputs, - uint256 tokenAmt, - uint8 numTips - ) public { - _testFulfillOrderEthToErc1155WithErc721Tips( - Context(referenceConsideration, inputs, tokenAmt, 0, numTips) - ); - _testFulfillOrderEthToErc1155WithErc721Tips( - Context(consideration, inputs, tokenAmt, 0, numTips) - ); - } - - function testFulfillOrderEthToErc721WithErc1155Tips( - FuzzInputsCommon memory inputs, - uint8 numTips - ) public { - _testFulfillOrderEthToErc721WithErc1155Tips( - Context(referenceConsideration, inputs, 0, 0, numTips) - ); - _testFulfillOrderEthToErc721WithErc1155Tips( - Context(consideration, inputs, 0, 0, numTips) - ); - } - - function testFulfillOrderEthToErc1155WithErc1155Tips( - FuzzInputsCommon memory inputs, - uint256 tokenAmt, - uint8 numTips - ) public { - _testFulfillOrderEthToErc1155WithErc1155Tips( - Context(referenceConsideration, inputs, tokenAmt, 0, numTips) - ); - _testFulfillOrderEthToErc1155WithErc1155Tips( - Context(consideration, inputs, tokenAmt, 0, numTips) - ); - } - - function testFulfillOrderEthToErc721WithErc20Tips( - FuzzInputsCommon memory inputs - ) public { - _testFulfillOrderEthToErc721WithErc20Tips( - Context(referenceConsideration, inputs, 0, 0, 0) - ); - _testFulfillOrderEthToErc721WithErc20Tips( - Context(consideration, inputs, 0, 0, 0) - ); - } - - function testFulfillOrderEthToErc1155WithErc20Tips( - FuzzInputsCommon memory inputs, - uint256 tokenAmt, - uint8 numTips - ) public { - _testFulfillOrderEthToErc1155WithErc20Tips( - Context(referenceConsideration, inputs, tokenAmt, 0, numTips) - ); - _testFulfillOrderEthToErc1155WithErc20Tips( - Context(consideration, inputs, tokenAmt, 0, numTips) - ); - } - - function testFulfillOrderEthToErc721FullRestricted( - FuzzInputsCommon memory inputs - ) public { - _testFulfillOrderEthToErc721FullRestricted( - Context(referenceConsideration, inputs, 0, 0, 0) - ); - _testFulfillOrderEthToErc721FullRestricted( - Context(consideration, inputs, 0, 0, 0) - ); - } - - function _testFulfillOrderEthToErc721(Context memory context) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) <= - 2**128 - 1 - ); - bytes32 conduitKey = context.args.useConduit - ? conduitKeyOne - : bytes32(0); - - test721_1.mint(alice, context.args.id); - offerItems.push( - OfferItem( - ItemType.ERC721, - address(test721_1), - context.args.id, - 1, - 1 - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[0]), - uint256(context.args.paymentAmts[0]), - payable(alice) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[1]), - uint256(context.args.paymentAmts[1]), - payable(context.args.zone) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[2]), - uint256(context.args.paymentAmts[2]), - payable(cal) - ) - ); - - OrderComponents memory orderComponents = OrderComponents( - alice, - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - context.consideration.getNonce(alice) - ); - bytes memory signature = signOrder( - context.consideration, - alicePk, - context.consideration.getOrderHash(orderComponents) - ); - OrderParameters memory orderParameters = OrderParameters( - address(alice), - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - considerationItems.length - ); - context.consideration.fulfillOrder{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] - }(Order(orderParameters, signature), conduitKey); - } - - function _testFulfillOrderEthToErc1155(Context memory context) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - vm.assume(context.erc1155amt > 0); - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) <= - 2**128 - 1 - ); - bytes32 conduitKey = context.args.useConduit - ? conduitKeyOne - : bytes32(0); - - test1155_1.mint(alice, context.args.id, context.erc1155amt); - offerItems.push( - OfferItem( - ItemType.ERC1155, - address(test1155_1), - context.args.id, - context.erc1155amt, - context.erc1155amt - ) - ); - - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[0]), - uint256(context.args.paymentAmts[0]), - payable(alice) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[1]), - uint256(context.args.paymentAmts[1]), - payable(context.args.zone) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[2]), - uint256(context.args.paymentAmts[2]), - payable(cal) - ) - ); - - OrderComponents memory orderComponents = OrderComponents( - alice, - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - context.consideration.getNonce(alice) - ); - bytes memory signature = signOrder( - context.consideration, - alicePk, - context.consideration.getOrderHash(orderComponents) - ); - OrderParameters memory orderParameters = OrderParameters( - address(alice), - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - considerationItems.length - ); - context.consideration.fulfillOrder{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] - }(Order(orderParameters, signature), conduitKey); - } - - function _testFulfillOrderSingleErc20ToSingleErc1155(Context memory context) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - vm.assume(context.erc1155amt > 0); - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) <= - 2**128 - 1 - ); - bytes32 conduitKey = context.args.useConduit - ? conduitKeyOne - : bytes32(0); - - test1155_1.mint(alice, context.args.id, context.erc1155amt); - - offerItems.push( - OfferItem( - ItemType.ERC1155, - address(test1155_1), - context.args.id, - context.erc1155amt, - context.erc1155amt - ) - ); - - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[0]), - uint256(context.args.paymentAmts[0]), - payable(alice) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[1]), - uint256(context.args.paymentAmts[1]), - payable(context.args.zone) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[2]), - uint256(context.args.paymentAmts[2]), - payable(cal) - ) - ); - - OrderComponents memory orderComponents = OrderComponents( - alice, - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - context.consideration.getNonce(alice) - ); - bytes memory signature = signOrder( - context.consideration, - alicePk, - context.consideration.getOrderHash(orderComponents) - ); - OrderParameters memory orderParameters = OrderParameters( - address(alice), - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - considerationItems.length - ); - - context.consideration.fulfillOrder{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] - }(Order(orderParameters, signature), conduitKey); - } - - function _testFulfillOrderEthToErc721WithSingleEthTip( - Context memory context - ) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 && - context.tipAmt > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) + - uint256(context.tipAmt) <= - 2**128 - 1 - ); - bytes32 conduitKey = context.args.useConduit - ? conduitKeyOne - : bytes32(0); - - test721_1.mint(alice, context.args.id); - offerItems.push( - OfferItem( - ItemType.ERC721, - address(test721_1), - context.args.id, - 1, - 1 - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[0]), - uint256(context.args.paymentAmts[0]), - payable(alice) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[1]), - uint256(context.args.paymentAmts[1]), - payable(context.args.zone) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[2]), - uint256(context.args.paymentAmts[2]), - payable(cal) - ) - ); - - OrderComponents memory orderComponents = OrderComponents( - alice, - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - context.consideration.getNonce(alice) - ); - bytes memory signature = signOrder( - context.consideration, - alicePk, - context.consideration.getOrderHash(orderComponents) - ); - - // Add tip - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - context.tipAmt, - context.tipAmt, - payable(bob) - ) - ); - - OrderParameters memory orderParameters = OrderParameters( - address(alice), - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - considerationItems.length - 1 - ); - - context.consideration.fulfillOrder{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] + - context.tipAmt - }(Order(orderParameters, signature), conduitKey); - } - - function _testFulfillOrderEthToErc1155WithSingleEthTip( - Context memory context - ) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - vm.assume(context.erc1155amt > 0); - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 && - context.tipAmt > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) + - uint256(context.tipAmt) <= - 2**128 - 1 - ); - bytes32 conduitKey = context.args.useConduit - ? conduitKeyOne - : bytes32(0); - - test1155_1.mint(alice, context.args.id, context.erc1155amt); - - offerItems.push( - OfferItem( - ItemType.ERC1155, - address(test1155_1), - context.args.id, - context.erc1155amt, - context.erc1155amt - ) - ); - - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[0]), - uint256(context.args.paymentAmts[0]), - payable(alice) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[1]), - uint256(context.args.paymentAmts[1]), - payable(context.args.zone) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[2]), - uint256(context.args.paymentAmts[2]), - payable(cal) - ) - ); - - OrderComponents memory orderComponents = OrderComponents( - alice, - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - context.consideration.getNonce(alice) - ); - bytes memory signature = signOrder( - context.consideration, - alicePk, - context.consideration.getOrderHash(orderComponents) - ); - - // Add tip - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - context.tipAmt, - context.tipAmt, - payable(bob) - ) - ); - - OrderParameters memory orderParameters = OrderParameters( - address(alice), - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - considerationItems.length - 1 - ); - - context.consideration.fulfillOrder{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] + - context.tipAmt - }(Order(orderParameters, signature), conduitKey); - } - - function _testFulfillOrderEthToErc721WithMultipleEthTips( - Context memory context - ) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - context.numTips = (context.numTips % 64) + 1; - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) + - uint256(context.numTips) * - ((1 + context.numTips) / 2) <= // avg of tip amounts from 1 to numberOfTips eth - 2**128 - 1 - ); - - bytes32 conduitKey = context.args.useConduit - ? conduitKeyOne - : bytes32(0); - - test721_1.mint(alice, context.args.id); - offerItems.push( - OfferItem( - ItemType.ERC721, - address(test721_1), - context.args.id, - 1, - 1 - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[0]), - uint256(context.args.paymentAmts[0]), - payable(alice) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[1]), - uint256(context.args.paymentAmts[1]), - payable(context.args.zone) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[2]), - uint256(context.args.paymentAmts[2]), - payable(cal) - ) - ); - - OrderComponents memory orderComponents = OrderComponents( - alice, - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - context.consideration.getNonce(alice) - ); - bytes memory signature = signOrder( - context.consideration, - alicePk, - context.consideration.getOrderHash(orderComponents) - ); - - uint128 sumOfTips; - for (uint128 i = 1; i < context.numTips + 1; i++) { - uint256 tipPk = 0xb0b + i; - address tipAddr = vm.addr(tipPk); - sumOfTips += i; - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - i, - i, - payable(tipAddr) - ) - ); - } - - OrderParameters memory orderParameters = OrderParameters( - address(alice), - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - considerationItems.length - context.numTips - ); - - context.consideration.fulfillOrder{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] + - sumOfTips - }(Order(orderParameters, signature), conduitKey); - } - - function _testFulfillOrderEthToErc1155WithMultipleEthTips( - Context memory context - ) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - context.numTips = (context.numTips % 64) + 1; - vm.assume(context.erc1155amt > 0); - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) + - uint256(context.numTips) * - ((1 + context.numTips) / 2) <= // avg of tip amounts from 1 to numberOfTips eth - 2**128 - 1 - ); - bytes32 conduitKey = context.args.useConduit - ? conduitKeyOne - : bytes32(0); - - test1155_1.mint(alice, context.args.id, context.erc1155amt); - - offerItems.push( - OfferItem( - ItemType.ERC1155, - address(test1155_1), - context.args.id, - context.erc1155amt, - context.erc1155amt - ) - ); - - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[0]), - uint256(context.args.paymentAmts[0]), - payable(alice) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[1]), - uint256(context.args.paymentAmts[1]), - payable(context.args.zone) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[2]), - uint256(context.args.paymentAmts[2]), - payable(cal) - ) - ); - - OrderComponents memory orderComponents = OrderComponents( - alice, - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - context.consideration.getNonce(alice) - ); - bytes memory signature = signOrder( - context.consideration, - alicePk, - context.consideration.getOrderHash(orderComponents) - ); - - uint128 sumOfTips; - // push tip of amount i eth to considerationitems - for (uint128 i = 1; i < context.numTips + 1; i++) { - uint256 tipPk = 0xb0b + i; - address tipAddr = vm.addr(tipPk); - sumOfTips += i; - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - i, - i, - payable(tipAddr) - ) - ); - } - - OrderParameters memory orderParameters = OrderParameters( - address(alice), - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - considerationItems.length - context.numTips - ); - - context.consideration.fulfillOrder{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] + - sumOfTips - }(Order(orderParameters, signature), conduitKey); - } - - function _testFulfillOrderEthToErc721WithErc721Tips(Context memory context) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - context.numTips = (context.numTips % 64) + 1; - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) <= - 2**128 - 1 - ); - bytes32 conduitKey = context.args.useConduit - ? conduitKeyOne - : bytes32(0); - - test721_1.mint(alice, context.args.id); - offerItems.push( - OfferItem( - ItemType.ERC721, - address(test721_1), - context.args.id, - 1, - 1 - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[0]), - uint256(context.args.paymentAmts[0]), - payable(alice) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[1]), - uint256(context.args.paymentAmts[1]), - payable(context.args.zone) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[2]), - uint256(context.args.paymentAmts[2]), - payable(cal) - ) - ); - - OrderComponents memory orderComponents = OrderComponents( - alice, - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - context.consideration.getNonce(alice) - ); - bytes memory signature = signOrder( - context.consideration, - alicePk, - context.consideration.getOrderHash(orderComponents) - ); - - // mint erc721s to the test contract and push tips to considerationItems - for (uint128 i = 1; i < context.numTips + 1; i++) { - uint256 tipPk = 0xb0b + i; - address tipAddr = vm.addr(tipPk); - test721_2.mint(address(this), i); // mint test721_2 tokens to avoid collision with fuzzed test721_1 tokenId - considerationItems.push( - ConsiderationItem( - ItemType.ERC721, - address(test721_2), - i, - 1, - 1, - payable(tipAddr) - ) - ); - } - - OrderParameters memory orderParameters = OrderParameters( - address(alice), - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - considerationItems.length - context.numTips - ); - - context.consideration.fulfillOrder{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] - }(Order(orderParameters, signature), conduitKey); - } - - function _testFulfillOrderEthToErc1155WithErc721Tips(Context memory context) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - context.numTips = (context.numTips % 64) + 1; - vm.assume(context.erc1155amt > 0); - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) <= - 2**128 - 1 - ); - bytes32 conduitKey = context.args.useConduit - ? conduitKeyOne - : bytes32(0); - - test1155_1.mint(alice, context.args.id, context.erc1155amt); - offerItems.push( - OfferItem( - ItemType.ERC1155, - address(test1155_1), - context.args.id, - context.erc1155amt, - context.erc1155amt - ) - ); - - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[0]), - uint256(context.args.paymentAmts[0]), - payable(alice) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[1]), - uint256(context.args.paymentAmts[1]), - payable(context.args.zone) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[2]), - uint256(context.args.paymentAmts[2]), - payable(cal) - ) - ); - - OrderComponents memory orderComponents = OrderComponents( - alice, - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - context.consideration.getNonce(alice) - ); - bytes memory signature = signOrder( - context.consideration, - alicePk, - context.consideration.getOrderHash(orderComponents) - ); - - // mint erc721s to the test contract and push tips to considerationItems - for (uint128 i = 1; i < context.numTips + 1; i++) { - uint256 tipPk = 0xb0b + i; - address tipAddr = vm.addr(tipPk); - test721_2.mint(address(this), i); // mint test721_2 tokens to avoid collision with fuzzed test721_1 tokenId - considerationItems.push( - ConsiderationItem( - ItemType.ERC721, - address(test721_2), - i, - 1, - 1, - payable(tipAddr) - ) - ); - } - - OrderParameters memory orderParameters = OrderParameters( - address(alice), - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - considerationItems.length - context.numTips - ); - - context.consideration.fulfillOrder{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] - }(Order(orderParameters, signature), conduitKey); - } - - function _testFulfillOrderEthToErc721WithErc1155Tips(Context memory context) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - context.numTips = (context.numTips % 64) + 1; - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) <= - 2**128 - 1 - ); - bytes32 conduitKey = context.args.useConduit - ? conduitKeyOne - : bytes32(0); - - test721_1.mint(alice, context.args.id); - - offerItems.push( - OfferItem( - ItemType.ERC721, - address(test721_1), - context.args.id, - 1, - 1 - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[0]), - uint256(context.args.paymentAmts[0]), - payable(alice) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[1]), - uint256(context.args.paymentAmts[1]), - payable(context.args.zone) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[2]), - uint256(context.args.paymentAmts[2]), - payable(cal) - ) - ); - - OrderComponents memory orderComponents = OrderComponents( - alice, - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - context.consideration.getNonce(alice) - ); - bytes memory signature = signOrder( - context.consideration, - alicePk, - context.consideration.getOrderHash(orderComponents) - ); - - for (uint256 i = 1; i < context.numTips + uint256(1); i++) { - uint256 tipPk = 0xb0b + i; - address tipAddr = vm.addr(tipPk); - test1155_1.mint(address(this), context.args.id + uint256(i), i); - considerationItems.push( - ConsiderationItem( - ItemType.ERC1155, - address(test1155_1), - context.args.id + uint256(i), - i, - i, - payable(tipAddr) - ) - ); - } - - OrderParameters memory orderParameters = OrderParameters( - address(alice), - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - considerationItems.length - context.numTips - ); - - context.consideration.fulfillOrder{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] - }(Order(orderParameters, signature), conduitKey); - } - - function _testFulfillOrderEthToErc1155WithErc1155Tips( - Context memory context - ) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - context.numTips = (context.numTips % 64) + 1; - vm.assume(context.erc1155amt > 0); - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) <= - 2**128 - 1 - ); - - bytes32 conduitKey = context.args.useConduit - ? conduitKeyOne - : bytes32(0); - - test1155_1.mint(alice, context.args.id, context.erc1155amt); - offerItems.push( - OfferItem( - ItemType.ERC1155, - address(test1155_1), - context.args.id, - context.erc1155amt, - context.erc1155amt - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[0]), - uint256(context.args.paymentAmts[0]), - payable(alice) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[1]), - uint256(context.args.paymentAmts[1]), - payable(context.args.zone) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[2]), - uint256(context.args.paymentAmts[2]), - payable(cal) - ) - ); - - OrderComponents memory orderComponents = OrderComponents( - alice, - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - context.consideration.getNonce(alice) - ); - bytes memory signature = signOrder( - context.consideration, - alicePk, - context.consideration.getOrderHash(orderComponents) - ); - - for (uint256 i = 1; i < context.numTips + uint256(1); i++) { - uint256 tipPk = 0xb0b + i; - address tipAddr = vm.addr(tipPk); - test1155_1.mint(address(this), context.args.id + uint256(i), i); - considerationItems.push( - ConsiderationItem( - ItemType.ERC1155, - address(test1155_1), - context.args.id + uint256(i), - i, - i, - payable(tipAddr) - ) - ); - } - - OrderParameters memory orderParameters = OrderParameters( - address(alice), - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - considerationItems.length - context.numTips - ); - - context.consideration.fulfillOrder{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] - }(Order(orderParameters, signature), conduitKey); - } - - function _testFulfillOrderEthToErc721WithErc20Tips(Context memory context) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) <= - 2**128 - 1 - ); - bytes32 conduitKey = context.args.useConduit - ? conduitKeyOne - : bytes32(0); - - test721_1.mint(alice, context.args.id); - - offerItems.push( - OfferItem( - ItemType.ERC721, - address(test721_1), - context.args.id, - 1, - 1 - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[0]), - uint256(context.args.paymentAmts[0]), - payable(alice) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[1]), - uint256(context.args.paymentAmts[1]), - payable(context.args.zone) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[2]), - uint256(context.args.paymentAmts[2]), - payable(cal) - ) - ); - - OrderComponents memory orderComponents = OrderComponents( - alice, - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - context.consideration.getNonce(alice) - ); - bytes memory signature = signOrder( - context.consideration, - alicePk, - context.consideration.getOrderHash(orderComponents) - ); - - for (uint256 i = 1; i < context.numTips + uint256(1); i++) { - uint256 tipPk = i; - address tipAddr = vm.addr(tipPk); - considerationItems.push( - ConsiderationItem( - ItemType.ERC20, - address(token1), - 0, // ignored for ERC20 - i, - i, - payable(tipAddr) - ) - ); - } - - OrderParameters memory orderParameters = OrderParameters( - address(alice), - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - considerationItems.length - context.numTips - ); - - context.consideration.fulfillOrder{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] - }(Order(orderParameters, signature), conduitKey); - } - - function _testFulfillOrderEthToErc1155WithErc20Tips(Context memory context) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - context.numTips = (context.numTips % 64) + 1; - vm.assume(context.erc1155amt > 0); - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) <= - 2**128 - 1 - ); - - bytes32 conduitKey = context.args.useConduit - ? conduitKeyOne - : bytes32(0); - - test1155_1.mint(alice, context.args.id, context.erc1155amt); - offerItems.push( - OfferItem( - ItemType.ERC1155, - address(test1155_1), - context.args.id, - context.erc1155amt, - context.erc1155amt - ) - ); - - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[0]), - uint256(context.args.paymentAmts[0]), - payable(alice) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[1]), - uint256(context.args.paymentAmts[1]), - payable(context.args.zone) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[2]), - uint256(context.args.paymentAmts[2]), - payable(cal) - ) - ); - - OrderComponents memory orderComponents = OrderComponents( - alice, - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - context.consideration.getNonce(alice) - ); - bytes memory signature = signOrder( - context.consideration, - alicePk, - context.consideration.getOrderHash(orderComponents) - ); - - for (uint256 i = 1; i < context.numTips + uint256(1); i++) { - uint256 tipPk = i; - address tipAddr = vm.addr(tipPk); - considerationItems.push( - ConsiderationItem( - ItemType.ERC20, - address(token1), - 0, // ignored for ERC20 - i, - i, - payable(tipAddr) - ) - ); - } - - OrderParameters memory orderParameters = OrderParameters( - address(alice), - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - considerationItems.length - context.numTips - ); - - context.consideration.fulfillOrder{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] - }(Order(orderParameters, signature), conduitKey); - } - - function _testFulfillOrderEthToErc721FullRestricted(Context memory context) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) <= - 2**128 - 1 - ); - bytes32 conduitKey = context.args.useConduit - ? conduitKeyOne - : bytes32(0); - - test721_1.mint(alice, context.args.id); - offerItems.push( - OfferItem( - ItemType.ERC721, - address(test721_1), - context.args.id, - 1, - 1 - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[0]), - uint256(context.args.paymentAmts[0]), - payable(alice) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[1]), - uint256(context.args.paymentAmts[1]), - payable(context.args.zone) - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(context.args.paymentAmts[2]), - uint256(context.args.paymentAmts[2]), - payable(cal) - ) - ); - - OrderComponents memory orderComponents = OrderComponents( - alice, - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_RESTRICTED, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - context.consideration.getNonce(alice) - ); - bytes memory signature = signOrder( - context.consideration, - alicePk, - context.consideration.getOrderHash(orderComponents) - ); - - OrderParameters memory orderParameters = OrderParameters( - address(alice), - context.args.zone, - offerItems, - considerationItems, - OrderType.FULL_RESTRICTED, - block.timestamp, - block.timestamp + 1, - context.args.zoneHash, - context.args.salt, - conduitKey, - considerationItems.length - ); - vm.prank(alice); - context.consideration.fulfillOrder{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] - }(Order(orderParameters, signature), conduitKey); - } -} diff --git a/test/foundry/FulfillOrderTest.t.sol b/test/foundry/FulfillOrderTest.t.sol new file mode 100644 index 000000000..3d05a820a --- /dev/null +++ b/test/foundry/FulfillOrderTest.t.sol @@ -0,0 +1,2544 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.13; + +import { OrderType, BasicOrderType, ItemType, Side } from "../../contracts/lib/ConsiderationEnums.sol"; +import { AdditionalRecipient } from "../../contracts/lib/ConsiderationStructs.sol"; +import { ConsiderationInterface } from "../../contracts/interfaces/ConsiderationInterface.sol"; +import { Order, OfferItem, OrderParameters, ConsiderationItem, OrderComponents, BasicOrderParameters } from "../../contracts/lib/ConsiderationStructs.sol"; +import { BaseOrderTest } from "./utils/BaseOrderTest.sol"; +import { TestERC721 } from "../../contracts/test/TestERC721.sol"; +import { TestERC1155 } from "../../contracts/test/TestERC1155.sol"; +import { TestERC20 } from "../../contracts/test/TestERC20.sol"; +import { ProxyRegistry } from "./interfaces/ProxyRegistry.sol"; +import { OwnableDelegateProxy } from "./interfaces/OwnableDelegateProxy.sol"; +import { ArithmeticUtil } from "./utils/ArithmeticUtil.sol"; + +contract FulfillOrderTest is BaseOrderTest { + using ArithmeticUtil for uint256; + using ArithmeticUtil for uint128; + using ArithmeticUtil for uint120; + using ArithmeticUtil for uint8; + + FuzzInputsCommon empty; + bytes signature1271; + + uint256 badIdentifier; + address badToken; + struct FuzzInputsCommon { + address zone; + uint128 id; + bytes32 zoneHash; + uint256 salt; + uint128[3] paymentAmts; + bool useConduit; + uint120 startAmount; + uint120 endAmount; + uint16 warpAmount; + } + + struct Context { + ConsiderationInterface consideration; + FuzzInputsCommon args; + uint256 erc1155Amt; + uint128 tipAmt; + uint8 numTips; + } + + function test(function(Context memory) external fn, Context memory context) + internal + { + try fn(context) {} catch (bytes memory reason) { + assertPass(reason); + } + } + + modifier validateInputs(FuzzInputsCommon memory args) { + vm.assume( + args.paymentAmts[0] > 0 && + args.paymentAmts[1] > 0 && + args.paymentAmts[2] > 0 + ); + vm.assume( + args.paymentAmts[0].add(args.paymentAmts[1]).add( + args.paymentAmts[2] + ) <= uint128(MAX_INT) + ); + _; + } + + modifier validateInputsWithTip( + FuzzInputsCommon memory args, + uint256 tipAmt + ) { + vm.assume( + args.paymentAmts[0] > 0 && + args.paymentAmts[1] > 0 && + args.paymentAmts[2] > 0 && + tipAmt > 0 + ); + vm.assume( + args + .paymentAmts[0] + .add(args.paymentAmts[1]) + .add(args.paymentAmts[2]) + .add(tipAmt) <= uint128(MAX_INT) + ); + _; + } + + modifier validateInputsWithMultipleTips( + FuzzInputsCommon memory args, + uint256 numTips + ) { + { + numTips = (numTips % 64) + 1; + vm.assume( + args.paymentAmts[0] > 0 && + args.paymentAmts[1] > 0 && + args.paymentAmts[2] > 0 + ); + vm.assume( + args + .paymentAmts[0] + .add(args.paymentAmts[1]) + .add(args.paymentAmts[2]) + .add(numTips.mul(numTips + 1).div(2)) <= uint128(MAX_INT) + ); + } + _; + } + + function testNoNativeOffers(uint8[8] memory itemTypes) public { + uint256 tokenId; + for (uint256 i; i < 8; i++) { + ItemType itemType = ItemType(itemTypes[i] % 4); + if (itemType == ItemType.NATIVE) { + addEthOfferItem(1); + } else if (itemType == ItemType.ERC20) { + addErc20OfferItem(1); + } else if (itemType == ItemType.ERC1155) { + test1155_1.mint(alice, tokenId, 1); + addErc1155OfferItem(tokenId, 1); + } else { + test721_1.mint(alice, tokenId); + addErc721OfferItem(tokenId); + } + tokenId++; + } + addEthOfferItem(1); + + addEthConsiderationItem(alice, 1); + + test(this.noNativeOfferItems, Context(consideration, empty, 0, 0, 0)); + test( + this.noNativeOfferItems, + Context(referenceConsideration, empty, 0, 0, 0) + ); + } + + function noNativeOfferItems(Context memory context) external stateless { + configureOrderParameters(alice); + uint256 counter = context.consideration.getCounter(alice); + _configureOrderComponents(counter); + bytes32 orderHash = context.consideration.getOrderHash( + baseOrderComponents + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + vm.expectRevert(abi.encodeWithSignature("InvalidNativeOfferItem()")); + + context.consideration.fulfillOrder( + Order(baseOrderParameters, signature), + bytes32(0) + ); + } + + function testNullAddressSpendReverts() public { + // mint token to null address + preapproved721.mint(address(0), 1); + // mint erc token to test address + token1.mint(address(this), 1); + // offer burnt erc721 + addErc721OfferItem(address(preapproved721), 1); + // consider erc20 to null address + addErc20ConsiderationItem(payable(0), 1); + // configure baseOrderParameters with null address as offerer + configureOrderParameters(address(0)); + test( + this.nullAddressSpendReverts, + Context(referenceConsideration, empty, 0, 0, 0) + ); + test( + this.nullAddressSpendReverts, + Context(consideration, empty, 0, 0, 0) + ); + } + + function nullAddressSpendReverts(Context memory context) + external + stateless + { + // create a bad signature + bytes memory signature = abi.encodePacked( + bytes32(0), + bytes32(0), + bytes1(uint8(27)) + ); + // test that signature is recognized as invalid even though signer recovered is null address + vm.expectRevert(abi.encodeWithSignature("InvalidSigner()")); + + context.consideration.fulfillOrder( + Order(baseOrderParameters, signature), + bytes32(0) + ); + } + + function testFulfillAscendingDescendingOffer(FuzzInputsCommon memory inputs) + public + validateInputs(inputs) + onlyPayable(inputs.zone) + { + vm.assume(inputs.startAmount > 0 && inputs.endAmount > 0); + inputs.warpAmount %= 1000; + test( + this.fulfillAscendingDescendingOffer, + Context(referenceConsideration, inputs, 0, 0, 0) + ); + test( + this.fulfillAscendingDescendingOffer, + Context(consideration, inputs, 0, 0, 0) + ); + } + + function fulfillAscendingDescendingOffer(Context memory context) + external + stateless + { + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + token1.mint( + alice, + ( + context.args.endAmount > context.args.startAmount + ? context.args.endAmount + : context.args.startAmount + ).mul(1000) + ); + addErc20OfferItem( + context.args.startAmount.mul(1000), + context.args.endAmount.mul(1000) + ); + addEthConsiderationItem(alice, 1000); + OrderParameters memory orderParameters = OrderParameters( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1000, + bytes32(0), + context.args.salt, + conduitKey, + 1 + ); + + OrderComponents memory orderComponents = getOrderComponents( + orderParameters, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + uint256 startTime = block.timestamp; + vm.warp(block.timestamp + context.args.warpAmount); + uint256 expectedAmount = _locateCurrentAmount( + context.args.startAmount.mul(1000), + context.args.endAmount.mul(1000), + startTime, + startTime + 1000, + false // don't round up offers + ); + vm.expectEmit(true, true, true, false, address(token1)); + emit Transfer(alice, address(this), expectedAmount); + context.consideration.fulfillOrder{ value: 1000 }( + Order(orderParameters, signature), + conduitKey + ); + } + + function testFulfillAscendingDescendingConsideration( + FuzzInputsCommon memory inputs, + uint256 erc1155Amt + ) public validateInputs(inputs) onlyPayable(inputs.zone) { + vm.assume(inputs.startAmount > 0 && inputs.endAmount > 0); + vm.assume(erc1155Amt > 0); + test( + this.fulfillAscendingDescendingConsideration, + Context(referenceConsideration, inputs, erc1155Amt, 0, 0) + ); + test( + this.fulfillAscendingDescendingConsideration, + Context(consideration, inputs, erc1155Amt, 0, 0) + ); + } + + function fulfillAscendingDescendingConsideration(Context memory context) + external + stateless + { + context.args.warpAmount %= 1000; + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test1155_1.mint(alice, context.args.id, context.erc1155Amt); + addErc1155OfferItem(context.args.id, context.erc1155Amt); + + addErc20ConsiderationItem( + alice, + context.args.startAmount.mul(1000), + context.args.endAmount.mul(1000) + ); + OrderParameters memory orderParameters = OrderParameters( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1000, + bytes32(0), + context.args.salt, + conduitKey, + 1 + ); + delete offerItems; + delete considerationItems; + + OrderComponents memory orderComponents = getOrderComponents( + orderParameters, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + uint256 startTime = block.timestamp; + vm.warp(block.timestamp + context.args.warpAmount); + uint256 expectedAmount = _locateCurrentAmount( + context.args.startAmount.mul(1000), + context.args.endAmount.mul(1000), + startTime, + startTime + 1000, + true // round up considerations + ); + token1.mint(address(this), expectedAmount); + vm.expectEmit(true, true, true, false, address(token1)); + emit Transfer(address(this), address(alice), expectedAmount); + context.consideration.fulfillOrder( + Order(orderParameters, signature), + conduitKey + ); + } + + function testFulfillOrderEthToErc721(FuzzInputsCommon memory inputs) + public + validateInputs(inputs) + onlyPayable(inputs.zone) + { + test( + this.fulfillOrderEthToErc721, + Context(referenceConsideration, inputs, 0, 0, 0) + ); + test( + this.fulfillOrderEthToErc721, + Context(consideration, inputs, 0, 0, 0) + ); + } + + function testFulfillOrderEthToErc1155( + FuzzInputsCommon memory inputs, + uint256 tokenAmount + ) public validateInputs(inputs) onlyPayable(inputs.zone) { + vm.assume(tokenAmount > 0); + test( + this.fulfillOrderEthToErc1155, + Context(referenceConsideration, inputs, tokenAmount, 0, 0) + ); + test( + this.fulfillOrderEthToErc1155, + Context(consideration, inputs, tokenAmount, 0, 0) + ); + } + + function testFulfillOrderEthToErc721WithSingleTip( + FuzzInputsCommon memory inputs, + uint128 tipAmt + ) public onlyPayable(inputs.zone) { + vm.assume( + inputs.paymentAmts[0] > 0 && + inputs.paymentAmts[1] > 0 && + inputs.paymentAmts[2] > 0 && + tipAmt > 0 + ); + vm.assume( + inputs.paymentAmts[0].add(inputs.paymentAmts[1]).add( + inputs.paymentAmts[2].add(tipAmt) + ) <= uint128(MAX_INT) + ); + test( + this.fulfillOrderEthToErc721WithSingleEthTip, + Context(referenceConsideration, inputs, 0, tipAmt, 0) + ); + test( + this.fulfillOrderEthToErc721WithSingleEthTip, + Context(consideration, inputs, 0, tipAmt, 0) + ); + } + + function testFulfillOrderEthToErc1155WithSingleTip( + FuzzInputsCommon memory inputs, + uint256 tokenAmt, + uint128 tipAmt + ) public onlyPayable(inputs.zone) { + vm.assume(tokenAmt > 0); + vm.assume( + inputs.paymentAmts[0] > 0 && + inputs.paymentAmts[1] > 0 && + inputs.paymentAmts[2] > 0 && + tipAmt > 0 + ); + vm.assume( + inputs.paymentAmts[0].add(inputs.paymentAmts[1]).add( + inputs.paymentAmts[2].add(tipAmt) + ) <= uint128(MAX_INT) + ); + test( + this.fulfillOrderEthToErc1155WithSingleEthTip, + Context(referenceConsideration, inputs, tokenAmt, tipAmt, 0) + ); + test( + this.fulfillOrderEthToErc1155WithSingleEthTip, + Context(consideration, inputs, tokenAmt, tipAmt, 0) + ); + } + + function testFulfillOrderEthToErc721WithMultipleTips( + FuzzInputsCommon memory inputs, + uint8 numTips + ) + public + validateInputsWithMultipleTips(inputs, numTips) + onlyPayable(inputs.zone) + { + test( + this.fulfillOrderEthToErc721WithMultipleEthTips, + Context(referenceConsideration, inputs, 0, 0, numTips) + ); + test( + this.fulfillOrderEthToErc721WithMultipleEthTips, + Context(consideration, inputs, 0, 0, numTips) + ); + } + + function testFulfillOrderEthToErc1155WithMultipleTips( + FuzzInputsCommon memory inputs, + uint256 tokenAmt, + uint8 numTips + ) + public + validateInputsWithMultipleTips(inputs, numTips) + onlyPayable(inputs.zone) + { + vm.assume(tokenAmt > 0); + + test( + this.fulfillOrderEthToErc1155WithMultipleEthTips, + Context(referenceConsideration, inputs, tokenAmt, 0, numTips) + ); + test( + this.fulfillOrderEthToErc1155WithMultipleEthTips, + Context(consideration, inputs, tokenAmt, 0, numTips) + ); + } + + function testFulfillOrderSingleErc20ToSingleErc1155( + FuzzInputsCommon memory inputs, + uint256 tokenAmt + ) public validateInputs(inputs) onlyPayable(inputs.zone) { + vm.assume(tokenAmt > 0); + test( + this.fulfillOrderSingleErc20ToSingleErc1155, + Context(referenceConsideration, inputs, tokenAmt, 0, 0) + ); + test( + this.fulfillOrderSingleErc20ToSingleErc1155, + Context(consideration, inputs, tokenAmt, 0, 0) + ); + } + + function testFulfillOrderEthToErc721WithErc721Tips( + FuzzInputsCommon memory inputs, + uint8 numTips + ) + public + validateInputsWithMultipleTips(inputs, numTips) + onlyPayable(inputs.zone) + { + vm.assume(numTips > 0); + test( + this.fulfillOrderEthToErc721WithErc721Tips, + Context(referenceConsideration, inputs, 0, 0, numTips) + ); + test( + this.fulfillOrderEthToErc721WithErc721Tips, + Context(consideration, inputs, 0, 0, numTips) + ); + } + + function testFulfillOrderEthToErc1155WithErc721Tips( + FuzzInputsCommon memory inputs, + uint256 tokenAmt, + uint8 numTips + ) + public + validateInputsWithMultipleTips(inputs, numTips) + onlyPayable(inputs.zone) + { + vm.assume(tokenAmt > 0); + test( + this.fulfillOrderEthToErc1155WithErc721Tips, + Context(referenceConsideration, inputs, tokenAmt, 0, numTips) + ); + test( + this.fulfillOrderEthToErc1155WithErc721Tips, + Context(consideration, inputs, tokenAmt, 0, numTips) + ); + } + + function testFulfillOrderEthToErc721WithErc1155Tips( + FuzzInputsCommon memory inputs, + uint8 numTips + ) public validateInputs(inputs) onlyPayable(inputs.zone) { + test( + this.fulfillOrderEthToErc721WithErc1155Tips, + Context(referenceConsideration, inputs, 0, 0, numTips) + ); + test( + this.fulfillOrderEthToErc721WithErc1155Tips, + Context(consideration, inputs, 0, 0, numTips) + ); + } + + function testFulfillOrderEthToErc1155WithErc1155Tips( + FuzzInputsCommon memory inputs, + uint256 tokenAmt, + uint8 numTips + ) public validateInputs(inputs) onlyPayable(inputs.zone) { + vm.assume(tokenAmt > 0); + test( + this.fulfillOrderEthToErc1155WithErc1155Tips, + Context(referenceConsideration, inputs, tokenAmt, 0, numTips) + ); + test( + this.fulfillOrderEthToErc1155WithErc1155Tips, + Context(consideration, inputs, tokenAmt, 0, numTips) + ); + } + + function testFulfillOrderEthToErc721WithErc20Tips( + FuzzInputsCommon memory inputs + ) public validateInputs(inputs) onlyPayable(inputs.zone) { + test( + this.fulfillOrderEthToErc721WithErc20Tips, + Context(referenceConsideration, inputs, 0, 0, 0) + ); + test( + this.fulfillOrderEthToErc721WithErc20Tips, + Context(consideration, inputs, 0, 0, 0) + ); + } + + function testFulfillOrderEthToErc1155WithErc20Tips( + FuzzInputsCommon memory inputs, + uint256 tokenAmt, + uint8 numTips + ) + public + validateInputsWithMultipleTips(inputs, numTips) + onlyPayable(inputs.zone) + { + vm.assume(tokenAmt > 0); + test( + this.fulfillOrderEthToErc1155WithErc20Tips, + Context(referenceConsideration, inputs, tokenAmt, 0, numTips) + ); + test( + this.fulfillOrderEthToErc1155WithErc20Tips, + Context(consideration, inputs, tokenAmt, 0, numTips) + ); + } + + function testFulfillOrderEthToErc721FullRestricted( + FuzzInputsCommon memory inputs + ) public validateInputs(inputs) onlyPayable(inputs.zone) { + test( + this.fulfillOrderEthToErc721FullRestricted, + Context(referenceConsideration, inputs, 0, 0, 0) + ); + test( + this.fulfillOrderEthToErc721FullRestricted, + Context(consideration, inputs, 0, 0, 0) + ); + } + + function testFulfillOrder64And65Byte1271Signatures() public { + signature1271 = abi.encodePacked(bytes32(0), bytes32(0), bytes1(0)); + assertEq(signature1271.length, 65); + test( + this.fulfillOrder64And65Byte1271Signatures, + Context(referenceConsideration, empty, 0, 0, 0) + ); + test( + this.fulfillOrder64And65Byte1271Signatures, + Context(consideration, empty, 0, 0, 0) + ); + signature1271 = abi.encodePacked(bytes32(0), bytes32(0)); + assertEq(signature1271.length, 64); + test( + this.fulfillOrder64And65Byte1271Signatures, + Context(referenceConsideration, empty, 0, 0, 0) + ); + test( + this.fulfillOrder64And65Byte1271Signatures, + Context(consideration, empty, 0, 0, 0) + ); + } + + function fulfillOrder64And65Byte1271Signatures(Context memory context) + external + stateless + { + test1155_1.mint(address(this), 1, 1); + addErc1155OfferItem(1, 1); + addEthConsiderationItem(payable(this), 1); + + _configureOrderParameters( + address(this), + address(0), + bytes32(0), + globalSalt++, + false + ); + + Order memory order = Order(baseOrderParameters, signature1271); + vm.prank(bob); + context.consideration.fulfillOrder{ value: 1 }(order, bytes32(0)); + } + + function testFulfillOrder2098() public { + test( + this.fulfillOrder2098, + Context(referenceConsideration, empty, 0, 0, 0) + ); + test(this.fulfillOrder2098, Context(consideration, empty, 0, 0, 0)); + } + + function fulfillOrder2098(Context memory context) external stateless { + test1155_1.mint(bob, 1, 1); + addErc1155OfferItem(1, 1); + addEthConsiderationItem(payable(bob), 1); + + _configureOrderParameters( + bob, + address(0), + bytes32(0), + globalSalt++, + false + ); + _configureOrderComponents(context.consideration.getCounter(bob)); + bytes32 orderHash = context.consideration.getOrderHash( + baseOrderComponents + ); + bytes memory signature = signOrder2098( + context.consideration, + bobPk, + orderHash + ); + + Order memory order = Order(baseOrderParameters, signature); + + context.consideration.fulfillOrder{ value: 1 }(order, bytes32(0)); + } + + function testFulfillOrderRevertInvalidConsiderationItemsLength( + uint256 fuzzTotalConsiderationItems, + uint256 fuzzAmountToSubtractFromConsiderationItemsLength + ) public { + uint256 totalConsiderationItems = fuzzTotalConsiderationItems % 200; + // Set amount to subtract from consideration item length + // to be at most totalConsiderationItems. + uint256 amountToSubtractFromConsiderationItemsLength = totalConsiderationItems > + 0 + ? fuzzAmountToSubtractFromConsiderationItemsLength % + totalConsiderationItems + : 0; + + // Create order + ( + Order memory _order, + OrderParameters memory _orderParameters, + + ) = _prepareOrder(1, totalConsiderationItems); + + // Get the calldata that will be passed into fulfillOrder. + bytes memory fulfillOrderCalldata = abi.encodeWithSelector( + consideration.fulfillOrder.selector, + _order, + conduitKeyOne + ); + + _performTestFulfillOrderRevertInvalidArrayLength( + consideration, + _order, + fulfillOrderCalldata, + // Order parameters starts at 0xa4 relative to the start of the + // order calldata because the order calldata starts with 0x20 bytes + // for order calldata length, 0x04 bytes for selector, and 0x80 + // bytes until the start of order parameters. + 0xa4, + 0x60, + _orderParameters.consideration.length, + amountToSubtractFromConsiderationItemsLength + ); + } + + function fulfillOrderEthToErc721(Context memory context) + external + stateless + { + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test721_1.mint(alice, context.args.id); + offerItems.push( + OfferItem( + ItemType.ERC721, + address(test721_1), + context.args.id, + 1, + 1 + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[0], + context.args.paymentAmts[0], + payable(alice) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[1], + context.args.paymentAmts[1], + payable(context.args.zone) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[2], + context.args.paymentAmts[2], + payable(cal) + ) + ); + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length + ); + context.consideration.fulfillOrder{ + value: context + .args + .paymentAmts[0] + .add(context.args.paymentAmts[1]) + .add(context.args.paymentAmts[2]) + }(Order(orderParameters, signature), conduitKey); + } + + function fulfillOrderEthToErc1155(Context memory context) + external + stateless + { + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test1155_1.mint(alice, context.args.id, context.erc1155Amt); + offerItems.push( + OfferItem( + ItemType.ERC1155, + address(test1155_1), + context.args.id, + context.erc1155Amt, + context.erc1155Amt + ) + ); + + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[0], + context.args.paymentAmts[0], + payable(alice) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[1], + context.args.paymentAmts[1], + payable(context.args.zone) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[2], + context.args.paymentAmts[2], + payable(cal) + ) + ); + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length + ); + context.consideration.fulfillOrder{ + value: context + .args + .paymentAmts[0] + .add(context.args.paymentAmts[1]) + .add(context.args.paymentAmts[2]) + }(Order(orderParameters, signature), conduitKey); + } + + function fulfillOrderSingleErc20ToSingleErc1155(Context memory context) + external + stateless + { + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test1155_1.mint(alice, context.args.id, context.erc1155Amt); + + offerItems.push( + OfferItem( + ItemType.ERC1155, + address(test1155_1), + context.args.id, + context.erc1155Amt, + context.erc1155Amt + ) + ); + + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[0], + context.args.paymentAmts[0], + payable(alice) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[1], + context.args.paymentAmts[1], + payable(context.args.zone) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[2], + context.args.paymentAmts[2], + payable(cal) + ) + ); + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length + ); + + context.consideration.fulfillOrder{ + value: context + .args + .paymentAmts[0] + .add(context.args.paymentAmts[1]) + .add(context.args.paymentAmts[2]) + }(Order(orderParameters, signature), conduitKey); + } + + function fulfillOrderEthToErc721WithSingleEthTip(Context memory context) + external + stateless + { + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test721_1.mint(alice, context.args.id); + offerItems.push( + OfferItem( + ItemType.ERC721, + address(test721_1), + context.args.id, + 1, + 1 + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[0], + context.args.paymentAmts[0], + payable(alice) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[1], + context.args.paymentAmts[1], + payable(context.args.zone) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[2], + context.args.paymentAmts[2], + payable(cal) + ) + ); + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + // Add tip + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.tipAmt, + context.tipAmt, + payable(bob) + ) + ); + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length - 1 + ); + + context.consideration.fulfillOrder{ + value: context + .args + .paymentAmts[0] + .add(context.args.paymentAmts[1]) + .add(context.args.paymentAmts[2]) + .add(context.tipAmt) + }(Order(orderParameters, signature), conduitKey); + } + + function fulfillOrderEthToErc1155WithSingleEthTip(Context memory context) + external + stateless + { + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test1155_1.mint(alice, context.args.id, context.erc1155Amt); + + offerItems.push( + OfferItem( + ItemType.ERC1155, + address(test1155_1), + context.args.id, + context.erc1155Amt, + context.erc1155Amt + ) + ); + + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[0], + context.args.paymentAmts[0], + payable(alice) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[1], + context.args.paymentAmts[1], + payable(context.args.zone) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[2], + context.args.paymentAmts[2], + payable(cal) + ) + ); + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + // Add tip + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.tipAmt, + context.tipAmt, + payable(bob) + ) + ); + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length - 1 + ); + + context.consideration.fulfillOrder{ + value: context + .args + .paymentAmts[0] + .add(context.args.paymentAmts[1]) + .add(context.args.paymentAmts[2]) + .add(context.tipAmt) + }(Order(orderParameters, signature), conduitKey); + } + + function fulfillOrderEthToErc721WithMultipleEthTips(Context memory context) + external + stateless + { + context.numTips = (context.numTips % 64) + 1; + + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test721_1.mint(alice, context.args.id); + offerItems.push( + OfferItem( + ItemType.ERC721, + address(test721_1), + context.args.id, + 1, + 1 + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[0], + context.args.paymentAmts[0], + payable(alice) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[1], + context.args.paymentAmts[1], + payable(context.args.zone) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[2], + context.args.paymentAmts[2], + payable(cal) + ) + ); + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + uint128 sumOfTips; + for (uint128 i = 1; i < context.numTips + 1; ++i) { + uint256 tipPk = 0xb0b + i; + address tipAddr = vm.addr(tipPk); + sumOfTips += i; + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + i, + i, + payable(tipAddr) + ) + ); + } + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length - context.numTips + ); + + context.consideration.fulfillOrder{ + value: context + .args + .paymentAmts[0] + .add(context.args.paymentAmts[1]) + .add(context.args.paymentAmts[2]) + .add(sumOfTips) + }(Order(orderParameters, signature), conduitKey); + } + + function fulfillOrderEthToErc1155WithMultipleEthTips(Context memory context) + external + stateless + { + context.numTips = (context.numTips % 64) + 1; + + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test1155_1.mint(alice, context.args.id, context.erc1155Amt); + + offerItems.push( + OfferItem( + ItemType.ERC1155, + address(test1155_1), + context.args.id, + context.erc1155Amt, + context.erc1155Amt + ) + ); + + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[0], + context.args.paymentAmts[0], + payable(alice) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[1], + context.args.paymentAmts[1], + payable(context.args.zone) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[2], + context.args.paymentAmts[2], + payable(cal) + ) + ); + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + uint128 sumOfTips; + // push tip of amount i eth to considerationitems + for (uint128 i = 1; i < context.numTips + 1; ++i) { + uint256 tipPk = 0xb0b + i; + address tipAddr = vm.addr(tipPk); + sumOfTips += i; + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + i, + i, + payable(tipAddr) + ) + ); + } + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length - context.numTips + ); + + context.consideration.fulfillOrder{ + value: context + .args + .paymentAmts[0] + .add(context.args.paymentAmts[1]) + .add(context.args.paymentAmts[2]) + .add(sumOfTips) + }(Order(orderParameters, signature), conduitKey); + } + + function fulfillOrderEthToErc721WithErc721Tips(Context memory context) + external + stateless + { + context.numTips = (context.numTips % 64) + 1; + + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test721_1.mint(alice, context.args.id); + offerItems.push( + OfferItem( + ItemType.ERC721, + address(test721_1), + context.args.id, + 1, + 1 + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[0], + context.args.paymentAmts[0], + payable(alice) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[1], + context.args.paymentAmts[1], + payable(context.args.zone) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[2], + context.args.paymentAmts[2], + payable(cal) + ) + ); + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + // mint erc721s to the test contract and push tips to considerationItems + for (uint128 i = 1; i < context.numTips + 1; ++i) { + uint256 tipPk = 0xb0b + i; + address tipAddr = vm.addr(tipPk); + test721_2.mint(address(this), i); // mint test721_2 tokens to avoid collision with fuzzed test721_1 tokenId + considerationItems.push( + ConsiderationItem( + ItemType.ERC721, + address(test721_2), + i, + 1, + 1, + payable(tipAddr) + ) + ); + } + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length - context.numTips + ); + + context.consideration.fulfillOrder{ + value: context + .args + .paymentAmts[0] + .add(context.args.paymentAmts[1]) + .add(context.args.paymentAmts[2]) + }(Order(orderParameters, signature), conduitKey); + } + + function fulfillOrderEthToErc1155WithErc721Tips(Context memory context) + external + stateless + { + context.numTips = (context.numTips % 64) + 1; + + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test1155_1.mint(alice, context.args.id, context.erc1155Amt); + offerItems.push( + OfferItem( + ItemType.ERC1155, + address(test1155_1), + context.args.id, + context.erc1155Amt, + context.erc1155Amt + ) + ); + + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[0], + context.args.paymentAmts[0], + payable(alice) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[1], + context.args.paymentAmts[1], + payable(context.args.zone) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[2], + context.args.paymentAmts[2], + payable(cal) + ) + ); + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + // mint erc721s to the test contract and push tips to considerationItems + for (uint128 i = 1; i < context.numTips + 1; ++i) { + uint256 tipPk = 0xb0b + i; + address tipAddr = vm.addr(tipPk); + test721_2.mint(address(this), i); // mint test721_2 tokens to avoid collision with fuzzed test721_1 tokenId + considerationItems.push( + ConsiderationItem( + ItemType.ERC721, + address(test721_2), + i, + 1, + 1, + payable(tipAddr) + ) + ); + } + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length - context.numTips + ); + + context.consideration.fulfillOrder{ + value: context + .args + .paymentAmts[0] + .add(context.args.paymentAmts[1]) + .add(context.args.paymentAmts[2]) + }(Order(orderParameters, signature), conduitKey); + } + + function fulfillOrderEthToErc721WithErc1155Tips(Context memory context) + external + stateless + { + context.numTips = (context.numTips % 64) + 1; + + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test721_1.mint(alice, context.args.id); + + offerItems.push( + OfferItem( + ItemType.ERC721, + address(test721_1), + context.args.id, + 1, + 1 + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[0], + context.args.paymentAmts[0], + payable(alice) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[1], + context.args.paymentAmts[1], + payable(context.args.zone) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[2], + context.args.paymentAmts[2], + payable(cal) + ) + ); + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + for (uint256 i = 1; i < context.numTips.add(1); ++i) { + uint256 tipPk = 0xb0b + i; + address tipAddr = vm.addr(tipPk); + test1155_1.mint(address(this), context.args.id.add(i), i); + considerationItems.push( + ConsiderationItem( + ItemType.ERC1155, + address(test1155_1), + context.args.id.add(i), + i, + i, + payable(tipAddr) + ) + ); + } + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length - context.numTips + ); + + context.consideration.fulfillOrder{ + value: context + .args + .paymentAmts[0] + .add(context.args.paymentAmts[1]) + .add(context.args.paymentAmts[2]) + }(Order(orderParameters, signature), conduitKey); + } + + function fulfillOrderEthToErc1155WithErc1155Tips(Context memory context) + external + stateless + { + context.numTips = (context.numTips % 64) + 1; + + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test1155_1.mint(alice, context.args.id, context.erc1155Amt); + offerItems.push( + OfferItem( + ItemType.ERC1155, + address(test1155_1), + context.args.id, + context.erc1155Amt, + context.erc1155Amt + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[0], + context.args.paymentAmts[0], + payable(alice) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[1], + context.args.paymentAmts[1], + payable(context.args.zone) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[2], + context.args.paymentAmts[2], + payable(cal) + ) + ); + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + for (uint256 i = 1; i < context.numTips.add(1); ++i) { + uint256 tipPk = 0xb0b + i; + address tipAddr = vm.addr(tipPk); + test1155_1.mint(address(this), context.args.id.add(i), i); + considerationItems.push( + ConsiderationItem( + ItemType.ERC1155, + address(test1155_1), + context.args.id.add(i), + i, + i, + payable(tipAddr) + ) + ); + } + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length - context.numTips + ); + + context.consideration.fulfillOrder{ + value: context + .args + .paymentAmts[0] + .add(context.args.paymentAmts[1]) + .add(context.args.paymentAmts[2]) + }(Order(orderParameters, signature), conduitKey); + } + + function fulfillOrderEthToErc721WithErc20Tips(Context memory context) + external + stateless + { + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test721_1.mint(alice, context.args.id); + + offerItems.push( + OfferItem( + ItemType.ERC721, + address(test721_1), + context.args.id, + 1, + 1 + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[0], + context.args.paymentAmts[0], + payable(alice) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[1], + context.args.paymentAmts[1], + payable(context.args.zone) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[2], + context.args.paymentAmts[2], + payable(cal) + ) + ); + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + for (uint256 i = 1; i < context.numTips.add(1); ++i) { + uint256 tipPk = i; + address tipAddr = vm.addr(tipPk); + considerationItems.push( + ConsiderationItem( + ItemType.ERC20, + address(token1), + 0, // ignored for ERC20 + i, + i, + payable(tipAddr) + ) + ); + } + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length - context.numTips + ); + + context.consideration.fulfillOrder{ + value: context + .args + .paymentAmts[0] + .add(context.args.paymentAmts[1]) + .add(context.args.paymentAmts[2]) + }(Order(orderParameters, signature), conduitKey); + } + + function fulfillOrderEthToErc1155WithErc20Tips(Context memory context) + external + stateless + { + context.numTips = (context.numTips % 64) + 1; + + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test1155_1.mint(alice, context.args.id, context.erc1155Amt); + offerItems.push( + OfferItem( + ItemType.ERC1155, + address(test1155_1), + context.args.id, + context.erc1155Amt, + context.erc1155Amt + ) + ); + + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[0], + context.args.paymentAmts[0], + payable(alice) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[1], + context.args.paymentAmts[1], + payable(context.args.zone) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[2], + context.args.paymentAmts[2], + payable(cal) + ) + ); + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + for (uint256 i = 1; i < context.numTips.add(1); ++i) { + uint256 tipPk = i; + address tipAddr = vm.addr(tipPk); + considerationItems.push( + ConsiderationItem( + ItemType.ERC20, + address(token1), + 0, // ignored for ERC20 + i, + i, + payable(tipAddr) + ) + ); + } + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length - context.numTips + ); + + context.consideration.fulfillOrder{ + value: context + .args + .paymentAmts[0] + .add(context.args.paymentAmts[1]) + .add(context.args.paymentAmts[2]) + }(Order(orderParameters, signature), conduitKey); + } + + function fulfillOrderEthToErc721FullRestricted(Context memory context) + external + stateless + { + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test721_1.mint(alice, context.args.id); + offerItems.push( + OfferItem( + ItemType.ERC721, + address(test721_1), + context.args.id, + 1, + 1 + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[0], + context.args.paymentAmts[0], + payable(alice) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[1], + context.args.paymentAmts[1], + payable(context.args.zone) + ) + ); + considerationItems.push( + ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + context.args.paymentAmts[2], + context.args.paymentAmts[2], + payable(cal) + ) + ); + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_RESTRICTED, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_RESTRICTED, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length + ); + vm.prank(alice); + context.consideration.fulfillOrder{ + value: context + .args + .paymentAmts[0] + .add(context.args.paymentAmts[1]) + .add(context.args.paymentAmts[2]) + }(Order(orderParameters, signature), conduitKey); + } + + function testFulfillOrderRevertUnusedItemParametersAddressSetOnNativeConsideration( + FuzzInputsCommon memory inputs, + uint256 tokenAmount, + address _badToken + ) public validateInputs(inputs) onlyPayable(inputs.zone) { + vm.assume(_badToken != address(0)); + badToken = _badToken; + + vm.assume(inputs.id > 0); + vm.assume(tokenAmount > 0); + test( + this + .fulfillOrderRevertUnusedItemParametersAddressSetOnNativeConsideration, + Context(consideration, inputs, tokenAmount, 0, 0) + ); + test( + this + .fulfillOrderRevertUnusedItemParametersAddressSetOnNativeConsideration, + Context(referenceConsideration, inputs, tokenAmount, 0, 0) + ); + } + + function fulfillOrderRevertUnusedItemParametersAddressSetOnNativeConsideration( + Context memory context + ) external stateless { + test1155_1.mint(alice, context.args.id, context.erc1155Amt); + addErc1155OfferItem(context.args.id, context.erc1155Amt); + addEthConsiderationItem(alice, 100); + + considerationItems[0].token = badToken; + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + bytes32(0), + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + bytes32(0), + considerationItems.length + ); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSignature("UnusedItemParameters()")); + context.consideration.fulfillOrder{ value: 100 }( + Order(orderParameters, signature), + bytes32(0) + ); + } + + function testFulfillOrderRevertUnusedItemParametersIdentifierSetOnNativeConsideration( + FuzzInputsCommon memory inputs, + uint256 tokenAmount, + uint256 _badIdentifier + ) public validateInputs(inputs) onlyPayable(inputs.zone) { + vm.assume(_badIdentifier != 0); + badIdentifier = _badIdentifier; + + vm.assume(inputs.id > 0); + vm.assume(tokenAmount > 0); + test( + this + .fulfillOrderRevertUnusedItemParametersIdentifierSetOnNativeConsideration, + Context(consideration, inputs, tokenAmount, 0, 0) + ); + test( + this + .fulfillOrderRevertUnusedItemParametersIdentifierSetOnNativeConsideration, + Context(referenceConsideration, inputs, tokenAmount, 0, 0) + ); + } + + function fulfillOrderRevertUnusedItemParametersIdentifierSetOnNativeConsideration( + Context memory context + ) external stateless { + test1155_1.mint(alice, context.args.id, context.erc1155Amt); + addErc1155OfferItem(context.args.id, context.erc1155Amt); + addEthConsiderationItem(alice, 100); + + considerationItems[0].identifierOrCriteria = badIdentifier; + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + bytes32(0), + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + bytes32(0), + considerationItems.length + ); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSignature("UnusedItemParameters()")); + context.consideration.fulfillOrder{ value: 100 }( + Order(orderParameters, signature), + bytes32(0) + ); + } + + function testFulfillOrderRevertUnusedItemParametersAddressAndIdentifierSetOnNativeConsideration( + FuzzInputsCommon memory inputs, + uint256 tokenAmount, + uint256 _badIdentifier, + address _badToken + ) public validateInputs(inputs) onlyPayable(inputs.zone) { + vm.assume(_badIdentifier != 0 || _badToken != address(0)); + badIdentifier = _badIdentifier; + badToken = _badToken; + + vm.assume(inputs.id > 0); + vm.assume(tokenAmount > 0); + test( + this + .fulfillOrderRevertUnusedItemParametersAddressAndIdentifierSetOnNativeConsideration, + Context(consideration, inputs, tokenAmount, 0, 0) + ); + test( + this + .fulfillOrderRevertUnusedItemParametersAddressAndIdentifierSetOnNativeConsideration, + Context(referenceConsideration, inputs, tokenAmount, 0, 0) + ); + } + + function fulfillOrderRevertUnusedItemParametersAddressAndIdentifierSetOnNativeConsideration( + Context memory context + ) external stateless { + test1155_1.mint(alice, context.args.id, context.erc1155Amt); + addErc1155OfferItem(context.args.id, context.erc1155Amt); + addEthConsiderationItem(alice, 100); + + considerationItems[0].identifierOrCriteria = badIdentifier; + considerationItems[0].token = badToken; + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + bytes32(0), + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + bytes32(0), + considerationItems.length + ); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSignature("UnusedItemParameters()")); + context.consideration.fulfillOrder{ value: 100 }( + Order(orderParameters, signature), + bytes32(0) + ); + } + + function testFulfillOrderRevertUnusedItemParametersIdentifierSetOnErc20Offer( + FuzzInputsCommon memory inputs, + uint256 tokenAmount, + uint256 _badIdentifier + ) public validateInputs(inputs) onlyPayable(inputs.zone) { + vm.assume(_badIdentifier != 0); + badIdentifier = _badIdentifier; + + vm.assume(inputs.id > 0); + vm.assume(tokenAmount > 0); + test( + this + .fulfillOrderRevertUnusedItemParametersIdentifierSetOnErc20Offer, + Context(consideration, inputs, tokenAmount, 0, 0) + ); + test( + this + .fulfillOrderRevertUnusedItemParametersIdentifierSetOnErc20Offer, + Context(referenceConsideration, inputs, tokenAmount, 0, 0) + ); + } + + function fulfillOrderRevertUnusedItemParametersIdentifierSetOnErc20Offer( + Context memory context + ) external stateless { + test721_1.mint(bob, context.args.id); + + addErc20OfferItem(100); + addErc721ConsiderationItem(alice, context.args.id); + + offerItems[0].identifierOrCriteria = badIdentifier; + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + bytes32(0), + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + bytes32(0), + considerationItems.length + ); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSignature("UnusedItemParameters()")); + context.consideration.fulfillOrder( + Order(orderParameters, signature), + bytes32(0) + ); + } + + function testFulfillOrderRevertUnusedItemParametersIdentifierSetOnErc20Consideration( + FuzzInputsCommon memory inputs, + uint256 tokenAmount, + uint256 _badIdentifier + ) public validateInputs(inputs) onlyPayable(inputs.zone) { + vm.assume(_badIdentifier != 0); + badIdentifier = _badIdentifier; + + vm.assume(inputs.id > 0); + vm.assume(tokenAmount > 0); + test( + this + .fulfillOrderRevertUnusedItemParametersIdentifierSetOnErc20Consideration, + Context(consideration, inputs, tokenAmount, 0, 0) + ); + test( + this + .fulfillOrderRevertUnusedItemParametersIdentifierSetOnErc20Consideration, + Context(referenceConsideration, inputs, tokenAmount, 0, 0) + ); + } + + function fulfillOrderRevertUnusedItemParametersIdentifierSetOnErc20Consideration( + Context memory context + ) external stateless { + test721_1.mint(alice, context.args.id); + addErc721OfferItem(context.args.id); + addErc20ConsiderationItem(alice, 100); + + considerationItems[0].identifierOrCriteria = badIdentifier; + + OrderComponents memory orderComponents = OrderComponents( + alice, + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + bytes32(0), + context.consideration.getCounter(alice) + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.FULL_OPEN, + block.timestamp, + block.timestamp + 1, + context.args.zoneHash, + context.args.salt, + bytes32(0), + considerationItems.length + ); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSignature("UnusedItemParameters()")); + context.consideration.fulfillOrder( + Order(orderParameters, signature), + bytes32(0) + ); + } +} diff --git a/test/foundry/FullfillAvailableOrder.t.sol b/test/foundry/FullfillAvailableOrder.t.sol index 29fabe205..8b8f67127 100644 --- a/test/foundry/FullfillAvailableOrder.t.sol +++ b/test/foundry/FullfillAvailableOrder.t.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { OrderType, BasicOrderType, ItemType, Side } from "../../contracts/lib/ConsiderationEnums.sol"; import { ConsiderationInterface } from "../../contracts/interfaces/ConsiderationInterface.sol"; @@ -10,8 +10,15 @@ import { TestERC721 } from "../../contracts/test/TestERC721.sol"; import { TestERC1155 } from "../../contracts/test/TestERC1155.sol"; import { TestERC20 } from "../../contracts/test/TestERC20.sol"; import { stdError } from "forge-std/Test.sol"; +import { ArithmeticUtil } from "./utils/ArithmeticUtil.sol"; contract FulfillAvailableOrder is BaseOrderTest { + using ArithmeticUtil for uint256; + using ArithmeticUtil for uint240; + using ArithmeticUtil for uint128; + using ArithmeticUtil for uint120; + + FuzzInputs empty; struct FuzzInputs { address zone; uint256 id; @@ -19,82 +26,212 @@ contract FulfillAvailableOrder is BaseOrderTest { uint248 salt; uint128[3] paymentAmts; bool useConduit; + uint240 amount; } struct Context { ConsiderationInterface consideration; FuzzInputs args; + ItemType itemType; + } + + modifier validateInputs(FuzzInputs memory inputs) { + vm.assume( + inputs.paymentAmts[0] > 0 && + inputs.paymentAmts[1] > 0 && + inputs.paymentAmts[2] > 0 + ); + vm.assume( + inputs.paymentAmts[0].add(inputs.paymentAmts[1]).add( + inputs.paymentAmts[2] + ) <= 2**128 - 1 + ); + _; + } + + function test(function(Context memory) external fn, Context memory context) + internal + { + try fn(context) {} catch (bytes memory reason) { + assertPass(reason); + } + } + + function testNoNativeOffersFulfillAvailable(uint8[8] memory itemTypes) + public + { + uint256 tokenId; + for (uint256 i; i < 8; i++) { + ItemType itemType = ItemType(itemTypes[i] % 4); + if (itemType == ItemType.NATIVE) { + addEthOfferItem(1); + } else if (itemType == ItemType.ERC20) { + addErc20OfferItem(1); + } else if (itemType == ItemType.ERC1155) { + test1155_1.mint(alice, tokenId, 1); + addErc1155OfferItem(tokenId, 1); + } else { + test721_1.mint(alice, tokenId); + addErc721OfferItem(tokenId); + } + tokenId++; + offerComponents.push(FulfillmentComponent(1, i)); + } + addEthOfferItem(1); + + addEthConsiderationItem(alice, 1); + considerationComponents.push(FulfillmentComponent(1, 0)); + + test( + this.noNativeOfferItemsFulfillAvailable, + Context(consideration, empty, ItemType(0)) + ); + test( + this.noNativeOfferItemsFulfillAvailable, + Context(referenceConsideration, empty, ItemType(0)) + ); + } + + function noNativeOfferItemsFulfillAvailable(Context memory context) + external + stateless + { + configureOrderParameters(alice); + uint256 counter = context.consideration.getCounter(alice); + _configureOrderComponents(counter); + bytes32 orderHash = context.consideration.getOrderHash( + baseOrderComponents + ); + bytes memory signature = signOrder( + context.consideration, + alicePk, + orderHash + ); + + Order[] memory orders = new Order[](2); + orders[1] = Order(baseOrderParameters, signature); + offerComponentsArray.push(offerComponents); + considerationComponentsArray.push(considerationComponents); + + delete offerItems; + delete considerationItems; + delete offerComponents; + delete considerationComponents; + + token1.mint(alice, 100); + addErc20OfferItem(100); + addEthConsiderationItem(alice, 1); + configureOrderParameters(alice); + counter = context.consideration.getCounter(alice); + _configureOrderComponents(counter); + bytes32 orderHash2 = context.consideration.getOrderHash( + baseOrderComponents + ); + bytes memory signature2 = signOrder( + context.consideration, + alicePk, + orderHash2 + ); + offerComponents.push(FulfillmentComponent(0, 0)); + considerationComponents.push(FulfillmentComponent(0, 0)); + offerComponentsArray.push(offerComponents); + considerationComponentsArray.push(considerationComponents); + + orders[0] = Order(baseOrderParameters, signature2); + + vm.expectRevert(abi.encodeWithSignature("InvalidNativeOfferItem()")); + context.consideration.fulfillAvailableOrders{ value: 2 }( + orders, + offerComponentsArray, + considerationComponentsArray, + bytes32(0), + 2 + ); } function testFulfillAvailableOrdersOverflowOfferSide() public { - for (uint256 i; i < 4; i++) { + // skip eth + for (uint256 i = 1; i < 4; ++i) { // skip 721s if (i == 2) { continue; } - _testFulfillAvailableOrdersOverflowOfferSide( - consideration, - ItemType(i) + test( + this.fulfillAvailableOrdersOverflowOfferSide, + Context(consideration, empty, ItemType(i)) ); - _testFulfillAvailableOrdersOverflowOfferSide( - referenceConsideration, - ItemType(i) + test( + this.fulfillAvailableOrdersOverflowOfferSide, + Context(referenceConsideration, empty, ItemType(i)) ); } } function testFulfillAvailableOrdersOverflowConsiderationSide() public { - for (uint256 i; i < 4; i++) { + for (uint256 i; i < 4; ++i) { // skip 721s if (i == 2) { continue; } - _testFulfillAvailableOrdersOverflowConsiderationSide( - consideration, - ItemType(i) + test( + this.fulfillAvailableOrdersOverflowConsiderationSide, + Context(consideration, empty, ItemType(i)) ); - _testFulfillAvailableOrdersOverflowConsiderationSide( - referenceConsideration, - ItemType(i) + test( + this.fulfillAvailableOrdersOverflowConsiderationSide, + Context(referenceConsideration, empty, ItemType(i)) ); } } function testSingleOrderViaFulfillAvailableOrdersEthToSingleErc721( FuzzInputs memory args - ) public { - _testSingleOrderViaFulfillAvailableOrdersEthToSingleErc721( - Context(referenceConsideration, args) + ) public validateInputs(args) onlyPayable(args.zone) { + test( + this.singleOrderViaFulfillAvailableOrdersEthToSingleErc721, + Context(referenceConsideration, args, ItemType(0)) ); - _testSingleOrderViaFulfillAvailableOrdersEthToSingleErc721( - Context(consideration, args) + test( + this.singleOrderViaFulfillAvailableOrdersEthToSingleErc721, + Context(consideration, args, ItemType(0)) ); } function testFulfillAndAggregateTwoOrdersViaFulfillAvailableOrdersEthToErc1155( - FuzzInputs memory args, - uint240 amount - ) public { - _testFulfillAndAggregateTwoOrdersViaFulfillAvailableOrdersEthToErc1155( - Context(referenceConsideration, args), - amount + FuzzInputs memory args + ) public onlyPayable(args.zone) { + vm.assume(args.amount > 0); + + args.paymentAmts[0] = uint120(args.paymentAmts[0].mul(2)); + args.paymentAmts[1] = uint120(args.paymentAmts[1].mul(2)); + args.paymentAmts[2] = uint120(args.paymentAmts[2].mul(2)); + vm.assume( + args.paymentAmts[0] > 0 && + args.paymentAmts[1] > 0 && + args.paymentAmts[2] > 0 + ); + test( + this + .fulfillAndAggregateTwoOrdersViaFulfillAvailableOrdersEthToErc1155, + Context(referenceConsideration, args, ItemType(0)) ); - _testFulfillAndAggregateTwoOrdersViaFulfillAvailableOrdersEthToErc1155( - Context(consideration, args), - amount + test( + this + .fulfillAndAggregateTwoOrdersViaFulfillAvailableOrdersEthToErc1155, + Context(consideration, args, ItemType(0)) ); } - function _testFulfillAvailableOrdersOverflowOfferSide( - ConsiderationInterface _consideration, - ItemType itemType - ) internal resetTokenBalancesBetweenRuns { + function fulfillAvailableOrdersOverflowOfferSide(Context memory context) + external + stateless + { // mint consideration nfts to the test contract test721_1.mint(address(this), 1); test721_1.mint(address(this), 2); - _configureOfferItem(itemType, 1, 100); - _configureConsiderationItem(alice, ItemType.ERC721, 1, 1); + addOfferItem(context.itemType, 1, 100); + addConsiderationItem(alice, ItemType.ERC721, 1, 1); OrderParameters memory orderParameters = OrderParameters( address(alice), @@ -112,20 +249,20 @@ contract FulfillAvailableOrder is BaseOrderTest { OrderComponents memory firstOrderComponents = getOrderComponents( orderParameters, - _consideration.getNonce(alice) + context.consideration.getCounter(alice) ); bytes memory signature = signOrder( - _consideration, + context.consideration, alicePk, - _consideration.getOrderHash(firstOrderComponents) + context.consideration.getOrderHash(firstOrderComponents) ); delete offerItems; delete considerationItems; // try to overflow the aggregated amount of tokens sent to alice - _configureOfferItem(itemType, 1, MAX_INT); - _configureConsiderationItem(bob, ItemType.ERC721, 2, 1); + addOfferItem(context.itemType, 1, MAX_INT); + addConsiderationItem(bob, ItemType.ERC721, 2, 1); OrderParameters memory secondOrderParameters = OrderParameters( address(alice), @@ -143,33 +280,33 @@ contract FulfillAvailableOrder is BaseOrderTest { OrderComponents memory secondOrderComponents = getOrderComponents( secondOrderParameters, - _consideration.getNonce(alice) + context.consideration.getCounter(alice) ); bytes memory secondSignature = signOrder( - _consideration, + context.consideration, alicePk, - _consideration.getOrderHash(secondOrderComponents) + context.consideration.getOrderHash(secondOrderComponents) ); Order[] memory orders = new Order[](2); orders[0] = Order(orderParameters, signature); orders[1] = Order(secondOrderParameters, secondSignature); - // agregate offers together + // aggregate offers together offerComponents.push(FulfillmentComponent(0, 0)); offerComponents.push(FulfillmentComponent(1, 0)); offerComponentsArray.push(offerComponents); - resetOfferComponents(); + delete offerComponents; considerationComponents.push(FulfillmentComponent(0, 0)); considerationComponentsArray.push(considerationComponents); delete considerationComponents; considerationComponents.push(FulfillmentComponent(1, 0)); considerationComponentsArray.push(considerationComponents); - resetConsiderationComponents(); + delete considerationComponents; vm.expectRevert(stdError.arithmeticError); - _consideration.fulfillAvailableOrders( + context.consideration.fulfillAvailableOrders( orders, offerComponentsArray, considerationComponentsArray, @@ -178,13 +315,12 @@ contract FulfillAvailableOrder is BaseOrderTest { ); } - function _testFulfillAvailableOrdersOverflowConsiderationSide( - ConsiderationInterface _consideration, - ItemType itemType - ) internal resetTokenBalancesBetweenRuns { + function fulfillAvailableOrdersOverflowConsiderationSide( + Context memory context + ) external stateless { test721_1.mint(alice, 1); - _configureOfferItem(ItemType.ERC721, 1, 1); - _configureConsiderationItem(alice, itemType, 1, 100); + addOfferItem(ItemType.ERC721, 1, 1); + addConsiderationItem(alice, context.itemType, 1, 100); OrderParameters memory orderParameters = OrderParameters( address(alice), @@ -202,21 +338,21 @@ contract FulfillAvailableOrder is BaseOrderTest { OrderComponents memory firstOrderComponents = getOrderComponents( orderParameters, - _consideration.getNonce(alice) + context.consideration.getCounter(alice) ); bytes memory signature = signOrder( - _consideration, + context.consideration, alicePk, - _consideration.getOrderHash(firstOrderComponents) + context.consideration.getOrderHash(firstOrderComponents) ); delete offerItems; delete considerationItems; test721_1.mint(bob, 2); - _configureOfferItem(ItemType.ERC721, 2, 1); + addOfferItem(ItemType.ERC721, 2, 1); // try to overflow the aggregated amount of tokens sent to alice - _configureConsiderationItem(alice, itemType, 1, MAX_INT); + addConsiderationItem(alice, context.itemType, 1, MAX_INT); OrderParameters memory secondOrderParameters = OrderParameters( address(bob), @@ -234,12 +370,12 @@ contract FulfillAvailableOrder is BaseOrderTest { OrderComponents memory secondOrderComponents = getOrderComponents( secondOrderParameters, - _consideration.getNonce(bob) + context.consideration.getCounter(bob) ); bytes memory secondSignature = signOrder( - _consideration, + context.consideration, bobPk, - _consideration.getOrderHash(secondOrderComponents) + context.consideration.getOrderHash(secondOrderComponents) ); Order[] memory orders = new Order[](2); @@ -251,15 +387,15 @@ contract FulfillAvailableOrder is BaseOrderTest { delete offerComponents; offerComponents.push(FulfillmentComponent(1, 0)); offerComponentsArray.push(offerComponents); - resetOfferComponents(); + delete offerComponents; - // agregate considerations together + // aggregate considerations together considerationComponents.push(FulfillmentComponent(0, 0)); considerationComponents.push(FulfillmentComponent(1, 0)); considerationComponentsArray.push(considerationComponents); - resetConsiderationComponents(); + delete considerationComponents; vm.expectRevert(stdError.arithmeticError); - _consideration.fulfillAvailableOrders{ value: 99 }( + context.consideration.fulfillAvailableOrders{ value: 99 }( orders, offerComponentsArray, considerationComponentsArray, @@ -268,26 +404,9 @@ contract FulfillAvailableOrder is BaseOrderTest { ); } - function _testSingleOrderViaFulfillAvailableOrdersEthToSingleErc721( + function singleOrderViaFulfillAvailableOrdersEthToSingleErc721( Context memory context - ) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) <= - 2**128 - 1 - ); - + ) external stateless { bytes32 conduitKey = context.args.useConduit ? conduitKeyOne : bytes32(0); @@ -349,7 +468,7 @@ contract FulfillAvailableOrder is BaseOrderTest { OrderComponents memory orderComponents = getOrderComponents( orderParameters, - context.consideration.getNonce(alice) + context.consideration.getCounter(alice) ); bytes memory signature = signOrder( @@ -363,19 +482,19 @@ contract FulfillAvailableOrder is BaseOrderTest { offerComponents.push(FulfillmentComponent(0, 0)); offerComponentsArray.push(offerComponents); - resetOfferComponents(); + delete offerComponents; considerationComponents.push(FulfillmentComponent(0, 0)); considerationComponentsArray.push(considerationComponents); - resetConsiderationComponents(); + delete considerationComponents; considerationComponents.push(FulfillmentComponent(0, 1)); considerationComponentsArray.push(considerationComponents); - resetConsiderationComponents(); + delete considerationComponents; considerationComponents.push(FulfillmentComponent(0, 2)); considerationComponentsArray.push(considerationComponents); - resetConsiderationComponents(); + delete considerationComponents; assertTrue(considerationComponentsArray.length == 3); @@ -392,46 +511,22 @@ contract FulfillAvailableOrder is BaseOrderTest { ); } - function _testFulfillAndAggregateTwoOrdersViaFulfillAvailableOrdersEthToErc1155( - Context memory context, - uint240 amount - ) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 - ); - vm.assume(amount > 0); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) <= - 2**128 - 1 - ); - vm.assume( - context.args.paymentAmts[0] % 2 == 0 && - context.args.paymentAmts[1] % 2 == 0 && - context.args.paymentAmts[2] % 2 == 0 - ); - + function fulfillAndAggregateTwoOrdersViaFulfillAvailableOrdersEthToErc1155( + Context memory context + ) external stateless { bytes32 conduitKey = context.args.useConduit ? conduitKeyOne : bytes32(0); - test1155_1.mint(alice, context.args.id, uint256(amount) * 2); + test1155_1.mint(alice, context.args.id, context.args.amount.mul(2)); offerItems.push( OfferItem( ItemType.ERC1155, address(test1155_1), context.args.id, - amount, - amount + context.args.amount, + context.args.amount ) ); considerationItems.push( @@ -480,7 +575,7 @@ contract FulfillAvailableOrder is BaseOrderTest { ); OrderComponents memory orderComponents = getOrderComponents( orderParameters, - context.consideration.getNonce(alice) + context.consideration.getCounter(alice) ); bytes memory signature = signOrder( @@ -505,7 +600,7 @@ contract FulfillAvailableOrder is BaseOrderTest { OrderComponents memory secondOrderComponents = getOrderComponents( secondOrderParameters, - context.consideration.getNonce(alice) + context.consideration.getCounter(alice) ); bytes memory secondOrderSignature = signOrder( @@ -521,22 +616,23 @@ contract FulfillAvailableOrder is BaseOrderTest { offerComponents.push(FulfillmentComponent(0, 0)); offerComponents.push(FulfillmentComponent(1, 0)); offerComponentsArray.push(offerComponents); - resetOfferComponents(); + delete offerComponents; considerationComponents.push(FulfillmentComponent(0, 0)); considerationComponents.push(FulfillmentComponent(1, 0)); considerationComponentsArray.push(considerationComponents); - resetConsiderationComponents(); + delete considerationComponents; considerationComponents.push(FulfillmentComponent(0, 1)); considerationComponents.push(FulfillmentComponent(1, 1)); considerationComponentsArray.push(considerationComponents); - resetConsiderationComponents(); + delete considerationComponents; considerationComponents.push(FulfillmentComponent(0, 2)); considerationComponents.push(FulfillmentComponent(1, 2)); considerationComponentsArray.push(considerationComponents); - resetConsiderationComponents(); + delete considerationComponents; + emit log_string("about to add"); context.consideration.fulfillAvailableOrders{ value: context.args.paymentAmts[0] + diff --git a/test/foundry/GetterTests.t.sol b/test/foundry/GetterTests.t.sol index 1e95f0de2..079323047 100644 --- a/test/foundry/GetterTests.t.sol +++ b/test/foundry/GetterTests.t.sol @@ -1,17 +1,47 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { BaseConsiderationTest } from "./utils/BaseConsiderationTest.sol"; contract TestGetters is BaseConsiderationTest { - function tesGetCorrectName() public { + function testGetCorrectName() public { assertEq(consideration.name(), "Consideration"); } + function testCleanName() public { + string memory name = consideration.name(); + + uint256 rds; + assembly { + rds := returndatasize() + } + + // offset (0x20) + length (0x20) + content (0x20) = 0x60 + assertEq(rds, 0x60); + + uint256 offset; + uint256 length; + bytes32 value; + assembly { + let freeMemoryPointer := mload(0x40) + returndatacopy(freeMemoryPointer, 0, returndatasize()) + offset := mload(freeMemoryPointer) + length := mload(add(freeMemoryPointer, 0x20)) + value := mload(add(freeMemoryPointer, 0x40)) + } + + // Default offset for abi.encode("Consideration") + assertEq(offset, 0x20); + // Length of "Consideration" + assertEq(length, 13); + // Check if there are dirty bits + assertEq(value, bytes32("Consideration")); + } + function testGetsCorrectVersion() public { (string memory version, , ) = consideration.information(); - assertEq(version, "1"); + assertEq(version, "1.1"); } function testGetCorrectDomainSeparator() public { diff --git a/test/foundry/MatchAdvancedOrder.t.sol b/test/foundry/MatchAdvancedOrder.t.sol index 62e1b4e19..006f3dab3 100644 --- a/test/foundry/MatchAdvancedOrder.t.sol +++ b/test/foundry/MatchAdvancedOrder.t.sol @@ -1,21 +1,22 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; -import { OrderType, BasicOrderType, ItemType, Side } from "../../contracts/lib/ConsiderationEnums.sol"; -import { Order, Fulfillment } from "../../contracts/lib/ConsiderationStructs.sol"; +import { OrderType, ItemType } from "../../contracts/lib/ConsiderationEnums.sol"; +import { Order } from "../../contracts/lib/ConsiderationStructs.sol"; import { ConsiderationInterface } from "../../contracts/interfaces/ConsiderationInterface.sol"; -import { AdvancedOrder, OfferItem, OrderParameters, ConsiderationItem, OrderComponents, BasicOrderParameters, CriteriaResolver, FulfillmentComponent } from "../../contracts/lib/ConsiderationStructs.sol"; +import { AdvancedOrder, OfferItem, OrderParameters, ConsiderationItem, OrderComponents, CriteriaResolver, FulfillmentComponent } from "../../contracts/lib/ConsiderationStructs.sol"; import { BaseOrderTest } from "./utils/BaseOrderTest.sol"; -import { TestERC721 } from "../../contracts/test/TestERC721.sol"; -import { TestERC1155 } from "../../contracts/test/TestERC1155.sol"; -import { TestERC20 } from "../../contracts/test/TestERC20.sol"; -import { ProxyRegistry } from "./interfaces/ProxyRegistry.sol"; -import { OwnableDelegateProxy } from "./interfaces/OwnableDelegateProxy.sol"; -import { Merkle } from "../../lib/murky/src/Merkle.sol"; import { stdError } from "forge-std/Test.sol"; +import { ArithmeticUtil } from "./utils/ArithmeticUtil.sol"; contract MatchAdvancedOrder is BaseOrderTest { + using ArithmeticUtil for uint256; + using ArithmeticUtil for uint128; + using ArithmeticUtil for uint120; + + FuzzInputs empty; + struct FuzzInputs { address zone; uint256 id; @@ -24,44 +25,77 @@ contract MatchAdvancedOrder is BaseOrderTest { uint128 amount; bool useConduit; } - + struct FuzzInputsAscendingDescending { + address zone; + uint256 id; + bytes32 zoneHash; + uint256 salt; + uint128 baseStart; + uint128 baseEnd; + uint120 multiplier; + uint120 fractionalComponent; + bool useConduit; + uint256 warp; + } struct Context { ConsiderationInterface consideration; FuzzInputs args; + ItemType itemType; + } + struct ContextAscendingDescending { + ConsiderationInterface consideration; + FuzzInputsAscendingDescending args; + } + + function test(function(Context memory) external fn, Context memory context) + internal + { + try fn(context) {} catch (bytes memory reason) { + assertPass(reason); + } + } + + function test( + function(ContextAscendingDescending memory) external fn, + ContextAscendingDescending memory context + ) internal { + try fn(context) {} catch (bytes memory reason) { + assertPass(reason); + } } function testMatchAdvancedOrdersOverflowOrderSide() public { // start at 1 to skip eth - for (uint256 i = 1; i < 4; i++) { + for (uint256 i = 1; i < 4; ++i) { // skip 721s if (i == 2) { continue; } - _testMatchAdvancedOrdersOverflowOrderSide( - consideration, - ItemType(i) + test( + this.matchAdvancedOrdersOverflowOrderSide, + Context(consideration, empty, ItemType(i)) ); - _testMatchAdvancedOrdersOverflowOrderSide( - referenceConsideration, - ItemType(i) + test( + this.matchAdvancedOrdersOverflowOrderSide, + Context(consideration, empty, ItemType(i)) ); } } function testMatchAdvancedOrdersOverflowConsiderationSide() public { // start at 1 to skip eth - for (uint256 i = 1; i < 4; i++) { + for (uint256 i = 1; i < 4; ++i) { // skip 721s if (i == 2) { continue; } - _testMatchAdvancedOrdersOverflowConsiderationSide( - consideration, - ItemType(i) + test( + this.matchAdvancedOrdersOverflowConsiderationSide, + Context(consideration, empty, ItemType(i)) ); - _testMatchAdvancedOrdersOverflowConsiderationSide( - referenceConsideration, - ItemType(i) + test( + this.matchAdvancedOrdersOverflowConsiderationSide, + Context(consideration, empty, ItemType(i)) ); } } @@ -69,20 +103,53 @@ contract MatchAdvancedOrder is BaseOrderTest { function testMatchAdvancedOrdersWithEmptyCriteriaEthToErc721( FuzzInputs memory args ) public { - _testMatchAdvancedOrdersWithEmptyCriteriaEthToErc721( - Context(referenceConsideration, args) + vm.assume(args.amount > 0); + test( + this.matchAdvancedOrdersWithEmptyCriteriaEthToErc721, + Context(referenceConsideration, args, ItemType(0)) + ); + test( + this.matchAdvancedOrdersWithEmptyCriteriaEthToErc721, + Context(consideration, args, ItemType(0)) + ); + } + + function testMatchOrdersAscendingDescendingOfferAmountPartialFill( + FuzzInputsAscendingDescending memory args + ) public { + vm.assume(args.baseStart != args.baseEnd); + vm.assume(args.baseStart > 0 && args.baseEnd > 0); + test( + this.matchOrdersAscendingDescendingOfferAmountPartialFill, + ContextAscendingDescending(consideration, args) + ); + test( + this.matchOrdersAscendingDescendingOfferAmountPartialFill, + ContextAscendingDescending(referenceConsideration, args) + ); + } + + function testMatchOrdersAscendingDescendingConsiderationAmountPartialFill( + FuzzInputsAscendingDescending memory args + ) public { + vm.assume(args.baseStart != args.baseEnd); + vm.assume(args.baseStart > 0 && args.baseEnd > 0); + test( + this.matchOrdersAscendingDescendingConsiderationAmountPartialFill, + ContextAscendingDescending(consideration, args) ); - _testMatchAdvancedOrdersWithEmptyCriteriaEthToErc721( - Context(consideration, args) + test( + this.matchOrdersAscendingDescendingConsiderationAmountPartialFill, + ContextAscendingDescending(referenceConsideration, args) ); } - function _testMatchAdvancedOrdersOverflowOrderSide( - ConsiderationInterface _consideration, - ItemType itemType - ) internal resetTokenBalancesBetweenRuns { - _configureOfferItem(itemType, 1, 100); - _configureErc721ConsiderationItem(alice, 1); + function matchAdvancedOrdersOverflowOrderSide(Context memory context) + external + stateless + { + addOfferItem(context.itemType, 1, 100); + addErc721ConsiderationItem(alice, 1); OrderParameters memory firstOrderParameters = OrderParameters( address(bob), @@ -100,19 +167,19 @@ contract MatchAdvancedOrder is BaseOrderTest { OrderComponents memory firstOrderComponents = getOrderComponents( firstOrderParameters, - _consideration.getNonce(bob) + context.consideration.getCounter(bob) ); bytes memory firstSignature = signOrder( - _consideration, + context.consideration, bobPk, - _consideration.getOrderHash(firstOrderComponents) + context.consideration.getOrderHash(firstOrderComponents) ); delete offerItems; delete considerationItems; - _configureOfferItem(itemType, 1, 2**256 - 1); - _configureErc721ConsiderationItem(alice, 2); + addOfferItem(context.itemType, 1, 2**256 - 1); + addErc721ConsiderationItem(alice, 2); OrderParameters memory secondOrderParameters = OrderParameters( address(bob), @@ -130,12 +197,12 @@ contract MatchAdvancedOrder is BaseOrderTest { OrderComponents memory secondOrderComponents = getOrderComponents( secondOrderParameters, - _consideration.getNonce(bob) + context.consideration.getCounter(bob) ); bytes memory secondSignature = signOrder( - _consideration, + context.consideration, bobPk, - _consideration.getOrderHash(secondOrderComponents) + context.consideration.getOrderHash(secondOrderComponents) ); delete offerItems; @@ -143,9 +210,9 @@ contract MatchAdvancedOrder is BaseOrderTest { test721_1.mint(alice, 1); test721_1.mint(alice, 2); - _configureERC721OfferItem(1); - _configureERC721OfferItem(2); - _configureConsiderationItem(bob, itemType, 1, 99); + addErc721OfferItem(1); + addErc721OfferItem(2); + addConsiderationItem(bob, context.itemType, 1, 99); OrderParameters memory thirdOrderParameters = OrderParameters( address(alice), @@ -163,13 +230,13 @@ contract MatchAdvancedOrder is BaseOrderTest { OrderComponents memory thirdOrderComponents = getOrderComponents( thirdOrderParameters, - _consideration.getNonce(alice) + context.consideration.getCounter(alice) ); bytes memory thirdSignature = signOrder( - _consideration, + context.consideration, alicePk, - _consideration.getOrderHash(thirdOrderComponents) + context.consideration.getOrderHash(thirdOrderComponents) ); delete offerItems; @@ -234,20 +301,19 @@ contract MatchAdvancedOrder is BaseOrderTest { delete fulfillment; vm.expectRevert(stdError.arithmeticError); - _consideration.matchAdvancedOrders{ value: 99 }( + context.consideration.matchAdvancedOrders{ value: 99 }( advancedOrders, new CriteriaResolver[](0), fulfillments ); } - function _testMatchAdvancedOrdersOverflowConsiderationSide( - ConsiderationInterface _consideration, - ItemType itemType - ) internal resetTokenBalancesBetweenRuns { + function matchAdvancedOrdersOverflowConsiderationSide( + Context memory context + ) external stateless { test721_1.mint(alice, 1); - _configureERC721OfferItem(1); - _configureConsiderationItem(alice, itemType, 1, 100); + addErc721OfferItem(1); + addConsiderationItem(alice, context.itemType, 1, 100); OrderParameters memory firstOrderParameters = OrderParameters( address(alice), @@ -265,20 +331,20 @@ contract MatchAdvancedOrder is BaseOrderTest { OrderComponents memory firstOrderComponents = getOrderComponents( firstOrderParameters, - _consideration.getNonce(alice) + context.consideration.getCounter(alice) ); bytes memory firstSignature = signOrder( - _consideration, + context.consideration, alicePk, - _consideration.getOrderHash(firstOrderComponents) + context.consideration.getOrderHash(firstOrderComponents) ); delete offerItems; delete considerationItems; test721_1.mint(bob, 2); - _configureERC721OfferItem(2); - _configureConsiderationItem(alice, itemType, 1, 2**256 - 1); + addErc721OfferItem(2); + addConsiderationItem(alice, context.itemType, 1, 2**256 - 1); OrderParameters memory secondOrderParameters = OrderParameters( address(bob), @@ -296,20 +362,20 @@ contract MatchAdvancedOrder is BaseOrderTest { OrderComponents memory secondOrderComponents = getOrderComponents( secondOrderParameters, - _consideration.getNonce(bob) + context.consideration.getCounter(bob) ); bytes memory secondSignature = signOrder( - _consideration, + context.consideration, bobPk, - _consideration.getOrderHash(secondOrderComponents) + context.consideration.getOrderHash(secondOrderComponents) ); delete offerItems; delete considerationItems; - _configureOfferItem(itemType, 1, 99); - _configureErc721ConsiderationItem(alice, 1); - _configureErc721ConsiderationItem(bob, 2); + addOfferItem(context.itemType, 1, 99); + addErc721ConsiderationItem(alice, 1); + addErc721ConsiderationItem(bob, 2); OrderParameters memory thirdOrderParameters = OrderParameters( address(bob), @@ -327,13 +393,13 @@ contract MatchAdvancedOrder is BaseOrderTest { OrderComponents memory thirdOrderComponents = getOrderComponents( thirdOrderParameters, - _consideration.getNonce(bob) + context.consideration.getCounter(bob) ); bytes memory thirdSignature = signOrder( - _consideration, + context.consideration, bobPk, - _consideration.getOrderHash(thirdOrderComponents) + context.consideration.getOrderHash(thirdOrderComponents) ); delete offerItems; @@ -398,23 +464,16 @@ contract MatchAdvancedOrder is BaseOrderTest { delete fulfillment; vm.expectRevert(stdError.arithmeticError); - _consideration.matchAdvancedOrders{ value: 99 }( + context.consideration.matchAdvancedOrders{ value: 99 }( advancedOrders, new CriteriaResolver[](0), fulfillments ); } - function _testMatchAdvancedOrdersWithEmptyCriteriaEthToErc721( + function matchAdvancedOrdersWithEmptyCriteriaEthToErc721( Context memory context - ) - internal - onlyPayable(context.args.zone) - topUp - resetTokenBalancesBetweenRuns - { - vm.assume(context.args.amount > 0); - + ) external stateless { bytes32 conduitKey = context.args.useConduit ? conduitKeyOne : bytes32(0); @@ -456,7 +515,7 @@ contract MatchAdvancedOrder is BaseOrderTest { ); OrderComponents memory orderComponents = getOrderComponents( orderParameters, - context.consideration.getNonce(alice) + context.consideration.getCounter(alice) ); bytes memory signature = signOrder( context.consideration, @@ -504,7 +563,7 @@ contract MatchAdvancedOrder is BaseOrderTest { OrderComponents memory mirrorOrderComponents = getOrderComponents( mirrorOrderParameters, - context.consideration.getNonce(cal) + context.consideration.getCounter(cal) ); bytes memory mirrorSignature = signOrder( @@ -548,4 +607,317 @@ contract MatchAdvancedOrder is BaseOrderTest { fulfillments ); } + + function matchOrdersAscendingDescendingOfferAmountPartialFill( + ContextAscendingDescending memory context + ) external stateless { + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test1155_1.mint(bob, context.args.id, 20); + token1.mint( + alice, + context.args.baseEnd > context.args.baseStart + ? context.args.baseEnd.mul(20) + : context.args.baseStart.mul(20) + ); + + emit log_named_uint( + "start amount * final multiplier", + context.args.baseStart.mul(20) + ); + emit log_named_uint( + "end amount * final multiplier", + context.args.baseEnd.mul(20) + ); + // multiply start and end amounts by multiplier and fractional component + addOfferItem( + ItemType.ERC20, + 0, + context.args.baseStart.mul(20), + context.args.baseEnd.mul(20) + ); + addConsiderationItem(alice, ItemType.ERC1155, context.args.id, 20); + + uint256 startTime = block.timestamp; + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.PARTIAL_OPEN, + startTime, + startTime + 1000, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length + ); + + OrderComponents memory orderComponents = getOrderComponents( + orderParameters, + context.consideration.getCounter(alice) + ); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + delete offerItems; + delete considerationItems; + + vm.warp(startTime + 500); + + // current amount should be mean of start and end amounts + uint256 currentAmount = _locateCurrentAmount( + context.args.baseStart.mul(20), // start amount + context.args.baseEnd.mul(20), // end amount + startTime, // startTime + startTime + 1000, // endTime + false // don't round up offers + ); + + emit log_named_uint("current amount", currentAmount); + emit log_named_uint( + "current amount scaled down by partial fill", + currentAmount.mul(2) / 10 + ); + + addErc1155OfferItem(context.args.id, 20); + // create mirror consideration item with current amount + addErc20ConsiderationItem(bob, currentAmount); + + OrderParameters memory mirrorOrderParameters = OrderParameters( + address(bob), + context.args.zone, + offerItems, + considerationItems, + OrderType.PARTIAL_OPEN, + startTime, + startTime + 1000, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length + ); + OrderComponents memory mirrorOrderComponents = getOrderComponents( + mirrorOrderParameters, + context.consideration.getCounter(bob) + ); + + bytes memory mirrorSignature = signOrder( + context.consideration, + bobPk, + context.consideration.getOrderHash(mirrorOrderComponents) + ); + + AdvancedOrder[] memory orders = new AdvancedOrder[](2); + // create advanced order with multiplier and fractional component as numerator and denominator + orders[0] = AdvancedOrder(orderParameters, 2, 10, signature, "0x"); + // also tried scaling down current amount and passing in full open order + orders[1] = AdvancedOrder( + mirrorOrderParameters, + 2, + 10, + mirrorSignature, + "0x" + ); + + fulfillmentComponent = FulfillmentComponent(0, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.offerComponents = fulfillmentComponents; + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(1, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.considerationComponents = fulfillmentComponents; + fulfillments.push(fulfillment); + delete fulfillmentComponents; + delete fulfillment; + + fulfillmentComponent = FulfillmentComponent(1, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.offerComponents = fulfillmentComponents; + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(0, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.considerationComponents = fulfillmentComponents; + fulfillments.push(fulfillment); + delete fulfillmentComponents; + delete fulfillment; + + uint256 balanceBeforeOrder = token1.balanceOf(bob); + context.consideration.matchAdvancedOrders( + orders, + new CriteriaResolver[](0), + fulfillments + ); + uint256 balanceAfterOrder = token1.balanceOf(bob); + // check the difference in alice's balance is equal to partial fill of current amount + assertEq( + balanceAfterOrder - balanceBeforeOrder, + currentAmount.mul(2) / 10 + ); + } + + function matchOrdersAscendingDescendingConsiderationAmountPartialFill( + ContextAscendingDescending memory context + ) external stateless { + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test1155_1.mint(alice, context.args.id, 20); + token1.mint( + bob, + context.args.baseEnd > context.args.baseStart + ? context.args.baseEnd.mul(20) + : context.args.baseStart.mul(20) + ); + + emit log_named_uint( + "start amount * final multiplier", + context.args.baseStart.mul(20) + ); + emit log_named_uint( + "end amount * final multiplier", + context.args.baseEnd.mul(20) + ); + // multiply start and end amounts by multiplier and fractional component + addOfferItem(ItemType.ERC1155, context.args.id, 20, 20); + addErc20ConsiderationItem( + alice, + context.args.baseStart.mul(20), + context.args.baseEnd.mul(20) + ); + + uint256 startTime = block.timestamp; + OrderParameters memory orderParameters = OrderParameters( + address(alice), + context.args.zone, + offerItems, + considerationItems, + OrderType.PARTIAL_OPEN, + startTime, + startTime + 1000, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length + ); + + OrderComponents memory orderComponents = getOrderComponents( + orderParameters, + context.consideration.getCounter(alice) + ); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(orderComponents) + ); + + delete offerItems; + delete considerationItems; + + vm.warp(startTime + 500); + + // current amount should be mean of start and end amounts + uint256 currentAmount = _locateCurrentAmount( + context.args.baseStart.mul(20), // start amount + context.args.baseEnd.mul(20), // end amount + startTime, // startTime + startTime + 1000, // endTime + true // round up considerations + ); + + emit log_named_uint("current amount", currentAmount); + emit log_named_uint( + "current amount scaled down by partial fill", + currentAmount.mul(2) / 10 + ); + + addOfferItem( + ItemType.ERC20, + address(token1), + 0, + currentAmount, + currentAmount + ); + // create mirror consideration item with current amount + addErc1155ConsiderationItem(bob, context.args.id, 20); + + OrderParameters memory mirrorOrderParameters = OrderParameters( + address(bob), + context.args.zone, + offerItems, + considerationItems, + OrderType.PARTIAL_OPEN, + block.timestamp, + block.timestamp + 1000, + context.args.zoneHash, + context.args.salt, + conduitKey, + considerationItems.length + ); + OrderComponents memory mirrorOrderComponents = getOrderComponents( + mirrorOrderParameters, + context.consideration.getCounter(bob) + ); + + bytes memory mirrorSignature = signOrder( + context.consideration, + bobPk, + context.consideration.getOrderHash(mirrorOrderComponents) + ); + + AdvancedOrder[] memory orders = new AdvancedOrder[](2); + // create advanced order with multiplier and fractional component as numerator and denominator + orders[0] = AdvancedOrder(orderParameters, 2, 10, signature, "0x"); + // also tried scaling down current amount and passing in full open order + orders[1] = AdvancedOrder( + mirrorOrderParameters, + 2, + 10, + mirrorSignature, + "0x" + ); + + fulfillmentComponent = FulfillmentComponent(0, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.offerComponents = fulfillmentComponents; + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(1, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.considerationComponents = fulfillmentComponents; + fulfillments.push(fulfillment); + delete fulfillmentComponents; + delete fulfillment; + + fulfillmentComponent = FulfillmentComponent(1, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.offerComponents = fulfillmentComponents; + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(0, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.considerationComponents = fulfillmentComponents; + fulfillments.push(fulfillment); + delete fulfillmentComponents; + delete fulfillment; + + uint256 balanceBeforeOrder = token1.balanceOf(alice); + context.consideration.matchAdvancedOrders( + orders, + new CriteriaResolver[](0), + fulfillments + ); + uint256 balanceAfterOrder = token1.balanceOf(alice); + // check the difference in alice's balance is equal to partial fill of current amount + assertEq( + balanceAfterOrder - balanceBeforeOrder, + currentAmount.mul(2) / 10 + ); + } } diff --git a/test/foundry/MatchOrders.t.sol b/test/foundry/MatchOrders.t.sol index 02be133ac..39dec3dfc 100644 --- a/test/foundry/MatchOrders.t.sol +++ b/test/foundry/MatchOrders.t.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { OrderType, ItemType } from "../../contracts/lib/ConsiderationEnums.sol"; import { Order, Fulfillment, OfferItem, OrderParameters, ConsiderationItem, OrderComponents, FulfillmentComponent } from "../../contracts/lib/ConsiderationStructs.sol"; @@ -10,9 +10,11 @@ import { BaseOrderTest } from "./utils/BaseOrderTest.sol"; import { TestERC721 } from "../../contracts/test/TestERC721.sol"; import { TestERC1155 } from "../../contracts/test/TestERC1155.sol"; import { TestERC20 } from "../../contracts/test/TestERC20.sol"; +import { ArithmeticUtil } from "./utils/ArithmeticUtil.sol"; import { stdError } from "forge-std/Test.sol"; contract MatchOrders is BaseOrderTest { + using ArithmeticUtil for uint128; struct FuzzInputsCommon { address zone; uint256 id; @@ -22,125 +24,314 @@ contract MatchOrders is BaseOrderTest { bool useConduit; } + struct FuzzInputsAscendingDescending { + address zone; + uint256 id; + bytes32 zoneHash; + uint256 salt; + uint128 amount; + bool useConduit; + uint256 warp; + } + struct Context { ConsiderationInterface consideration; FuzzInputsCommon args; } + struct ContextAscendingDescending { + ConsiderationInterface consideration; + FuzzInputsAscendingDescending args; + } + + modifier validateInputs(Context memory context) { + vm.assume( + context.args.paymentAmts[0] > 0 && + context.args.paymentAmts[1] > 0 && + context.args.paymentAmts[2] > 0 + ); + vm.assume( + uint256(context.args.paymentAmts[0]) + + uint256(context.args.paymentAmts[1]) + + uint256(context.args.paymentAmts[2]) <= + 2**128 - 1 + ); + _; + } + + modifier validateInputsAscendingDescending( + ContextAscendingDescending memory context + ) { + vm.assume(context.args.amount > 100); + vm.assume(uint256(context.args.amount) * 2 <= 2**128 - 1); + vm.assume(context.args.warp > 10 && context.args.warp < 1000); + _; + } + + function test(function(Context memory) external fn, Context memory context) + internal + { + try fn(context) {} catch (bytes memory reason) { + assertPass(reason); + } + } + + function testAscendingDescending( + function(ContextAscendingDescending memory) external fn, + ContextAscendingDescending memory context + ) internal { + try fn(context) {} catch (bytes memory reason) { + assertPass(reason); + } + } + + function testOverflow( + function(Context memory, ItemType) external fn, + Context memory context, + ItemType itemType + ) internal { + try fn(context, itemType) {} catch (bytes memory reason) { + assertPass(reason); + } + } + function testMatchOrdersSingleErc721OfferSingleEthConsideration( FuzzInputsCommon memory inputs - ) public { - _testMatchOrdersSingleErc721OfferSingleEthConsideration( - Context(referenceConsideration, inputs) - ); - _testMatchOrdersSingleErc721OfferSingleEthConsideration( + ) public validateInputs(Context(consideration, inputs)) { + addErc721OfferItem(inputs.id); + addEthConsiderationItem(alice, 1); + _configureOrderParameters( + alice, + inputs.zone, + inputs.zoneHash, + inputs.salt, + inputs.useConduit + ); + _configureOrderComponents(consideration.getCounter(alice)); + test( + this.matchOrdersSingleErc721OfferSingleEthConsideration, Context(consideration, inputs) ); + test( + this.matchOrdersSingleErc721OfferSingleEthConsideration, + Context(referenceConsideration, inputs) + ); } - function testMatchOrdersOverflowOrderSide() public { - // start at 1 to skip eth - for (uint256 i = 1; i < 4; i++) { - // skip 721s + function testMatchOrdersOverflowOfferSide(FuzzInputsCommon memory inputs) + public + validateInputs(Context(consideration, inputs)) + { + for (uint256 i = 1; i < 4; ++i) { if (i == 2) { continue; } - _testMatchOrdersOverflowOrderSide(consideration, ItemType(i)); - _testMatchOrdersOverflowOrderSide( - referenceConsideration, + testOverflow( + this.matchOrdersOverflowOfferSide, + Context(referenceConsideration, inputs), ItemType(i) ); + testOverflow( + this.matchOrdersOverflowOfferSide, + Context(consideration, inputs), + ItemType(i) + ); + delete offerItems; + delete considerationItems; } } - function testMatchOrdersOverflowConsiderationSide() public { + function testMatchOrdersOverflowConsiderationSide( + FuzzInputsCommon memory inputs + ) public validateInputs(Context(consideration, inputs)) { // start at 1 to skip eth - for (uint256 i = 1; i < 4; i++) { - // skip 721s + for (uint256 i = 1; i < 4; ++i) { if (i == 2) { continue; } - _testMatchOrdersOverflowConsiderationSide( - consideration, + testOverflow( + this.matchOrdersOverflowConsiderationSide, + Context(referenceConsideration, inputs), ItemType(i) ); - _testMatchOrdersOverflowConsiderationSide( - referenceConsideration, + testOverflow( + this.matchOrdersOverflowConsiderationSide, + Context(consideration, inputs), ItemType(i) ); + delete offerItems; + delete considerationItems; } } + function testMatchOrdersAscendingOfferAmount( + FuzzInputsAscendingDescending memory inputs + ) + public + validateInputsAscendingDescending( + ContextAscendingDescending(consideration, inputs) + ) + { + addOfferItem(ItemType.ERC20, 0, inputs.amount, inputs.amount * 2); + addConsiderationItem(alice, ItemType.ERC721, inputs.id, 1); + _configureOrderParametersSetEndTime( + alice, + inputs.zone, + 1001, + inputs.zoneHash, + inputs.salt, + inputs.useConduit + ); + _configureOrderComponents(consideration.getCounter(alice)); + testAscendingDescending( + this.matchOrdersAscendingOfferAmount, + ContextAscendingDescending(referenceConsideration, inputs) + ); + testAscendingDescending( + this.matchOrdersAscendingOfferAmount, + ContextAscendingDescending(consideration, inputs) + ); + } + function testMatchOrdersAscendingConsiderationAmount( - FuzzInputsCommon memory inputs - ) public { - _testMatchOrdersAscendingConsiderationAmount( - Context(referenceConsideration, inputs) + FuzzInputsAscendingDescending memory inputs + ) + public + validateInputsAscendingDescending( + ContextAscendingDescending(consideration, inputs) + ) + { + addOfferItem(ItemType.ERC721, inputs.id, 1); + addErc20ConsiderationItem(alice, inputs.amount, inputs.amount.mul(2)); + _configureOrderParametersSetEndTime( + alice, + inputs.zone, + 1001, + inputs.zoneHash, + inputs.salt, + inputs.useConduit ); - _testMatchOrdersAscendingConsiderationAmount( - Context(consideration, inputs) + _configureOrderComponents(consideration.getCounter(alice)); + testAscendingDescending( + this.matchOrdersAscendingConsiderationAmount, + ContextAscendingDescending(referenceConsideration, inputs) + ); + testAscendingDescending( + this.matchOrdersAscendingConsiderationAmount, + ContextAscendingDescending(consideration, inputs) ); } - function _testMatchOrdersOverflowOrderSide( - ConsiderationInterface _consideration, - ItemType itemType - ) internal resetTokenBalancesBetweenRuns { - _configureOfferItem(itemType, 1, 100); - _configureErc721ConsiderationItem(alice, 1); + function testMatchOrdersDescendingOfferAmount( + FuzzInputsAscendingDescending memory inputs + ) + public + validateInputsAscendingDescending( + ContextAscendingDescending(consideration, inputs) + ) + { + addOfferItem(ItemType.ERC20, 0, inputs.amount * 2, inputs.amount); + addErc721ConsiderationItem(alice, inputs.id); + _configureOrderParametersSetEndTime( + alice, + inputs.zone, + 1001, + inputs.zoneHash, + inputs.salt, + inputs.useConduit + ); + _configureOrderComponents(consideration.getCounter(alice)); + testAscendingDescending( + this.matchOrdersDescendingOfferAmount, + ContextAscendingDescending(referenceConsideration, inputs) + ); + testAscendingDescending( + this.matchOrdersDescendingOfferAmount, + ContextAscendingDescending(consideration, inputs) + ); + } - OrderParameters memory firstOrderParameters = OrderParameters( - address(bob), - address(0), - offerItems, - considerationItems, - OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, - bytes32(0), - 0, - bytes32(0), - considerationItems.length + function testMatchOrdersDescendingConsiderationAmount( + FuzzInputsAscendingDescending memory inputs + ) + public + validateInputsAscendingDescending( + ContextAscendingDescending(consideration, inputs) + ) + { + addOfferItem(ItemType.ERC721, inputs.id, 1); + addErc20ConsiderationItem(alice, inputs.amount.mul(2), inputs.amount); + _configureOrderParametersSetEndTime( + alice, + inputs.zone, + 1001, + inputs.zoneHash, + inputs.salt, + inputs.useConduit + ); + _configureOrderComponents(consideration.getCounter(alice)); + testAscendingDescending( + this.matchOrdersDescendingConsiderationAmount, + ContextAscendingDescending(referenceConsideration, inputs) ); + testAscendingDescending( + this.matchOrdersDescendingConsiderationAmount, + ContextAscendingDescending(consideration, inputs) + ); + } - OrderComponents memory firstOrderComponents = getOrderComponents( - firstOrderParameters, - _consideration.getNonce(bob) + function matchOrdersOverflowOfferSide( + Context memory context, + ItemType itemType + ) external stateless { + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + addOfferItem(itemType, 1, 100); + addErc721ConsiderationItem(alice, 1); + _configureOrderParameters( + bob, + context.args.zone, + context.args.zoneHash, + context.args.salt, + context.args.useConduit ); - bytes memory firstSignature = signOrder( - _consideration, + _configureOrderComponents(consideration.getCounter(bob)); + bytes memory baseSignature = signOrder( + context.consideration, bobPk, - _consideration.getOrderHash(firstOrderComponents) + context.consideration.getOrderHash(baseOrderComponents) ); delete offerItems; delete considerationItems; - _configureOfferItem(itemType, 1, 2**256 - 1); - _configureErc721ConsiderationItem(alice, 2); + addOfferItem(itemType, 1, 2**256 - 1); + addErc721ConsiderationItem(alice, 2); OrderParameters memory secondOrderParameters = OrderParameters( address(bob), - address(0), + context.args.zone, offerItems, considerationItems, OrderType.FULL_OPEN, block.timestamp, block.timestamp + 1, - bytes32(0), - 0, - bytes32(0), + context.args.zoneHash, + context.args.salt, + conduitKey, considerationItems.length ); OrderComponents memory secondOrderComponents = getOrderComponents( secondOrderParameters, - _consideration.getNonce(bob) + context.consideration.getCounter(bob) ); bytes memory secondSignature = signOrder( - _consideration, + context.consideration, bobPk, - _consideration.getOrderHash(secondOrderComponents) + context.consideration.getOrderHash(secondOrderComponents) ); delete offerItems; @@ -148,40 +339,40 @@ contract MatchOrders is BaseOrderTest { test721_1.mint(alice, 1); test721_1.mint(alice, 2); - _configureERC721OfferItem(1); - _configureERC721OfferItem(2); - _configureConsiderationItem(bob, itemType, 1, 99); + addErc721OfferItem(1); + addErc721OfferItem(2); + addConsiderationItem(bob, itemType, 1, 99); OrderParameters memory thirdOrderParameters = OrderParameters( address(alice), - address(0), + context.args.zone, offerItems, considerationItems, OrderType.FULL_OPEN, block.timestamp, block.timestamp + 1, - bytes32(0), - 0, - bytes32(0), + context.args.zoneHash, + context.args.salt, + conduitKey, considerationItems.length ); OrderComponents memory thirdOrderComponents = getOrderComponents( thirdOrderParameters, - _consideration.getNonce(alice) + context.consideration.getCounter(alice) ); bytes memory thirdSignature = signOrder( - _consideration, + context.consideration, alicePk, - _consideration.getOrderHash(thirdOrderComponents) + context.consideration.getOrderHash(thirdOrderComponents) ); delete offerItems; delete considerationItems; Order[] memory orders = new Order[](3); - orders[0] = Order(firstOrderParameters, firstSignature); + orders[0] = Order(baseOrderParameters, baseSignature); orders[1] = Order(secondOrderParameters, secondSignature); orders[2] = Order(thirdOrderParameters, thirdSignature); @@ -221,102 +412,106 @@ contract MatchOrders is BaseOrderTest { delete fulfillment; vm.expectRevert(stdError.arithmeticError); - _consideration.matchOrders{ value: 99 }(orders, fulfillments); + context.consideration.matchOrders{ value: 99 }(orders, fulfillments); } - function _testMatchOrdersOverflowConsiderationSide( - ConsiderationInterface _consideration, + function matchOrdersOverflowConsiderationSide( + Context memory context, ItemType itemType - ) internal resetTokenBalancesBetweenRuns { + ) external stateless { + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + test721_1.mint(alice, 1); - _configureERC721OfferItem(1); - _configureConsiderationItem(alice, itemType, 1, 100); + addErc721OfferItem(1); + addConsiderationItem(alice, itemType, 1, 100); OrderParameters memory firstOrderParameters = OrderParameters( address(alice), - address(0), + context.args.zone, offerItems, considerationItems, OrderType.FULL_OPEN, block.timestamp, block.timestamp + 1, - bytes32(0), - 0, - bytes32(0), + context.args.zoneHash, + context.args.salt, + conduitKey, considerationItems.length ); OrderComponents memory firstOrderComponents = getOrderComponents( firstOrderParameters, - _consideration.getNonce(alice) + context.consideration.getCounter(alice) ); bytes memory firstSignature = signOrder( - _consideration, + context.consideration, alicePk, - _consideration.getOrderHash(firstOrderComponents) + context.consideration.getOrderHash(firstOrderComponents) ); delete offerItems; delete considerationItems; test721_1.mint(bob, 2); - _configureERC721OfferItem(2); - _configureConsiderationItem(alice, itemType, 1, 2**256 - 1); + addErc721OfferItem(2); + addConsiderationItem(alice, itemType, 1, 2**256 - 1); OrderParameters memory secondOrderParameters = OrderParameters( address(bob), - address(0), + context.args.zone, offerItems, considerationItems, OrderType.FULL_OPEN, block.timestamp, block.timestamp + 1, - bytes32(0), - 0, - bytes32(0), + context.args.zoneHash, + context.args.salt, + conduitKey, considerationItems.length ); OrderComponents memory secondOrderComponents = getOrderComponents( secondOrderParameters, - _consideration.getNonce(bob) + context.consideration.getCounter(bob) ); bytes memory secondSignature = signOrder( - _consideration, + context.consideration, bobPk, - _consideration.getOrderHash(secondOrderComponents) + context.consideration.getOrderHash(secondOrderComponents) ); delete offerItems; delete considerationItems; - _configureOfferItem(itemType, 1, 99); - _configureErc721ConsiderationItem(alice, 1); - _configureErc721ConsiderationItem(bob, 2); + addOfferItem(itemType, 1, 99); + addErc721ConsiderationItem(alice, 1); + addErc721ConsiderationItem(bob, 2); OrderParameters memory thirdOrderParameters = OrderParameters( address(bob), - address(0), + context.args.zone, offerItems, considerationItems, OrderType.FULL_OPEN, block.timestamp, block.timestamp + 1, - bytes32(0), - 0, - bytes32(0), + context.args.zoneHash, + context.args.salt, + conduitKey, considerationItems.length ); OrderComponents memory thirdOrderComponents = getOrderComponents( thirdOrderParameters, - _consideration.getNonce(bob) + context.consideration.getCounter(bob) ); bytes memory thirdSignature = signOrder( - _consideration, + context.consideration, bobPk, - _consideration.getOrderHash(thirdOrderComponents) + context.consideration.getOrderHash(thirdOrderComponents) ); delete offerItems; @@ -363,124 +558,224 @@ contract MatchOrders is BaseOrderTest { delete fulfillment; vm.expectRevert(stdError.arithmeticError); - _consideration.matchOrders(orders, fulfillments); + context.consideration.matchOrders(orders, fulfillments); } - function _testMatchOrdersSingleErc721OfferSingleEthConsideration( + function matchOrdersSingleErc721OfferSingleEthConsideration( Context memory context - ) internal resetTokenBalancesBetweenRuns { - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 + ) external stateless { + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test721_1.mint(alice, context.args.id); + + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(baseOrderComponents) ); - vm.assume( - uint256(context.args.paymentAmts[0]) + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) <= - 2**128 - 1 + + OrderParameters + memory mirrorOrderParameters = createMirrorOrderParameters( + baseOrderParameters, + cal, + context.args.zone, + conduitKey + ); + + OrderComponents memory mirrorOrderComponents = getOrderComponents( + mirrorOrderParameters, + context.consideration.getCounter(cal) ); + + bytes memory mirrorSignature = signOrder( + context.consideration, + calPk, + context.consideration.getOrderHash(mirrorOrderComponents) + ); + + Order[] memory orders = new Order[](2); + orders[0] = Order(baseOrderParameters, signature); + orders[1] = Order(mirrorOrderParameters, mirrorSignature); + + fulfillmentComponent = FulfillmentComponent(0, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.offerComponents = fulfillmentComponents; + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(1, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.considerationComponents = fulfillmentComponents; + fulfillments.push(fulfillment); + delete fulfillmentComponents; + delete fulfillment; + + fulfillmentComponent = FulfillmentComponent(1, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.offerComponents = fulfillmentComponents; + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(0, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.considerationComponents = fulfillmentComponents; + fulfillments.push(fulfillment); + delete fulfillmentComponents; + delete fulfillment; + + context.consideration.matchOrders{ + value: context.args.paymentAmts[0] + + context.args.paymentAmts[1] + + context.args.paymentAmts[2] + }(orders, fulfillments); + } + + function matchOrdersAscendingOfferAmount( + ContextAscendingDescending memory context + ) external stateless { bytes32 conduitKey = context.args.useConduit ? conduitKeyOne : bytes32(0); - test721_1.mint(alice, context.args.id); + test721_1.mint(bob, context.args.id); - offerItems.push( - OfferItem( - ItemType.ERC721, - address(test721_1), - context.args.id, - 1, - 1 - ) - ); - considerationItems.push( - ConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - uint256(1), - uint256(1), - payable(alice) - ) - ); - - OrderParameters memory orderParameters = OrderParameters( - address(alice), + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(baseOrderComponents) + ); + + delete offerItems; + delete considerationItems; + + uint256 startTime = 1; + vm.warp(startTime + context.args.warp); + + uint256 currentAmount = _locateCurrentAmount( + context.args.amount, // start amount + context.args.amount * 2, // end amount + startTime, // startTime + startTime + 1000, // endTime + false // don't round up offers + ); + + addOfferItem(ItemType.ERC721, context.args.id, 1); + addErc20ConsiderationItem(bob, currentAmount); + + OrderParameters memory mirrorOrderParameters = OrderParameters( + address(bob), context.args.zone, offerItems, considerationItems, OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, + 1, + 1001, context.args.zoneHash, context.args.salt, conduitKey, considerationItems.length ); + OrderComponents memory mirrorOrderComponents = getOrderComponents( + mirrorOrderParameters, + context.consideration.getCounter(bob) + ); - OrderComponents memory orderComponents = getOrderComponents( - orderParameters, - context.consideration.getNonce(alice) + bytes memory mirrorSignature = signOrder( + context.consideration, + bobPk, + context.consideration.getOrderHash(mirrorOrderComponents) ); + Order[] memory orders = new Order[](2); + orders[0] = Order(baseOrderParameters, signature); + orders[1] = Order(mirrorOrderParameters, mirrorSignature); + + fulfillmentComponent = FulfillmentComponent(0, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.offerComponents = fulfillmentComponents; + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(1, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.considerationComponents = fulfillmentComponents; + fulfillments.push(fulfillment); + delete fulfillmentComponents; + delete fulfillment; + + fulfillmentComponent = FulfillmentComponent(1, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.offerComponents = fulfillmentComponents; + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(0, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.considerationComponents = fulfillmentComponents; + fulfillments.push(fulfillment); + delete fulfillmentComponents; + delete fulfillment; + + vm.warp(1 + context.args.warp); + + uint256 balanceBeforeOrder = token1.balanceOf(bob); + context.consideration.matchOrders(orders, fulfillments); + uint256 balanceAfterOrder = token1.balanceOf(bob); + // check the difference in alice's balance is equal to endAmount of offer item + assertEq(balanceAfterOrder - balanceBeforeOrder, currentAmount); + } + + function matchOrdersAscendingConsiderationAmount( + ContextAscendingDescending memory context + ) external stateless { + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test721_1.mint(alice, context.args.id); + bytes memory signature = signOrder( context.consideration, alicePk, - context.consideration.getOrderHash(orderComponents) + context.consideration.getOrderHash(baseOrderComponents) ); - OfferItem[] memory mirrorOfferItems = new OfferItem[](1); - - // push the original order's consideration item into mirrorOfferItems - mirrorOfferItems[0] = OfferItem( - considerationItems[0].itemType, - considerationItems[0].token, - considerationItems[0].identifierOrCriteria, - considerationItems[0].startAmount, - considerationItems[0].endAmount - ); + delete offerItems; + delete considerationItems; - ConsiderationItem[] - memory mirrorConsiderationItems = new ConsiderationItem[](1); + uint256 startTime = 1; + vm.warp(startTime + context.args.warp); - // push the original order's offer item into mirrorConsiderationItems - mirrorConsiderationItems[0] = ConsiderationItem( - offerItems[0].itemType, - offerItems[0].token, - offerItems[0].identifierOrCriteria, - offerItems[0].startAmount, - offerItems[0].endAmount, - payable(cal) + uint256 currentAmount = _locateCurrentAmount( + context.args.amount, // start amount + context.args.amount * 2, // end amount + startTime, // startTime + startTime + 1000, // endTime + true // round up considerations ); + addOfferItem(ItemType.ERC20, 0, currentAmount, currentAmount); + addConsiderationItem(bob, ItemType.ERC721, context.args.id, 1); OrderParameters memory mirrorOrderParameters = OrderParameters( - address(cal), + address(bob), context.args.zone, - mirrorOfferItems, - mirrorConsiderationItems, + offerItems, + considerationItems, OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1, + 1, + 1001, context.args.zoneHash, context.args.salt, conduitKey, - mirrorConsiderationItems.length + considerationItems.length ); OrderComponents memory mirrorOrderComponents = getOrderComponents( mirrorOrderParameters, - context.consideration.getNonce(cal) + context.consideration.getCounter(bob) ); bytes memory mirrorSignature = signOrder( context.consideration, - calPk, + bobPk, context.consideration.getOrderHash(mirrorOrderComponents) ); Order[] memory orders = new Order[](2); - orders[0] = Order(orderParameters, signature); + orders[0] = Order(baseOrderParameters, signature); orders[1] = Order(mirrorOrderParameters, mirrorSignature); fulfillmentComponent = FulfillmentComponent(0, 0); @@ -505,110 +800,170 @@ contract MatchOrders is BaseOrderTest { delete fulfillmentComponents; delete fulfillment; - context.consideration.matchOrders{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] - }(orders, fulfillments); + uint256 balanceBeforeOrder = token1.balanceOf(alice); + context.consideration.matchOrders(orders, fulfillments); + uint256 balanceAfterOrder = token1.balanceOf(alice); + // check the difference in alice's balance is equal to endAmount of offer item + assertEq(balanceAfterOrder - balanceBeforeOrder, currentAmount); } - function _testMatchOrdersAscendingConsiderationAmount( - Context memory context - ) internal resetTokenBalancesBetweenRuns { - vm.assume( - context.args.paymentAmts[0] > 0 && - context.args.paymentAmts[1] > 0 && - context.args.paymentAmts[2] > 0 - ); - vm.assume( - uint256(context.args.paymentAmts[0]) * - 2 + - uint256(context.args.paymentAmts[1]) + - uint256(context.args.paymentAmts[2]) <= - 2**128 - 1 - ); + function matchOrdersDescendingOfferAmount( + ContextAscendingDescending memory context + ) external stateless { bytes32 conduitKey = context.args.useConduit ? conduitKeyOne : bytes32(0); - test721_1.mint(alice, context.args.id); + test721_1.mint(bob, context.args.id); - _configureOfferItem(ItemType.ERC721, context.args.id, 1); - // set endAmount to 2 * startAmount - _configureEthConsiderationItem( - alice, - context.args.paymentAmts[0], - context.args.paymentAmts[0] * 2 + bytes memory signature = signOrder( + context.consideration, + alicePk, + context.consideration.getOrderHash(baseOrderComponents) ); - _configureEthConsiderationItem(alice, context.args.paymentAmts[1]); - _configureEthConsiderationItem(alice, context.args.paymentAmts[2]); - OrderParameters memory orderParameters = OrderParameters( - address(alice), + delete offerItems; + delete considerationItems; + + uint256 startTime = 1; + vm.warp(startTime + context.args.warp); + + uint256 currentAmount = _locateCurrentAmount( + context.args.amount * 2, // start amount + context.args.amount, // end amount + startTime, // startTime + startTime + 1000, // endTime + false // don't round up offers + ); + + addOfferItem(ItemType.ERC721, context.args.id, 1); + addErc20ConsiderationItem(bob, currentAmount); + + OrderParameters memory mirrorOrderParameters = OrderParameters( + address(bob), context.args.zone, offerItems, considerationItems, OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1000, + 1, + 1001, context.args.zoneHash, context.args.salt, conduitKey, considerationItems.length ); - OrderComponents memory orderComponents = getOrderComponents( - orderParameters, - context.consideration.getNonce(alice) + OrderComponents memory mirrorOrderComponents = getOrderComponents( + mirrorOrderParameters, + context.consideration.getCounter(bob) ); + bytes memory mirrorSignature = signOrder( + context.consideration, + bobPk, + context.consideration.getOrderHash(mirrorOrderComponents) + ); + + Order[] memory orders = new Order[](2); + orders[0] = Order(baseOrderParameters, signature); + orders[1] = Order(mirrorOrderParameters, mirrorSignature); + + fulfillmentComponent = FulfillmentComponent(0, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.offerComponents = fulfillmentComponents; + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(1, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.considerationComponents = fulfillmentComponents; + fulfillments.push(fulfillment); + delete fulfillmentComponents; + delete fulfillment; + + fulfillmentComponent = FulfillmentComponent(1, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.offerComponents = fulfillmentComponents; + delete fulfillmentComponents; + fulfillmentComponent = FulfillmentComponent(0, 0); + fulfillmentComponents.push(fulfillmentComponent); + fulfillment.considerationComponents = fulfillmentComponents; + fulfillments.push(fulfillment); + delete fulfillmentComponents; + delete fulfillment; + + uint256 balaceBeforeOrder = token1.balanceOf(bob); + context.consideration.matchOrders(orders, fulfillments); + uint256 balanceAfterOrder = token1.balanceOf(bob); + // check the difference in balance is equal to endAmount of offer item + assertEq(balanceAfterOrder - balaceBeforeOrder, currentAmount); + } + + function matchOrdersDescendingConsiderationAmount( + ContextAscendingDescending memory context + ) external stateless { + bytes32 conduitKey = context.args.useConduit + ? conduitKeyOne + : bytes32(0); + + test721_1.mint(alice, context.args.id); + bytes memory signature = signOrder( context.consideration, alicePk, - context.consideration.getOrderHash(orderComponents) + context.consideration.getOrderHash(baseOrderComponents) ); delete offerItems; delete considerationItems; - // minimum amount of eth required to match orders at endTime - uint256 sumOfPaymentAmts = uint256(context.args.paymentAmts[0]) * - 2 + - context.args.paymentAmts[1] + - context.args.paymentAmts[2]; + uint256 startTime = 1; + vm.warp(startTime + context.args.warp); - // aggregate original order's eth consideration items into one mirror offer item - _configureOfferItem(ItemType.NATIVE, 0, sumOfPaymentAmts); + uint256 currentAmount = _locateCurrentAmount( + context.args.amount * 2, // start amount + context.args.amount, // end amount + startTime, // startTime + startTime + 1000, // endTime + true // round up considerations + ); - // push the original order's offer item into mirrorConsiderationItems - _configureConsiderationItem(cal, ItemType.ERC721, context.args.id, 1); + emit log_named_uint("Current Amount: ", currentAmount); + + addOfferItem( + ItemType.ERC20, + address(token1), + 0, + currentAmount, + currentAmount + ); + addConsiderationItem(bob, ItemType.ERC721, context.args.id, 1); OrderParameters memory mirrorOrderParameters = OrderParameters( - address(cal), + address(bob), context.args.zone, offerItems, considerationItems, OrderType.FULL_OPEN, - block.timestamp, - block.timestamp + 1000, + 1, + 1001, context.args.zoneHash, context.args.salt, conduitKey, considerationItems.length ); + OrderComponents memory mirrorOrderComponents = getOrderComponents( mirrorOrderParameters, - context.consideration.getNonce(cal) + context.consideration.getCounter(bob) ); bytes memory mirrorSignature = signOrder( context.consideration, - calPk, + bobPk, context.consideration.getOrderHash(mirrorOrderComponents) ); Order[] memory orders = new Order[](2); - orders[0] = Order(orderParameters, signature); + orders[0] = Order(baseOrderParameters, signature); orders[1] = Order(mirrorOrderParameters, mirrorSignature); fulfillmentComponent = FulfillmentComponent(0, 0); @@ -628,40 +983,16 @@ contract MatchOrders is BaseOrderTest { delete fulfillmentComponents; fulfillmentComponent = FulfillmentComponent(0, 0); fulfillmentComponents.push(fulfillmentComponent); - fulfillmentComponent = FulfillmentComponent(0, 1); - fulfillmentComponents.push(fulfillmentComponent); - fulfillmentComponent = FulfillmentComponent(0, 2); - fulfillmentComponents.push(fulfillmentComponent); fulfillment.considerationComponents = fulfillmentComponents; fulfillments.push(fulfillment); delete fulfillmentComponents; delete fulfillment; - delete offerItems; - delete considerationItems; - - bytes32 orderHash = context.consideration.getOrderHash(orderComponents); - - // set blockTimestamp to right before endTime and set insufficient value for transaction - vm.warp(block.timestamp + 999); - vm.expectRevert( - ConsiderationEventsAndErrors.InsufficientEtherSupplied.selector - ); - context.consideration.matchOrders{ - value: context.args.paymentAmts[0] + - context.args.paymentAmts[1] + - context.args.paymentAmts[2] - }(orders, fulfillments); + uint256 balanceBeforeOrder = token1.balanceOf(alice); + context.consideration.matchOrders(orders, fulfillments); - // set transaction value to sum of eth consideration items (including endAmount of considerationItem[0]) - context.consideration.matchOrders{ value: sumOfPaymentAmts }( - orders, - fulfillments - ); - (, , uint256 totalFilled, uint256 totalSize) = context - .consideration - .getOrderStatus(orderHash); - assertEq(totalFilled, 1); - assertEq(totalSize, 1); + uint256 balanceAfterOrder = token1.balanceOf(alice); + // check the difference in alice's balance is equal to endAmount of offer item + assertEq(balanceAfterOrder - balanceBeforeOrder, currentAmount); } } diff --git a/test/foundry/NonReentrant.t.sol b/test/foundry/NonReentrant.t.sol index 64dec2a11..f23f60da3 100644 --- a/test/foundry/NonReentrant.t.sol +++ b/test/foundry/NonReentrant.t.sol @@ -1,26 +1,24 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { OrderType, BasicOrderType, ItemType, Side } from "../../contracts/lib/ConsiderationEnums.sol"; -import { AdditionalRecipient } from "../../contracts/lib/ConsiderationStructs.sol"; import { ConsiderationInterface } from "../../contracts/interfaces/ConsiderationInterface.sol"; import { AdditionalRecipient, Fulfillment, OfferItem, ConsiderationItem, FulfillmentComponent, OrderComponents, AdvancedOrder, BasicOrderParameters, Order } from "../../contracts/lib/ConsiderationStructs.sol"; import { BaseOrderTest } from "./utils/BaseOrderTest.sol"; import { EntryPoint, ReentryPoint } from "./utils/reentrancy/ReentrantEnums.sol"; -import { FulfillBasicOrderParameters, FulfillOrderParameters, OrderParameters, FulfillAdvancedOrderParameters, FulfillAvailableOrdersParameters, FulfillAvailableAdvancedOrdersParameters, MatchOrdersParameters, MatchAdvancedOrdersParameters, CancelParameters, ValidateParameters, ReentrantCallParameters, CriteriaResolver } from "./utils/reentrancy/ReentrantStructs.sol"; +import { OrderParameters, CriteriaResolver } from "./utils/reentrancy/ReentrantStructs.sol"; contract NonReentrantTest is BaseOrderTest { BasicOrderParameters basicOrderParameters; OrderComponents orderComponents; AdditionalRecipient recipient; - AdditionalRecipient[] additionalRecipients; OrderParameters orderParameters; Order order; Order[] orders; ReentryPoint reentryPoint; ConsiderationInterface currentConsideration; - - uint256 globalSalt; + bool reentered; + bool shouldReenter; /** * @dev Foundry fuzzes enums as uints, so we need to manually fuzz on uints and use vm.assume @@ -46,51 +44,37 @@ contract NonReentrantTest is BaseOrderTest { event BytesReason(bytes data); - modifier resetStorageState() { - _; - delete additionalRecipients; - delete considerationComponentsArray; - delete considerationItems; - delete currentConsideration; - delete fulfillment; - delete fulfillmentComponent; - delete fulfillmentComponents; - delete offerComponents; - delete offerComponentsArray; - delete offerItems; - delete order; - delete orderComponents; - delete orderParameters; - delete orders; - delete recipient; - delete reentryPoint; - delete basicOrderParameters; + function test(function(Context memory) external fn, Context memory context) + internal + { + try fn(context) {} catch (bytes memory reason) { + assertPass(reason); + } } function testNonReentrant() public { - for (uint256 i; i < 7; i++) { - for (uint256 j; j < 10; j++) { + for (uint256 i; i < 7; ++i) { + for (uint256 j; j < 10; ++j) { NonReentrantInputs memory inputs = NonReentrantInputs( EntryPoint(i), ReentryPoint(j) ); - _testNonReentrant(Context(referenceConsideration, inputs)); - _testNonReentrant(Context(consideration, inputs)); + test( + this.nonReentrant, + Context(referenceConsideration, inputs) + ); + test(this.nonReentrant, Context(consideration, inputs)); } } } - function _testNonReentrant(Context memory context) - internal - resetTokenBalancesBetweenRuns - resetStorageState - { + function nonReentrant(Context memory context) external stateless { currentConsideration = context.consideration; reentryPoint = context.args.reentryPoint; - _entryPoint(context.args.entryPoint, 2, false); + this._entryPoint(context.args.entryPoint, 2, false); // make sure reentry calls are valid by calling with a new token id - _reentryPoint(11); + this._reentryPoint(11); } // public so we can use try/catch @@ -103,6 +87,7 @@ contract NonReentrantTest is BaseOrderTest { BasicOrderParameters memory _basicOrderParameters = prepareBasicOrder(tokenId); if (!reentering) { + shouldReenter = true; vm.expectEmit( true, false, @@ -112,10 +97,8 @@ contract NonReentrantTest is BaseOrderTest { ); emit BytesReason(abi.encodeWithSignature("NoReentrantCalls()")); } - - currentConsideration.fulfillBasicOrder{ value: 1 }( - _basicOrderParameters - ); + currentConsideration.fulfillBasicOrder(_basicOrderParameters); + shouldReenter = false; } else if (entryPoint == EntryPoint.FulfillOrder) { ( Order memory params, @@ -123,7 +106,7 @@ contract NonReentrantTest is BaseOrderTest { uint256 value ) = prepareOrder(tokenId); if (!reentering) { - vm.expectEmit(true, false, false, false, address(this)); + vm.expectEmit(true, false, false, true, address(this)); emit BytesReason(abi.encodeWithSignature("NoReentrantCalls()")); } currentConsideration.fulfillOrder{ value: value }( @@ -138,13 +121,14 @@ contract NonReentrantTest is BaseOrderTest { uint256 value ) = prepareAdvancedOrder(tokenId); if (!reentering) { - vm.expectEmit(true, false, false, false, address(this)); + vm.expectEmit(true, false, false, true, address(this)); emit BytesReason(abi.encodeWithSignature("NoReentrantCalls()")); } currentConsideration.fulfillAdvancedOrder{ value: value }( _order, criteriaResolvers, - fulfillerConduitKey + fulfillerConduitKey, + address(0) ); } else if (entryPoint == EntryPoint.FulfillAvailableOrders) { ( @@ -155,7 +139,7 @@ contract NonReentrantTest is BaseOrderTest { uint256 maximumFulfilled ) = prepareAvailableOrders(tokenId); if (!reentering) { - vm.expectEmit(true, false, false, false, address(this)); + vm.expectEmit(true, false, false, true, address(this)); emit BytesReason(abi.encodeWithSignature("NoReentrantCalls()")); } vm.prank(alice); @@ -176,7 +160,7 @@ contract NonReentrantTest is BaseOrderTest { uint256 maximumFulfilled ) = prepareFulfillAvailableAdvancedOrders(tokenId); if (!reentering) { - vm.expectEmit(true, false, false, false, address(this)); + vm.expectEmit(true, false, false, true, address(this)); emit BytesReason(abi.encodeWithSignature("NoReentrantCalls()")); } vm.prank(alice); @@ -186,6 +170,7 @@ contract NonReentrantTest is BaseOrderTest { _offerFulfillments, _considerationFulfillments, fulfillerConduitKey, + address(0), maximumFulfilled ); } else if (entryPoint == EntryPoint.MatchOrders) { @@ -194,7 +179,7 @@ contract NonReentrantTest is BaseOrderTest { Fulfillment[] memory _fulfillments ) = prepareMatchOrders(tokenId); if (!reentering) { - vm.expectEmit(true, false, false, false, address(this)); + vm.expectEmit(true, false, false, true, address(this)); emit BytesReason(abi.encodeWithSignature("NoReentrantCalls()")); } currentConsideration.matchOrders{ value: 1 }( @@ -208,7 +193,7 @@ contract NonReentrantTest is BaseOrderTest { Fulfillment[] memory _fulfillments ) = prepareMatchAdvancedOrders(tokenId); if (!reentering) { - vm.expectEmit(true, false, false, false, address(this)); + vm.expectEmit(true, false, false, true, address(this)); emit BytesReason(abi.encodeWithSignature("NoReentrantCalls()")); } currentConsideration.matchAdvancedOrders{ value: 1 }( @@ -230,41 +215,21 @@ contract NonReentrantTest is BaseOrderTest { (Order memory _order, , ) = prepareOrder(tokenId); _orders[0] = _order; currentConsideration.validate(_orders); - } else if (reentryPoint == ReentryPoint.IncrementNonce) { - currentConsideration.incrementNonce(); + } else if (reentryPoint == ReentryPoint.IncrementCounter) { + currentConsideration.incrementCounter(); } } - function getOrderParameters(address payable offerer, OrderType orderType) - internal - returns (OrderParameters memory) - { - return - OrderParameters( - offerer, - address(0), - offerItems, - considerationItems, - orderType, - block.timestamp, - block.timestamp + 1, - bytes32(0), - globalSalt++, - bytes32(0), - considerationItems.length - ); - } - function prepareBasicOrder(uint256 tokenId) internal returns (BasicOrderParameters memory _basicOrderParameters) { - test721_1.mint(address(this), tokenId); + test1155_1.mint(address(this), tokenId, 2); offerItems.push( OfferItem( - ItemType.ERC721, // ItemType - address(test721_1), // token + ItemType.ERC1155, // ItemType + address(test1155_1), // token tokenId, // identifier 1, // start amt 1 // end amt @@ -273,16 +238,16 @@ contract NonReentrantTest is BaseOrderTest { considerationItems.push( ConsiderationItem( - ItemType.NATIVE, // ItemType - address(0), // Token + ItemType.ERC20, // ItemType + address(token1), // Token 0, // identifier 1, // start amount - 1, // end amout + 1, // end amount payable(address(this)) // recipient ) ); - uint256 nonce = currentConsideration.getNonce(address(this)); + uint256 counter = currentConsideration.getCounter(address(this)); orderComponents.offerer = address(this); orderComponents.zone = address(1); @@ -293,8 +258,8 @@ contract NonReentrantTest is BaseOrderTest { orderComponents.endTime = block.timestamp + 1; orderComponents.zoneHash = bytes32(0); orderComponents.salt = globalSalt++; - orderComponents.conduitKey = bytes32(0); - orderComponents.nonce = nonce; + orderComponents.conduitKey = conduitKeyOne; + orderComponents.counter = counter; bytes32 orderHash = currentConsideration.getOrderHash(orderComponents); bytes memory signature = signOrder( @@ -305,7 +270,7 @@ contract NonReentrantTest is BaseOrderTest { return toBasicOrderParameters( orderComponents, - BasicOrderType.ETH_TO_ERC721_FULL_OPEN, + BasicOrderType.ERC20_TO_ERC1155_FULL_OPEN, signature ); } @@ -320,11 +285,11 @@ contract NonReentrantTest is BaseOrderTest { { test1155_1.mint(address(this), tokenId, 10); - _configureERC1155OfferItem(tokenId, uint256(10)); - _configureEthConsiderationItem(payable(this), uint256(10)); - _configureEthConsiderationItem(payable(0), uint256(10)); - _configureEthConsiderationItem(alice, uint256(10)); - uint256 nonce = currentConsideration.getNonce(address(this)); + addErc1155OfferItem(tokenId, 10); + addEthConsiderationItem(payable(this), 10); + addEthConsiderationItem(payable(0), 10); + addEthConsiderationItem(alice, 10); + uint256 counter = currentConsideration.getCounter(address(this)); OrderParameters memory _orderParameters = getOrderParameters( payable(this), @@ -332,7 +297,7 @@ contract NonReentrantTest is BaseOrderTest { ); OrderComponents memory _orderComponents = toOrderComponents( _orderParameters, - nonce + counter ); bytes32 orderHash = currentConsideration.getOrderHash(_orderComponents); @@ -358,18 +323,18 @@ contract NonReentrantTest is BaseOrderTest { { test1155_1.mint(address(this), tokenId, 10); - _configureERC1155OfferItem(tokenId, uint256(10)); - _configureEthConsiderationItem(payable(this), uint256(10)); - _configureEthConsiderationItem(payable(address(0)), uint256(10)); - _configureEthConsiderationItem(payable(address(this)), uint256(10)); - uint256 nonce = currentConsideration.getNonce(address(this)); + addErc1155OfferItem(tokenId, uint256(10)); + addEthConsiderationItem(payable(this), uint256(10)); + addEthConsiderationItem(payable(address(0)), uint256(10)); + addEthConsiderationItem(payable(address(this)), uint256(10)); + uint256 counter = currentConsideration.getCounter(address(this)); OrderParameters memory _orderParameters = getOrderParameters( payable(this), OrderType.PARTIAL_OPEN ); OrderComponents memory _orderComponents = toOrderComponents( _orderParameters, - nonce + counter ); bytes32 orderHash = currentConsideration.getOrderHash(_orderComponents); @@ -386,82 +351,6 @@ contract NonReentrantTest is BaseOrderTest { fulfillerConduitKey = bytes32(0); } - function toOrderComponents(OrderParameters memory _params, uint256 nonce) - internal - pure - returns (OrderComponents memory) - { - return - OrderComponents( - _params.offerer, - _params.zone, - _params.offer, - _params.consideration, - _params.orderType, - _params.startTime, - _params.endTime, - _params.zoneHash, - _params.salt, - _params.conduitKey, - nonce - ); - } - - function toBasicOrderParameters( - Order memory _order, - BasicOrderType basicOrderType - ) internal pure returns (BasicOrderParameters memory) { - return - BasicOrderParameters( - _order.parameters.consideration[0].token, - _order.parameters.consideration[0].identifierOrCriteria, - _order.parameters.consideration[0].endAmount, - payable(_order.parameters.offerer), - _order.parameters.zone, - _order.parameters.offer[0].token, - _order.parameters.offer[0].identifierOrCriteria, - _order.parameters.offer[0].endAmount, - basicOrderType, - _order.parameters.startTime, - _order.parameters.endTime, - _order.parameters.zoneHash, - _order.parameters.salt, - _order.parameters.conduitKey, - bytes32(0), - 0, - new AdditionalRecipient[](0), - _order.signature - ); - } - - function toBasicOrderParameters( - OrderComponents memory _order, - BasicOrderType basicOrderType, - bytes memory signature - ) internal pure returns (BasicOrderParameters memory) { - return - BasicOrderParameters( - _order.consideration[0].token, - _order.consideration[0].identifierOrCriteria, - _order.consideration[0].endAmount, - payable(_order.offerer), - _order.zone, - _order.offer[0].token, - _order.offer[0].identifierOrCriteria, - _order.offer[0].endAmount, - basicOrderType, - _order.startTime, - _order.endTime, - _order.zoneHash, - _order.salt, - _order.conduitKey, - bytes32(0), - 0, - new AdditionalRecipient[](0), - signature - ); - } - function prepareAvailableOrders(uint256 tokenId) internal returns ( @@ -473,9 +362,9 @@ contract NonReentrantTest is BaseOrderTest { ) { test721_1.mint(address(this), tokenId); - _configureERC721OfferItem(tokenId); - _configureEthConsiderationItem(payable(address(this)), 1); - uint256 nonce = currentConsideration.getNonce(address(this)); + addErc721OfferItem(tokenId); + addEthConsiderationItem(payable(address(this)), 1); + uint256 counter = currentConsideration.getCounter(address(this)); OrderParameters memory _orderParameters = getOrderParameters( payable(this), @@ -483,7 +372,7 @@ contract NonReentrantTest is BaseOrderTest { ); OrderComponents memory _orderComponents = toOrderComponents( _orderParameters, - nonce + counter ); bytes32 orderHash = currentConsideration.getOrderHash(_orderComponents); bytes memory signature = signOrder( @@ -491,9 +380,6 @@ contract NonReentrantTest is BaseOrderTest { alicePk, orderHash ); - delete fulfillmentComponents; - delete offerComponentsArray; - delete considerationComponentsArray; fulfillmentComponents.push(FulfillmentComponent(0, 0)); offerComponentsArray.push(fulfillmentComponents); @@ -542,9 +428,9 @@ contract NonReentrantTest is BaseOrderTest { returns (Order[] memory, Fulfillment[] memory) { test721_1.mint(address(this), tokenId); - _configureERC721OfferItem(tokenId); - _configureEthConsiderationItem(payable(address(this)), 1); - uint256 nonce = currentConsideration.getNonce(address(this)); + addErc721OfferItem(tokenId); + addEthConsiderationItem(payable(address(this)), 1); + uint256 counter = currentConsideration.getCounter(address(this)); orderComponents.offerer = address(this); orderComponents.zone = address(0); orderComponents.offer = offerItems; @@ -555,7 +441,7 @@ contract NonReentrantTest is BaseOrderTest { orderComponents.zoneHash = bytes32(0); orderComponents.salt = globalSalt++; orderComponents.conduitKey = bytes32(0); - orderComponents.nonce = nonce; + orderComponents.counter = counter; bytes32 orderHash = currentConsideration.getOrderHash(orderComponents); bytes memory signature = signOrder( currentConsideration, @@ -582,9 +468,9 @@ contract NonReentrantTest is BaseOrderTest { delete offerItems; delete considerationItems; - _configureEthOfferItem(1); - _configureErc721ConsiderationItem(payable(this), tokenId); - nonce = currentConsideration.getNonce(address(bob)); + addEthOfferItem(1); + addErc721ConsiderationItem(payable(this), tokenId); + counter = currentConsideration.getCounter(address(bob)); orderComponents.offerer = bob; orderComponents.zone = address(0); orderComponents.offer = offerItems; @@ -595,7 +481,7 @@ contract NonReentrantTest is BaseOrderTest { orderComponents.zoneHash = bytes32(0); orderComponents.salt = globalSalt++; orderComponents.conduitKey = bytes32(0); - orderComponents.nonce = nonce; + orderComponents.counter = counter; bytes32 mirrorOrderHash = currentConsideration.getOrderHash( orderComponents @@ -666,15 +552,6 @@ contract NonReentrantTest is BaseOrderTest { return (_orders, criteriaResolvers, _fulfillments); } - ///@dev allow signing for this contract since it needs to be recipient of basic order to reenter on receive - function isValidSignature(bytes32, bytes memory) - external - pure - returns (bytes4) - { - return 0x1626ba7e; - } - function _doReenter() internal { if (uint256(reentryPoint) < 7) { try @@ -692,4 +569,18 @@ contract NonReentrantTest is BaseOrderTest { receive() external payable override { _doReenter(); } + + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) public override returns (bytes4) { + if (shouldReenter && !reentered) { + reentered = true; + _doReenter(); + } + return this.onERC1155Received.selector; + } } diff --git a/test/foundry/TransferHelperTest.sol b/test/foundry/TransferHelperTest.sol new file mode 100644 index 000000000..051934490 --- /dev/null +++ b/test/foundry/TransferHelperTest.sol @@ -0,0 +1,728 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; +// prettier-ignore +import { BaseConsiderationTest } from "./utils/BaseConsiderationTest.sol"; + +import { BaseOrderTest } from "./utils/BaseOrderTest.sol"; + +import { ConduitInterface } from "../../contracts/interfaces/ConduitInterface.sol"; + +import { ConduitItemType } from "../../contracts/conduit/lib/ConduitEnums.sol"; + +import { TransferHelper } from "../../contracts/helpers/TransferHelper.sol"; + +import { TransferHelperItem } from "../../contracts/helpers/TransferHelperStructs.sol"; + +import { TestERC20 } from "../../contracts/test/TestERC20.sol"; +import { TestERC721 } from "../../contracts/test/TestERC721.sol"; +import { TestERC1155 } from "../../contracts/test/TestERC1155.sol"; + +import { TokenTransferrerErrors } from "../../contracts/interfaces/TokenTransferrerErrors.sol"; + +import { TransferHelperInterface } from "../../contracts/interfaces/TransferHelperInterface.sol"; + +contract TransferHelperTest is BaseOrderTest { + TransferHelper transferHelper; + // Total supply of fungible tokens to be used in tests for all fungible tokens. + uint256 constant TOTAL_FUNGIBLE_TOKENS = 1e6; + // Total number of token identifiers to mint tokens for for ERC721s and ERC1155s. + uint256 constant TOTAL_TOKEN_IDENTIFERS = 10; + // Constant bytes used for expecting revert with no message. + bytes constant REVERT_DATA_NO_MSG = "revert no message"; + + struct FromToBalance { + // Balance of from address. + uint256 from; + // Balance of to address. + uint256 to; + } + + struct FuzzInputsCommon { + // Indicates if a conduit should be used for the transfer + bool useConduit; + // Amounts that can be used for the amount field on TransferHelperItem + uint256[10] amounts; + // Identifiers that can be used for the identifier field on TransferHelperItem + uint256[10] identifiers; + // Indexes that can be used to select tokens from the arrays erc20s/erc721s/erc1155s + uint256[10] tokenIndex; + } + + function setUp() public override { + super.setUp(); + _deployAndConfigurePrecompiledTransferHelper(); + vm.label(address(transferHelper), "transferHelper"); + + // Mint initial tokens to alice for tests. + for (uint256 tokenIdx = 0; tokenIdx < erc20s.length; tokenIdx++) { + erc20s[tokenIdx].mint(alice, TOTAL_FUNGIBLE_TOKENS); + } + + // Mint ERC721 and ERC1155 with token IDs 0 to TOTAL_TOKEN_IDENTIFERS - 1 to alice + for ( + uint256 identifier = 0; + identifier < TOTAL_TOKEN_IDENTIFERS; + identifier++ + ) { + for (uint256 tokenIdx = 0; tokenIdx < erc721s.length; tokenIdx++) { + erc721s[tokenIdx].mint(alice, identifier); + } + for (uint256 tokenIdx = 0; tokenIdx < erc1155s.length; tokenIdx++) { + erc1155s[tokenIdx].mint( + alice, + identifier, + TOTAL_FUNGIBLE_TOKENS + ); + } + } + + // Allow transfer helper to perform transfers for these addresses. + _setApprovals(alice); + _setApprovals(bob); + _setApprovals(cal); + + // Open a channel for transfer helper on the conduit + _updateConduitChannel(true); + } + + /** + * @dev TransferHelper depends on precomputed Conduit creation code hash, which will differ + * if tests are run with different compiler settings (which they are by default) + */ + function _deployAndConfigurePrecompiledTransferHelper() public { + transferHelper = TransferHelper( + deployCode( + "optimized-out/TransferHelper.sol/TransferHelper.json", + abi.encode(address(conduitController)) + ) + ); + } + + // Helper functions + + function _setApprovals(address _owner) internal override { + super._setApprovals(_owner); + vm.startPrank(_owner); + for (uint256 i = 0; i < erc20s.length; i++) { + erc20s[i].approve(address(transferHelper), MAX_INT); + } + for (uint256 i = 0; i < erc1155s.length; i++) { + erc1155s[i].setApprovalForAll(address(transferHelper), true); + } + for (uint256 i = 0; i < erc721s.length; i++) { + erc721s[i].setApprovalForAll(address(transferHelper), true); + } + vm.stopPrank(); + emit log_named_address( + "Owner proxy approved for all tokens from", + _owner + ); + emit log_named_address( + "Consideration approved for all tokens from", + _owner + ); + } + + function _updateConduitChannel(bool open) internal { + (address _conduit, ) = conduitController.getConduit(conduitKeyOne); + vm.prank(address(conduitController)); + ConduitInterface(_conduit).updateChannel(address(transferHelper), open); + } + + function _balanceOfTransferItemForAddress( + TransferHelperItem memory item, + address addr + ) internal view returns (uint256) { + if (item.itemType == ConduitItemType.ERC20) { + return TestERC20(item.token).balanceOf(addr); + } else if (item.itemType == ConduitItemType.ERC721) { + return + TestERC721(item.token).ownerOf(item.identifier) == addr ? 1 : 0; + } else if (item.itemType == ConduitItemType.ERC1155) { + return TestERC1155(item.token).balanceOf(addr, item.identifier); + } else if (item.itemType == ConduitItemType.NATIVE) { + // Balance for native does not matter as don't support native transfers so just return dummy value. + return 0; + } + // Revert on unsupported ConduitItemType. + revert(); + } + + function _balanceOfTransferItemForFromTo( + TransferHelperItem memory item, + address from, + address to + ) internal view returns (FromToBalance memory) { + return + FromToBalance( + _balanceOfTransferItemForAddress(item, from), + _balanceOfTransferItemForAddress(item, to) + ); + } + + function _performSingleItemTransferAndCheckBalances( + TransferHelperItem memory item, + address from, + address to, + bool useConduit, + bytes memory expectRevertData + ) public { + TransferHelperItem[] memory items = new TransferHelperItem[](1); + items[0] = item; + _performMultiItemTransferAndCheckBalances( + items, + from, + to, + useConduit, + expectRevertData + ); + } + + function _performMultiItemTransferAndCheckBalances( + TransferHelperItem[] memory items, + address from, + address to, + bool useConduit, + bytes memory expectRevertData + ) public { + vm.startPrank(from); + + // Get balances before transfer + FromToBalance[] memory beforeTransferBalances = new FromToBalance[]( + items.length + ); + for (uint256 i = 0; i < items.length; i++) { + beforeTransferBalances[i] = _balanceOfTransferItemForFromTo( + items[i], + from, + to + ); + } + + // Register expected revert if present. + if ( + // Compare hashes as we cannot directly compare bytes memory with bytes storage. + keccak256(expectRevertData) == keccak256(REVERT_DATA_NO_MSG) + ) { + vm.expectRevert(); + } else if (expectRevertData.length > 0) { + vm.expectRevert(expectRevertData); + } + // Perform transfer. + transferHelper.bulkTransfer( + items, + to, + useConduit ? conduitKeyOne : bytes32(0) + ); + + // Get balances after transfer + FromToBalance[] memory afterTransferBalances = new FromToBalance[]( + items.length + ); + for (uint256 i = 0; i < items.length; i++) { + afterTransferBalances[i] = _balanceOfTransferItemForFromTo( + items[i], + from, + to + ); + } + + if (expectRevertData.length > 0) { + // If revert is expected, balances should not have changed. + for (uint256 i = 0; i < items.length; i++) { + assert( + beforeTransferBalances[i].from == + afterTransferBalances[i].from + ); + assert( + beforeTransferBalances[i].to == afterTransferBalances[i].to + ); + } + return; + } + + // Check after transfer balances are as expected by calculating difference against before transfer balances. + for (uint256 i = 0; i < items.length; i++) { + // ERC721 balance should only ever change by amount 1. + uint256 amount = items[i].itemType == ConduitItemType.ERC721 + ? 1 + : items[i].amount; + assertEq( + afterTransferBalances[i].from, + beforeTransferBalances[i].from - amount + ); + assertEq( + afterTransferBalances[i].to, + beforeTransferBalances[i].to + amount + ); + } + + vm.stopPrank(); + } + + function _getFuzzedTransferItem( + ConduitItemType itemType, + uint256 fuzzAmount, + uint256 fuzzIndex, + uint256 fuzzIdentifier + ) internal view returns (TransferHelperItem memory) { + uint256 amount = fuzzAmount % TOTAL_FUNGIBLE_TOKENS; + uint256 identifier = fuzzIdentifier % TOTAL_TOKEN_IDENTIFERS; + if (itemType == ConduitItemType.ERC20) { + uint256 index = fuzzIndex % erc20s.length; + TestERC20 erc20 = erc20s[index]; + return + TransferHelperItem( + itemType, + address(erc20), + identifier, + amount + ); + } else if (itemType == ConduitItemType.ERC1155) { + uint256 index = fuzzIndex % erc1155s.length; + TestERC1155 erc1155 = erc1155s[index]; + return + TransferHelperItem( + itemType, + address(erc1155), + identifier, + amount + ); + } else if (itemType == ConduitItemType.NATIVE) { + return TransferHelperItem(itemType, address(0), identifier, amount); + } else if (itemType == ConduitItemType.ERC721) { + uint256 index = fuzzIndex % erc721s.length; + return + TransferHelperItem( + itemType, + address(erc721s[index]), + identifier, + 1 + ); + } + revert(); + } + + function _getFuzzedERC721TransferItemWithAmountGreaterThan1( + uint256 fuzzAmount, + uint256 fuzzIndex, + uint256 fuzzIdentifier + ) internal view returns (TransferHelperItem memory) { + TransferHelperItem memory item = _getFuzzedTransferItem( + ConduitItemType.ERC721, + fuzzAmount, + fuzzIndex, + fuzzIdentifier + ); + item.amount = 2 + (fuzzAmount % TOTAL_FUNGIBLE_TOKENS); + return item; + } + + // Test successful transfers + + function testBulkTransferERC20(FuzzInputsCommon memory inputs) public { + TransferHelperItem memory item = _getFuzzedTransferItem( + ConduitItemType.ERC20, + inputs.amounts[0], + inputs.tokenIndex[0], + inputs.identifiers[0] + ); + + _performSingleItemTransferAndCheckBalances( + item, + alice, + bob, + inputs.useConduit, + "" + ); + } + + function testBulkTransferERC721(FuzzInputsCommon memory inputs) public { + TransferHelperItem memory item = _getFuzzedTransferItem( + ConduitItemType.ERC721, + inputs.amounts[0], + inputs.tokenIndex[0], + inputs.identifiers[0] + ); + + _performSingleItemTransferAndCheckBalances( + item, + alice, + bob, + inputs.useConduit, + "" + ); + } + + function testBulkTransferERC721toBobThenCal(FuzzInputsCommon memory inputs) + public + { + TransferHelperItem memory item = _getFuzzedTransferItem( + ConduitItemType.ERC721, + inputs.amounts[0], + inputs.tokenIndex[0], + inputs.identifiers[0] + ); + + _performSingleItemTransferAndCheckBalances( + item, + alice, + bob, + inputs.useConduit, + "" + ); + _performSingleItemTransferAndCheckBalances( + item, + bob, + cal, + inputs.useConduit, + "" + ); + } + + function testBulkTransferERC1155(FuzzInputsCommon memory inputs) public { + TransferHelperItem memory item = _getFuzzedTransferItem( + ConduitItemType.ERC1155, + inputs.amounts[0], + inputs.tokenIndex[0], + inputs.identifiers[0] + ); + + _performSingleItemTransferAndCheckBalances( + item, + alice, + bob, + inputs.useConduit, + "" + ); + } + + function testBulkTransferERC1155andERC721(FuzzInputsCommon memory inputs) + public + { + TransferHelperItem[] memory items = new TransferHelperItem[](2); + items[0] = _getFuzzedTransferItem( + ConduitItemType.ERC1155, + inputs.amounts[0], + inputs.tokenIndex[0], + inputs.identifiers[0] + ); + items[1] = _getFuzzedTransferItem( + ConduitItemType.ERC721, + inputs.amounts[1], + inputs.tokenIndex[1], + inputs.identifiers[1] + ); + + _performMultiItemTransferAndCheckBalances( + items, + alice, + bob, + inputs.useConduit, + "" + ); + } + + function testBulkTransferERC1155andERC721andERC20( + FuzzInputsCommon memory inputs + ) public { + TransferHelperItem[] memory items = new TransferHelperItem[](3); + items[0] = _getFuzzedTransferItem( + ConduitItemType.ERC1155, + inputs.amounts[0], + inputs.tokenIndex[0], + inputs.identifiers[0] + ); + items[1] = _getFuzzedTransferItem( + ConduitItemType.ERC721, + inputs.amounts[1], + inputs.tokenIndex[1], + inputs.identifiers[1] + ); + items[2] = _getFuzzedTransferItem( + ConduitItemType.ERC20, + inputs.amounts[2], + inputs.tokenIndex[2], + inputs.identifiers[2] + ); + + _performMultiItemTransferAndCheckBalances( + items, + alice, + bob, + inputs.useConduit, + "" + ); + } + + function testBulkTransferMultipleERC721SameContract( + FuzzInputsCommon memory inputs + ) public { + uint256 numItems = 3; + TransferHelperItem[] memory items = new TransferHelperItem[](numItems); + for (uint256 i = 0; i < numItems; i++) { + items[i] = _getFuzzedTransferItem( + ConduitItemType.ERC721, + inputs.amounts[i], + // Same token index for all items since this is testing from same contract + inputs.tokenIndex[0], + // Each item has a different token identifier as alice only owns one ERC721 token + // for each identifier for this particular contract + i + ); + } + + _performMultiItemTransferAndCheckBalances( + items, + alice, + bob, + inputs.useConduit, + "" + ); + } + + function testBulkTransferMultipleERC721DifferentContracts( + FuzzInputsCommon memory inputs + ) public { + TransferHelperItem[] memory items = new TransferHelperItem[](3); + items[0] = _getFuzzedTransferItem( + ConduitItemType.ERC721, + inputs.amounts[0], + // Different token index for all items since this is testing from different contracts + 0, + inputs.identifiers[0] + ); + items[1] = _getFuzzedTransferItem( + ConduitItemType.ERC721, + inputs.amounts[1], + 1, + inputs.identifiers[1] + ); + items[2] = _getFuzzedTransferItem( + ConduitItemType.ERC721, + inputs.amounts[2], + 2, + inputs.identifiers[2] + ); + + _performMultiItemTransferAndCheckBalances( + items, + alice, + bob, + inputs.useConduit, + "" + ); + } + + function testBulkTransferMultipleERC721andMultipleERC1155( + FuzzInputsCommon memory inputs + ) public { + uint256 numItems = 6; + TransferHelperItem[] memory items = new TransferHelperItem[](numItems); + + // Fill items such that the first floor(numItems / 2) items are ERC1155 and the remaining + // items are ERC721 + for (uint256 i = 0; i < numItems; i++) { + if (i < numItems / 2) { + items[i] = _getFuzzedTransferItem( + ConduitItemType.ERC1155, + inputs.amounts[i], + // Ensure each item is from a different contract + i, + inputs.identifiers[i] + ); + } else { + items[i] = _getFuzzedTransferItem( + ConduitItemType.ERC721, + inputs.amounts[i], + i, + inputs.identifiers[i] + ); + } + } + + _performMultiItemTransferAndCheckBalances( + items, + alice, + bob, + inputs.useConduit, + "" + ); + } + + function testBulkTransferERC721AmountMoreThan1NotUsingConduit( + FuzzInputsCommon memory inputs + ) public { + TransferHelperItem + memory item = _getFuzzedERC721TransferItemWithAmountGreaterThan1( + inputs.amounts[0], + inputs.tokenIndex[0], + inputs.identifiers[0] + ); + + _performSingleItemTransferAndCheckBalances(item, alice, bob, false, ""); + } + + function testBulkTransferERC721AmountMoreThan1AndERC20NotUsingConduit( + FuzzInputsCommon memory inputs + ) public { + TransferHelperItem[] memory items = new TransferHelperItem[](2); + items[0] = _getFuzzedERC721TransferItemWithAmountGreaterThan1( + inputs.amounts[0], + inputs.tokenIndex[0], + inputs.identifiers[0] + ); + + items[1] = _getFuzzedTransferItem( + ConduitItemType.ERC20, + inputs.amounts[1], + inputs.tokenIndex[1], + inputs.identifiers[1] + ); + + _performMultiItemTransferAndCheckBalances(items, alice, bob, false, ""); + } + + // Test reverts + + function testRevertBulkTransferETHonly(FuzzInputsCommon memory inputs) + public + { + TransferHelperItem memory item = _getFuzzedTransferItem( + ConduitItemType.NATIVE, + inputs.amounts[0], + inputs.tokenIndex[0], + inputs.identifiers[0] + ); + + _performSingleItemTransferAndCheckBalances( + item, + alice, + bob, + inputs.useConduit, + abi.encodePacked(TransferHelperInterface.InvalidItemType.selector) + ); + } + + function testRevertBulkTransferETHandERC721(FuzzInputsCommon memory inputs) + public + { + TransferHelperItem[] memory items = new TransferHelperItem[](2); + items[0] = _getFuzzedTransferItem( + ConduitItemType.NATIVE, + inputs.amounts[0], + inputs.tokenIndex[0], + inputs.identifiers[0] + ); + items[1] = _getFuzzedTransferItem( + ConduitItemType.ERC721, + inputs.amounts[1], + inputs.tokenIndex[1], + inputs.identifiers[1] + ); + + _performMultiItemTransferAndCheckBalances( + items, + alice, + bob, + inputs.useConduit, + abi.encodePacked(TransferHelperInterface.InvalidItemType.selector) + ); + } + + function testRevertBulkTransferERC721AmountMoreThan1UsingConduit( + FuzzInputsCommon memory inputs + ) public { + TransferHelperItem + memory item = _getFuzzedERC721TransferItemWithAmountGreaterThan1( + inputs.amounts[0], + inputs.tokenIndex[0], + inputs.identifiers[0] + ); + + _performSingleItemTransferAndCheckBalances( + item, + alice, + bob, + true, + abi.encodePacked( + TokenTransferrerErrors.InvalidERC721TransferAmount.selector + ) + ); + } + + function testRevertBulkTransferERC721AmountMoreThan1AndERC20UsingConduit( + FuzzInputsCommon memory inputs + ) public { + TransferHelperItem[] memory items = new TransferHelperItem[](2); + items[0] = _getFuzzedERC721TransferItemWithAmountGreaterThan1( + inputs.amounts[0], + inputs.tokenIndex[0], + inputs.identifiers[0] + ); + + items[1] = _getFuzzedTransferItem( + ConduitItemType.ERC20, + inputs.amounts[1], + inputs.tokenIndex[1], + inputs.identifiers[1] + ); + + _performMultiItemTransferAndCheckBalances( + items, + alice, + bob, + true, + abi.encodePacked( + TokenTransferrerErrors.InvalidERC721TransferAmount.selector + ) + ); + } + + function testRevertBulkTransferNotOpenConduitChannel( + FuzzInputsCommon memory inputs + ) public { + TransferHelperItem memory item = _getFuzzedTransferItem( + ConduitItemType.ERC20, + inputs.amounts[0], + inputs.tokenIndex[0], + inputs.identifiers[0] + ); + _updateConduitChannel(false); + _performSingleItemTransferAndCheckBalances( + item, + alice, + bob, + true, + abi.encodeWithSelector( + ConduitInterface.ChannelClosed.selector, + address(transferHelper) + ) + ); + } + + function testRevertBulkTransferUnknownConduit( + FuzzInputsCommon memory inputs, + bytes32 fuzzConduitKey + ) public { + // Assume fuzzConduitKey is not equal to TransferHelper's value for "no conduit". + vm.assume( + fuzzConduitKey != bytes32(0) && fuzzConduitKey != conduitKeyOne + ); + TransferHelperItem memory item = _getFuzzedTransferItem( + ConduitItemType.ERC20, + inputs.amounts[0], + inputs.tokenIndex[0], + inputs.identifiers[0] + ); + // Reassign the conduit key that gets passed into TransferHelper to fuzzConduitKey. + conduitKeyOne = fuzzConduitKey; + _performSingleItemTransferAndCheckBalances( + item, + alice, + bob, + true, + REVERT_DATA_NO_MSG + ); + } +} diff --git a/test/foundry/conduit/BaseConduitTest.sol b/test/foundry/conduit/BaseConduitTest.sol index 73ccb747a..da7d87236 100644 --- a/test/foundry/conduit/BaseConduitTest.sol +++ b/test/foundry/conduit/BaseConduitTest.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { BaseConsiderationTest } from "../utils/BaseConsiderationTest.sol"; import { ConduitTransfer, ConduitItemType, ConduitBatch1155Transfer } from "../../../contracts/conduit/lib/ConduitStructs.sol"; @@ -38,29 +38,6 @@ contract BaseConduitTest is IdAmount[10] idAmounts; } - modifier resetTokenBalancesBetweenRuns(ConduitTransfer[] memory transfers) { - vm.record(); - _; - resetTokenBalances(transfers); - } - - modifier resetBatchTokenBalancesBetweenRuns( - ConduitBatch1155Transfer[] memory batchTransfers - ) { - vm.record(); - _; - resetTokenBalances(batchTransfers); - } - - modifier resetTransferAndBatchTransferTokenBalancesBetweenRuns( - ConduitTransfer[] memory transfers, - ConduitBatch1155Transfer[] memory batchTransfers - ) { - vm.record(); - _; - resetTokenBalances(transfers, batchTransfers); - } - function setUp() public virtual override { super.setUp(); conduitController.updateChannel(address(conduit), address(this), true); @@ -139,7 +116,7 @@ contract BaseConduitTest is } uint256 truncatedNumTokenIds = (intermediate.numTokenIds % 8) + 1; transfers = new ConduitTransfer[](truncatedNumTokenIds); - for (uint256 i = 0; i < truncatedNumTokenIds; i++) { + for (uint256 i = 0; i < truncatedNumTokenIds; ++i) { if (itemType == ConduitItemType.ERC1155) { transfers[i] = ConduitTransfer( itemType, @@ -170,10 +147,10 @@ contract BaseConduitTest is ConduitTransfer[] memory transfers = new ConduitTransfer[]( original.length + extension.length ); - for (uint256 i = 0; i < original.length; i++) { + for (uint256 i = 0; i < original.length; ++i) { transfers[i] = original[i]; } - for (uint256 i = 0; i < extension.length; i++) { + for (uint256 i = 0; i < extension.length; ++i) { transfers[i + original.length] = extension[i]; } return transfers; @@ -187,10 +164,10 @@ contract BaseConduitTest is memory transfers = new ConduitBatch1155Transfer[]( original.length + extension.length ); - for (uint256 i = 0; i < original.length; i++) { + for (uint256 i = 0; i < original.length; ++i) { transfers[i] = original[i]; } - for (uint256 i = 0; i < extension.length; i++) { + for (uint256 i = 0; i < extension.length; ++i) { transfers[i + original.length] = extension[i]; } return transfers; @@ -237,7 +214,7 @@ contract BaseConduitTest is uint256[] memory amounts = new uint256[]( batchIntermediate.idAmounts.length ); - for (uint256 n = 0; n < batchIntermediate.idAmounts.length; n++) { + for (uint256 n = 0; n < batchIntermediate.idAmounts.length; ++n) { ids[n] = batchIntermediate.idAmounts[n].id; amounts[n] = uint256(batchIntermediate.idAmounts[n].amount) + 1; } @@ -261,7 +238,7 @@ contract BaseConduitTest is * address if it can't */ function makeRecipientsSafe(ConduitTransfer[] memory transfers) internal { - for (uint256 i; i < transfers.length; i++) { + for (uint256 i; i < transfers.length; ++i) { ConduitTransfer memory transfer = transfers[i]; address from = receiver(transfer.from, transfer.itemType); address to = receiver(transfer.to, transfer.itemType); @@ -273,7 +250,7 @@ contract BaseConduitTest is function makeRecipientsSafe( ConduitBatch1155Transfer[] memory batchTransfers ) internal { - for (uint256 i; i < batchTransfers.length; i++) { + for (uint256 i; i < batchTransfers.length; ++i) { ConduitBatch1155Transfer memory batchTransfer = batchTransfers[i]; address from = receiver( batchTransfer.from, @@ -287,10 +264,9 @@ contract BaseConduitTest is } function mintTokensAndSetTokenApprovalsForConduit( - ConduitTransfer[] memory transfers, - address conduitAddress + ConduitTransfer[] memory transfers ) internal { - for (uint256 i = 0; i < transfers.length; i++) { + for (uint256 i = 0; i < transfers.length; ++i) { ConduitTransfer memory transfer = transfers[i]; ConduitItemType itemType = transfer.itemType; address from = transfer.from; @@ -298,40 +274,47 @@ contract BaseConduitTest is if (itemType == ConduitItemType.ERC20) { TestERC20 erc20 = TestERC20(token); erc20.mint(from, transfer.amount); - vm.prank(from); - erc20.approve(conduitAddress, 2**256 - 1); + vm.startPrank(from); + erc20.approve(address(conduit), 2**256 - 1); + erc20.approve(address(referenceConduit), 2**256 - 1); + vm.stopPrank(); } else if (itemType == ConduitItemType.ERC1155) { TestERC1155 erc1155 = TestERC1155(token); erc1155.mint(from, transfer.identifier, transfer.amount); - vm.prank(from); - erc1155.setApprovalForAll(conduitAddress, true); + vm.startPrank(from); + erc1155.setApprovalForAll(address(conduit), true); + erc1155.setApprovalForAll(address(referenceConduit), true); + vm.stopPrank(); } else { TestERC721 erc721 = TestERC721(token); erc721.mint(from, transfer.identifier); - vm.prank(from); - erc721.setApprovalForAll(conduitAddress, true); + vm.startPrank(from); + erc721.setApprovalForAll(address(referenceConduit), true); + erc721.setApprovalForAll(address(conduit), true); + vm.stopPrank(); } } } function mintTokensAndSetTokenApprovalsForConduit( - ConduitBatch1155Transfer[] memory batchTransfers, - address conduitAddress + ConduitBatch1155Transfer[] memory batchTransfers ) internal { - for (uint256 i = 0; i < batchTransfers.length; i++) { + for (uint256 i = 0; i < batchTransfers.length; ++i) { ConduitBatch1155Transfer memory batchTransfer = batchTransfers[i]; address from = batchTransfer.from; address token = batchTransfer.token; TestERC1155 erc1155 = TestERC1155(token); - for (uint256 n = 0; n < batchTransfer.ids.length; n++) { + for (uint256 n = 0; n < batchTransfer.ids.length; ++n) { erc1155.mint( from, batchTransfer.ids[n], batchTransfer.amounts[n] ); } - vm.prank(from); - erc1155.setApprovalForAll(conduitAddress, true); + vm.startPrank(from); + erc1155.setApprovalForAll(address(conduit), true); + erc1155.setApprovalForAll(address(referenceConduit), true); + vm.stopPrank(); } } @@ -352,7 +335,7 @@ contract BaseConduitTest is uint256[] memory batchTokenBalances = new uint256[]( batchTransfer.ids.length ); - for (uint256 i = 0; i < batchTransfer.ids.length; i++) { + for (uint256 i = 0; i < batchTransfer.ids.length; ++i) { batchTokenBalances[i] = userToExpectedTokenIdentifierBalance[ batchTransfer.to ][batchTransfer.token][batchTransfer.ids[i]]; @@ -363,7 +346,7 @@ contract BaseConduitTest is function updateExpectedTokenBalances(ConduitTransfer[] memory transfers) internal { - for (uint256 i = 0; i < transfers.length; i++) { + for (uint256 i = 0; i < transfers.length; ++i) { ConduitTransfer memory transfer = transfers[i]; ConduitItemType itemType = transfer.itemType; if (itemType != ConduitItemType.ERC721) { @@ -375,7 +358,7 @@ contract BaseConduitTest is function updateExpectedTokenBalances( ConduitBatch1155Transfer[] memory batchTransfers ) internal { - for (uint256 i = 0; i < batchTransfers.length; i++) { + for (uint256 i = 0; i < batchTransfers.length; ++i) { updateExpectedBatchBalances(batchTransfers[i]); } } @@ -389,43 +372,10 @@ contract BaseConduitTest is function updateExpectedBatchBalances( ConduitBatch1155Transfer memory batchTransfer ) internal { - for (uint256 i = 0; i < batchTransfer.ids.length; i++) { + for (uint256 i = 0; i < batchTransfer.ids.length; ++i) { userToExpectedTokenIdentifierBalance[batchTransfer.to][ batchTransfer.token ][batchTransfer.ids[i]] += batchTransfer.amounts[i]; } } - - /** - * @dev reset all token contract storage changed since vm.record was started - */ - function resetTokenBalances(ConduitTransfer[] memory transfers) internal { - for (uint256 i = 0; i < transfers.length; i++) { - ConduitTransfer memory transfer = transfers[i]; - _resetStorage(transfer.token); - } - } - - function resetTokenBalances( - ConduitBatch1155Transfer[] memory batchTransfers - ) internal { - for (uint256 i = 0; i < batchTransfers.length; i++) { - ConduitBatch1155Transfer memory batchTransfer = batchTransfers[i]; - _resetStorage(batchTransfer.token); - } - } - - function resetTokenBalances( - ConduitTransfer[] memory transfers, - ConduitBatch1155Transfer[] memory batchTransfers - ) internal { - for (uint256 i = 0; i < transfers.length; i++) { - ConduitTransfer memory transfer = transfers[i]; - _resetStorage(transfer.token); - } - for (uint256 i = 0; i < batchTransfers.length; i++) { - ConduitBatch1155Transfer memory batchTransfer = batchTransfers[i]; - _resetStorage(batchTransfer.token); - } - } } diff --git a/test/foundry/conduit/ConduitExecute.t.sol b/test/foundry/conduit/ConduitExecute.t.sol index bf6bc1334..1cc43ab7c 100644 --- a/test/foundry/conduit/ConduitExecute.t.sol +++ b/test/foundry/conduit/ConduitExecute.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { BaseConsiderationTest } from "../utils/BaseConsiderationTest.sol"; import { ConduitTransfer, ConduitItemType } from "../../../contracts/conduit/lib/ConduitStructs.sol"; @@ -21,33 +21,35 @@ contract ConduitExecuteTest is BaseConduitTest { ConduitTransfer[] transfers; } + function test(function(Context memory) external fn, Context memory context) + internal + { + try fn(context) {} catch (bytes memory reason) { + assertPass(reason); + } + } + function testExecute(FuzzInputs memory inputs) public { ConduitTransfer[] memory transfers = new ConduitTransfer[](0); - for (uint8 i; i < inputs.intermediates.length; i++) { + for (uint8 i; i < inputs.intermediates.length; ++i) { transfers = extendConduitTransferArray( transfers, deployTokenAndCreateConduitTransfers(inputs.intermediates[i]) ); } makeRecipientsSafe(transfers); - mintTokensAndSetTokenApprovalsForConduit( - transfers, - address(referenceConduit) - ); + mintTokensAndSetTokenApprovalsForConduit(transfers); updateExpectedTokenBalances(transfers); - _testExecute(Context(referenceConduit, transfers)); - mintTokensAndSetTokenApprovalsForConduit(transfers, address(conduit)); - _testExecute(Context(conduit, transfers)); + + test(this.execute, Context(referenceConduit, transfers)); + test(this.execute, Context(conduit, transfers)); } - function _testExecute(Context memory context) - internal - resetTokenBalancesBetweenRuns(context.transfers) - { + function execute(Context memory context) external stateless { bytes4 magicValue = context.conduit.execute(context.transfers); assertEq(magicValue, Conduit.execute.selector); - for (uint256 i; i < context.transfers.length; i++) { + for (uint256 i; i < context.transfers.length; ++i) { ConduitTransfer memory transfer = context.transfers[i]; ConduitItemType itemType = transfer.itemType; if (itemType == ConduitItemType.ERC20) { diff --git a/test/foundry/conduit/ConduitExecuteBatch1155.t.sol b/test/foundry/conduit/ConduitExecuteBatch1155.t.sol index 54f70e52f..8d41f7259 100644 --- a/test/foundry/conduit/ConduitExecuteBatch1155.t.sol +++ b/test/foundry/conduit/ConduitExecuteBatch1155.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { BaseConsiderationTest } from "../utils/BaseConsiderationTest.sol"; import { ConduitTransfer, ConduitBatch1155Transfer, ConduitItemType } from "../../../contracts/conduit/lib/ConduitStructs.sol"; @@ -21,10 +21,18 @@ contract ConduitExecuteBatch1155Test is BaseConduitTest { ConduitBatch1155Transfer[] batchTransfers; } + function test(function(Context memory) external fn, Context memory context) + internal + { + try fn(context) {} catch (bytes memory reason) { + assertPass(reason); + } + } + function testExecuteBatch1155(FuzzInputs memory inputs) public { ConduitBatch1155Transfer[] memory batchTransfers = new ConduitBatch1155Transfer[](0); - for (uint8 j = 0; j < inputs.batchIntermediates.length; j++) { + for (uint8 j = 0; j < inputs.batchIntermediates.length; ++j) { batchTransfers = extendConduitTransferArray( batchTransfers, deployTokenAndCreateConduitBatch1155Transfer( @@ -33,36 +41,26 @@ contract ConduitExecuteBatch1155Test is BaseConduitTest { ); } makeRecipientsSafe(batchTransfers); - mintTokensAndSetTokenApprovalsForConduit( - batchTransfers, - address(referenceConduit) - ); + mintTokensAndSetTokenApprovalsForConduit(batchTransfers); updateExpectedTokenBalances(batchTransfers); - _testExecuteBatch1155(Context(referenceConduit, batchTransfers)); - mintTokensAndSetTokenApprovalsForConduit( - batchTransfers, - address(conduit) - ); - _testExecuteBatch1155(Context(conduit, batchTransfers)); + test(this.executeBatch1155, Context(referenceConduit, batchTransfers)); + test(this.executeBatch1155, Context(conduit, batchTransfers)); } - function _testExecuteBatch1155(Context memory context) - internal - resetBatchTokenBalancesBetweenRuns(context.batchTransfers) - { + function executeBatch1155(Context memory context) external stateless { bytes4 magicValue = context.conduit.executeBatch1155( context.batchTransfers ); assertEq(magicValue, Conduit.executeBatch1155.selector); - for (uint256 i = 0; i < context.batchTransfers.length; i++) { + for (uint256 i = 0; i < context.batchTransfers.length; ++i) { ConduitBatch1155Transfer memory batchTransfer = context .batchTransfers[i]; address[] memory toAddresses = new address[]( batchTransfer.ids.length ); - for (uint256 j = 0; j < batchTransfer.ids.length; j++) { + for (uint256 j = 0; j < batchTransfer.ids.length; ++j) { toAddresses[j] = batchTransfer.to; } uint256[] memory actualBatchBalances = TestERC1155( @@ -76,7 +74,7 @@ contract ConduitExecuteBatch1155Test is BaseConduitTest { actualBatchBalances.length == expectedBatchBalances.length ); - for (uint256 j = 0; j < actualBatchBalances.length; j++) { + for (uint256 j = 0; j < actualBatchBalances.length; ++j) { assertEq(actualBatchBalances[j], expectedBatchBalances[j]); } } diff --git a/test/foundry/conduit/ConduitExecuteWithBatch1155.t.sol b/test/foundry/conduit/ConduitExecuteWithBatch1155.t.sol index 344d80f02..3d2c9ed93 100644 --- a/test/foundry/conduit/ConduitExecuteWithBatch1155.t.sol +++ b/test/foundry/conduit/ConduitExecuteWithBatch1155.t.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { Conduit } from "../../../contracts/conduit/Conduit.sol"; import { ConduitController } from "../../../contracts/conduit/ConduitController.sol"; @@ -24,9 +24,17 @@ contract ConduitExecuteWithBatch1155Test is BaseConduitTest { ConduitBatch1155Transfer[] batchTransfers; } + function test(function(Context memory) external fn, Context memory context) + internal + { + try fn(context) {} catch (bytes memory reason) { + assertPass(reason); + } + } + function testExecuteWithBatch1155(FuzzInputs memory inputs) public { ConduitTransfer[] memory transfers = new ConduitTransfer[](0); - for (uint8 i = 0; i < inputs.transferIntermediates.length; i++) { + for (uint8 i = 0; i < inputs.transferIntermediates.length; ++i) { transfers = extendConduitTransferArray( transfers, deployTokenAndCreateConduitTransfers( @@ -37,7 +45,7 @@ contract ConduitExecuteWithBatch1155Test is BaseConduitTest { ConduitBatch1155Transfer[] memory batchTransfers = new ConduitBatch1155Transfer[](0); - for (uint8 j = 0; j < inputs.batchIntermediates.length; j++) { + for (uint8 j = 0; j < inputs.batchIntermediates.length; ++j) { batchTransfers = extendConduitTransferArray( batchTransfers, deployTokenAndCreateConduitBatch1155Transfer( @@ -47,41 +55,29 @@ contract ConduitExecuteWithBatch1155Test is BaseConduitTest { } makeRecipientsSafe(transfers); makeRecipientsSafe(batchTransfers); - mintTokensAndSetTokenApprovalsForConduit( - transfers, - address(referenceConduit) - ); + mintTokensAndSetTokenApprovalsForConduit(transfers); updateExpectedTokenBalances(transfers); - mintTokensAndSetTokenApprovalsForConduit( - batchTransfers, - address(referenceConduit) - ); + mintTokensAndSetTokenApprovalsForConduit(batchTransfers); updateExpectedTokenBalances(batchTransfers); - _testExecuteWithBatch1155( + + test( + this.executeWithBatch1155, Context(referenceConduit, transfers, batchTransfers) ); - mintTokensAndSetTokenApprovalsForConduit(transfers, address(conduit)); - mintTokensAndSetTokenApprovalsForConduit( - batchTransfers, - address(conduit) + test( + this.executeWithBatch1155, + Context(conduit, transfers, batchTransfers) ); - _testExecuteWithBatch1155(Context(conduit, transfers, batchTransfers)); } - function _testExecuteWithBatch1155(Context memory context) - internal - resetTransferAndBatchTransferTokenBalancesBetweenRuns( - context.transfers, - context.batchTransfers - ) - { + function executeWithBatch1155(Context memory context) external stateless { bytes4 magicValue = context.conduit.executeWithBatch1155( context.transfers, context.batchTransfers ); assertEq(magicValue, Conduit.executeWithBatch1155.selector); - for (uint256 i = 0; i < context.transfers.length; i++) { + for (uint256 i = 0; i < context.transfers.length; ++i) { ConduitTransfer memory transfer = context.transfers[i]; ConduitItemType itemType = transfer.itemType; emit log_uint(uint256(transfer.itemType)); @@ -107,14 +103,14 @@ contract ConduitExecuteWithBatch1155Test is BaseConduitTest { } } - for (uint256 i = 0; i < context.batchTransfers.length; i++) { + for (uint256 i = 0; i < context.batchTransfers.length; ++i) { ConduitBatch1155Transfer memory batchTransfer = context .batchTransfers[i]; address[] memory toAddresses = new address[]( batchTransfer.ids.length ); - for (uint256 j = 0; j < batchTransfer.ids.length; j++) { + for (uint256 j = 0; j < batchTransfer.ids.length; ++j) { toAddresses[j] = batchTransfer.to; } uint256[] memory actualBatchBalances = TestERC1155( @@ -128,7 +124,7 @@ contract ConduitExecuteWithBatch1155Test is BaseConduitTest { actualBatchBalances.length == expectedBatchBalances.length ); - for (uint256 j = 0; j < actualBatchBalances.length; j++) { + for (uint256 j = 0; j < actualBatchBalances.length; ++j) { assertEq(actualBatchBalances[j], expectedBatchBalances[j]); } } diff --git a/test/foundry/interfaces/OwnableDelegateProxy.sol b/test/foundry/interfaces/OwnableDelegateProxy.sol index e97991f25..c4cb97ecd 100644 --- a/test/foundry/interfaces/OwnableDelegateProxy.sol +++ b/test/foundry/interfaces/OwnableDelegateProxy.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; interface OwnableDelegateProxy { function name() external returns (string memory); diff --git a/test/foundry/interfaces/ProxyRegistry.sol b/test/foundry/interfaces/ProxyRegistry.sol index 15039d30c..90fa938bc 100644 --- a/test/foundry/interfaces/ProxyRegistry.sol +++ b/test/foundry/interfaces/ProxyRegistry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { OwnableDelegateProxy } from "./OwnableDelegateProxy.sol"; diff --git a/test/foundry/token/ERC721.sol b/test/foundry/token/ERC721.sol new file mode 100644 index 000000000..751cebb17 --- /dev/null +++ b/test/foundry/token/ERC721.sol @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +/// @notice Modern, minimalist, and gas efficient ERC-721 implementation. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC721.sol) +/// @notice modified for testing purposes +abstract contract ERC721 { + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event Transfer( + address indexed from, + address indexed to, + uint256 indexed id + ); + + event Approval( + address indexed owner, + address indexed spender, + uint256 indexed id + ); + + event ApprovalForAll( + address indexed owner, + address indexed operator, + bool approved + ); + + /*////////////////////////////////////////////////////////////// + METADATA STORAGE/LOGIC + //////////////////////////////////////////////////////////////*/ + + string public name; + + string public symbol; + + function tokenURI(uint256 id) public view virtual returns (string memory); + + /*////////////////////////////////////////////////////////////// + ERC721 BALANCE/OWNER STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(uint256 => address) internal _ownerOf; + + mapping(address => uint256) internal _balanceOf; + + function ownerOf(uint256 id) public view virtual returns (address owner) { + require((owner = _ownerOf[id]) != address(0), "NOT_MINTED"); + } + + function balanceOf(address owner) public view virtual returns (uint256) { + require(owner != address(0), "ZERO_ADDRESS"); + + return _balanceOf[owner]; + } + + /*////////////////////////////////////////////////////////////// + ERC721 APPROVAL STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(uint256 => address) public getApproved; + + mapping(address => mapping(address => bool)) internal _isApprovedForAll; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(string memory _name, string memory _symbol) { + name = _name; + symbol = _symbol; + } + + /*////////////////////////////////////////////////////////////// + ERC721 LOGIC + //////////////////////////////////////////////////////////////*/ + + function approve(address spender, uint256 id) public virtual { + address owner = _ownerOf[id]; + + require( + msg.sender == owner || isApprovedForAll(owner, msg.sender), + "NOT_AUTHORIZED" + ); + + getApproved[id] = spender; + + emit Approval(owner, spender, id); + } + + function setApprovalForAll(address operator, bool approved) public virtual { + _isApprovedForAll[msg.sender][operator] = approved; + + emit ApprovalForAll(msg.sender, operator, approved); + } + + function isApprovedForAll(address owner, address spender) + public + view + virtual + returns (bool) + { + return _isApprovedForAll[owner][spender]; + } + + function transferFrom( + address from, + address to, + uint256 id + ) public virtual { + require(from == _ownerOf[id], "WRONG_FROM"); + + require( + msg.sender == from || + isApprovedForAll(from, msg.sender) || + msg.sender == getApproved[id], + "NOT_AUTHORIZED" + ); + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + unchecked { + _balanceOf[from]--; + + _balanceOf[to]++; + } + + _ownerOf[id] = to; + + delete getApproved[id]; + + emit Transfer(from, to, id); + } + + function safeTransferFrom( + address from, + address to, + uint256 id + ) public virtual { + transferFrom(from, to, id); + + require( + to.code.length == 0 || + ERC721TokenReceiver(to).onERC721Received( + msg.sender, + from, + id, + "" + ) == + ERC721TokenReceiver.onERC721Received.selector, + "UNSAFE_RECIPIENT" + ); + } + + function safeTransferFrom( + address from, + address to, + uint256 id, + bytes calldata data + ) public virtual { + transferFrom(from, to, id); + + require( + to.code.length == 0 || + ERC721TokenReceiver(to).onERC721Received( + msg.sender, + from, + id, + data + ) == + ERC721TokenReceiver.onERC721Received.selector, + "UNSAFE_RECIPIENT" + ); + } + + /*////////////////////////////////////////////////////////////// + ERC165 LOGIC + //////////////////////////////////////////////////////////////*/ + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + returns (bool) + { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 + interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata + } + + /*////////////////////////////////////////////////////////////// + INTERNAL MINT/BURN LOGIC + //////////////////////////////////////////////////////////////*/ + + function _mint(address to, uint256 id) internal virtual { + require(_ownerOf[id] == address(0), "ALREADY_MINTED"); + + // Counter overflow is incredibly unrealistic. + unchecked { + _balanceOf[to]++; + } + + _ownerOf[id] = to; + + emit Transfer(address(0), to, id); + } + + function _burn(uint256 id) internal virtual { + address owner = _ownerOf[id]; + + require(owner != address(0), "NOT_MINTED"); + + // Ownership check above ensures no underflow. + unchecked { + _balanceOf[owner]--; + } + + delete _ownerOf[id]; + + delete getApproved[id]; + + emit Transfer(owner, address(0), id); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL SAFE MINT LOGIC + //////////////////////////////////////////////////////////////*/ + + function _safeMint(address to, uint256 id) internal virtual { + _mint(to, id); + + require( + to.code.length == 0 || + ERC721TokenReceiver(to).onERC721Received( + msg.sender, + address(0), + id, + "" + ) == + ERC721TokenReceiver.onERC721Received.selector, + "UNSAFE_RECIPIENT" + ); + } + + function _safeMint( + address to, + uint256 id, + bytes memory data + ) internal virtual { + _mint(to, id); + + require( + to.code.length == 0 || + ERC721TokenReceiver(to).onERC721Received( + msg.sender, + address(0), + id, + data + ) == + ERC721TokenReceiver.onERC721Received.selector, + "UNSAFE_RECIPIENT" + ); + } +} + +/// @notice A generic interface for a contract which properly accepts ERC721 tokens. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC721.sol) +abstract contract ERC721TokenReceiver { + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) external virtual returns (bytes4) { + return ERC721TokenReceiver.onERC721Received.selector; + } +} diff --git a/test/foundry/utils/ArithmeticUtil.sol b/test/foundry/utils/ArithmeticUtil.sol new file mode 100644 index 000000000..6c59092ba --- /dev/null +++ b/test/foundry/utils/ArithmeticUtil.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +library ArithmeticUtil { + ///@dev utility function to avoid overflows when multiplying fuzzed uints with widths <256 + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + return a * b; + } + + ///@dev utility function to avoid overflows when adding fuzzed uints with widths <256 + function add(uint256 a, uint256 b) internal pure returns (uint256) { + return a + b; + } + + ///@dev utility function to avoid overflows when subtracting fuzzed uints with widths <256 + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return a - b; + } + + ///@dev utility function to avoid overflows when dividing fuzzed uints with widths <256 + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return a / b; + } +} diff --git a/test/foundry/utils/BaseConsiderationTest.sol b/test/foundry/utils/BaseConsiderationTest.sol index 6a5e7790a..2b4d9ed34 100644 --- a/test/foundry/utils/BaseConsiderationTest.sol +++ b/test/foundry/utils/BaseConsiderationTest.sol @@ -1,19 +1,21 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { ConduitController } from "../../../contracts/conduit/ConduitController.sol"; import { ConsiderationInterface } from "../../../contracts/interfaces/ConsiderationInterface.sol"; import { OrderType, BasicOrderType, ItemType, Side } from "../../../contracts/lib/ConsiderationEnums.sol"; import { OfferItem, ConsiderationItem, OrderComponents, BasicOrderParameters } from "../../../contracts/lib/ConsiderationStructs.sol"; import { Test } from "forge-std/Test.sol"; +import { DifferentialTest } from "./DifferentialTest.sol"; +import { StructCopier } from "./StructCopier.sol"; import { stdStorage, StdStorage } from "forge-std/Test.sol"; import { ReferenceConduitController } from "../../../reference/conduit/ReferenceConduitController.sol"; import { ReferenceConsideration } from "../../../reference/ReferenceConsideration.sol"; import { Conduit } from "../../../contracts/conduit/Conduit.sol"; -import { Consideration } from "../../../contracts/Consideration.sol"; +import { Consideration } from "../../../contracts/lib/Consideration.sol"; /// @dev Base test case that deploys Consideration and its dependencies -contract BaseConsiderationTest is Test { +contract BaseConsiderationTest is DifferentialTest, StructCopier { using stdStorage for StdStorage; ConsiderationInterface consideration; @@ -26,24 +28,10 @@ contract BaseConsiderationTest is Test { function setUp() public virtual { conduitKeyOne = bytes32(uint256(uint160(address(this))) << 96); - vm.label(address(this), "testContract"); _deployAndConfigurePrecompiledOptimizedConsideration(); - string[] memory args = new string[](2); - args[0] = "echo"; - args[1] = "-n"; - // if ffi is enabled, this will not enter the catch block. - // assume that the local foundry profile is specified, and deploy - // reference normally, so stack traces and debugger have source map, - // with the caveat that reference contracts will have been compiled - // with 0.8.13 - try vm.ffi(args) { - emit log("Deploying reference from import"); - _deployAndConfigureReferenceConsideration(); - } catch (bytes memory) { - emit log("Deploying reference from precompiled source"); - _deployAndConfigurePrecompiledReferenceConsideration(); - } + emit log("Deploying reference from precompiled source"); + _deployAndConfigurePrecompiledReferenceConsideration(); vm.label(address(conduitController), "conduitController"); vm.label(address(consideration), "consideration"); @@ -54,31 +42,11 @@ contract BaseConsiderationTest is Test { ); vm.label(address(referenceConsideration), "referenceConsideration"); vm.label(address(referenceConduit), "referenceConduit"); + vm.label(address(this), "testContract"); } - function _deployAndConfigureReferenceConsideration() public { - referenceConduitController = ConduitController( - address(new ReferenceConduitController()) - ); - referenceConsideration = ConsiderationInterface( - address( - new ReferenceConsideration(address(referenceConduitController)) - ) - ); - referenceConduit = Conduit( - referenceConduitController.createConduit( - conduitKeyOne, - address(this) - ) - ); - referenceConduitController.updateChannel( - address(referenceConduit), - address(referenceConsideration), - true - ); - } - - ///@dev deploy optimized consideration contracts from pre-compiled source (solc-0.8.13, IR pipeline enabled) + ///@dev deploy optimized consideration contracts from pre-compiled source + // (solc-0.8.14, IR pipeline enabled) function _deployAndConfigurePrecompiledOptimizedConsideration() public { conduitController = ConduitController( deployCode( @@ -172,6 +140,46 @@ contract BaseConsiderationTest is Test { uint256 _pkOfSigner, bytes32 _orderHash ) internal returns (bytes memory) { + (bytes32 r, bytes32 s, uint8 v) = getSignatureComponents( + _consideration, + _pkOfSigner, + _orderHash + ); + return abi.encodePacked(r, s, v); + } + + function signOrder2098( + ConsiderationInterface _consideration, + uint256 _pkOfSigner, + bytes32 _orderHash + ) internal returns (bytes memory) { + (bytes32 r, bytes32 s, uint8 v) = getSignatureComponents( + _consideration, + _pkOfSigner, + _orderHash + ); + uint256 yParity; + if (v == 27) { + yParity = 0; + } else { + yParity = 1; + } + uint256 yParityAndS = (yParity << 255) | uint256(s); + return abi.encodePacked(r, yParityAndS); + } + + function getSignatureComponents( + ConsiderationInterface _consideration, + uint256 _pkOfSigner, + bytes32 _orderHash + ) + internal + returns ( + bytes32, + bytes32, + uint8 + ) + { (, bytes32 domainSeparator, ) = _consideration.information(); (uint8 v, bytes32 r, bytes32 s) = vm.sign( _pkOfSigner, @@ -179,19 +187,6 @@ contract BaseConsiderationTest is Test { abi.encodePacked(bytes2(0x1901), domainSeparator, _orderHash) ) ); - return abi.encodePacked(r, s, v); - } - - /** - * @dev reset all storage written at an address thus far to 0; will overwrite totalSupply()for ERC20s but that should be fine - * with the goal of resetting the balances and owners of tokens - but note: should be careful about approvals, etc - * - * note: must be called in conjunction with vm.record() - */ - function _resetStorage(address _addr) internal { - (, bytes32[] memory writeSlots) = vm.accesses(_addr); - for (uint256 i = 0; i < writeSlots.length; i++) { - vm.store(_addr, writeSlots[i], bytes32(0)); - } + return (r, s, v); } } diff --git a/test/foundry/utils/BaseOrderTest.sol b/test/foundry/utils/BaseOrderTest.sol index fe2de1c17..084e43dcd 100644 --- a/test/foundry/utils/BaseOrderTest.sol +++ b/test/foundry/utils/BaseOrderTest.sol @@ -1,56 +1,29 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { BaseConsiderationTest } from "./BaseConsiderationTest.sol"; import { stdStorage, StdStorage } from "forge-std/Test.sol"; -import { TestERC1155 } from "../../../contracts/test/TestERC1155.sol"; -import { TestERC20 } from "../../../contracts/test/TestERC20.sol"; -import { TestERC721 } from "../../../contracts/test/TestERC721.sol"; -import { ERC721Recipient } from "./ERC721Recipient.sol"; -import { ERC1155Recipient } from "./ERC1155Recipient.sol"; import { ProxyRegistry } from "../interfaces/ProxyRegistry.sol"; import { OwnableDelegateProxy } from "../interfaces/OwnableDelegateProxy.sol"; -import { ConsiderationItem, OfferItem, Fulfillment, FulfillmentComponent, ItemType, OrderComponents, OrderParameters } from "../../../contracts/lib/ConsiderationStructs.sol"; +import { OneWord } from "../../../contracts/lib/ConsiderationConstants.sol"; +import { ConsiderationInterface } from "../../../contracts/interfaces/ConsiderationInterface.sol"; +import { BasicOrderType, OrderType } from "../../../contracts/lib/ConsiderationEnums.sol"; +import { BasicOrderParameters, ConsiderationItem, AdditionalRecipient, OfferItem, Fulfillment, FulfillmentComponent, ItemType, Order, OrderComponents, OrderParameters } from "../../../contracts/lib/ConsiderationStructs.sol"; +import { ArithmeticUtil } from "./ArithmeticUtil.sol"; +import { OfferConsiderationItemAdder } from "./OfferConsiderationItemAdder.sol"; +import { AmountDeriver } from "../../../contracts/lib/AmountDeriver.sol"; /// @dev base test class for cases that depend on pre-deployed token contracts -contract BaseOrderTest is - BaseConsiderationTest, - ERC721Recipient, - ERC1155Recipient -{ +contract BaseOrderTest is OfferConsiderationItemAdder, AmountDeriver { using stdStorage for StdStorage; + using ArithmeticUtil for uint256; + using ArithmeticUtil for uint128; + using ArithmeticUtil for uint120; - uint256 constant MAX_INT = ~uint256(0); + uint256 internal globalSalt; - uint256 internal alicePk = 0xa11ce; - uint256 internal bobPk = 0xb0b; - uint256 internal calPk = 0xca1; - address payable internal alice = payable(vm.addr(alicePk)); - address payable internal bob = payable(vm.addr(bobPk)); - address payable internal cal = payable(vm.addr(calPk)); - - TestERC20 internal token1; - TestERC20 internal token2; - TestERC20 internal token3; - - TestERC721 internal test721_1; - TestERC721 internal test721_2; - TestERC721 internal test721_3; - - TestERC1155 internal test1155_1; - TestERC1155 internal test1155_2; - TestERC1155 internal test1155_3; - - address[] allTokens; - TestERC20[] erc20s; - TestERC721[] erc721s; - TestERC1155[] erc1155s; - address[] accounts; - - OfferItem offerItem; - ConsiderationItem considerationItem; - OfferItem[] offerItems; - ConsiderationItem[] considerationItems; + OrderParameters baseOrderParameters; + OrderComponents baseOrderComponents; FulfillmentComponent[] offerComponents; FulfillmentComponent[] considerationComponents; @@ -75,12 +48,17 @@ contract BaseOrderTest is FulfillmentComponent[] fulfillmentComponents; Fulfillment fulfillment; - uint256 internal globalTokenId; + AdditionalRecipient[] additionalRecipients; - struct RestoreERC20Balance { - address token; - address who; - } + event Transfer(address indexed from, address indexed to, uint256 value); + + event TransferSingle( + address indexed operator, + address indexed from, + address indexed to, + uint256 id, + uint256 value + ); modifier onlyPayable(address _addr) { { @@ -90,45 +68,13 @@ contract BaseOrderTest is success := call(gas(), _addr, 1, 0, 0, 0, 0) } vm.assume(success); + vm.deal(address(this), uint128(MAX_INT)); } _; } - /** - @dev top up eth of this contract to uint128(MAX_INT) to avoid fuzz failures - */ - modifier topUp() { - vm.deal(address(this), uint128(MAX_INT)); - _; - } - - /** - * @dev hook to record storage writes and reset token balances in between differential runs - */ - - modifier resetTokenBalancesBetweenRuns() { - vm.record(); - _; - _resetTokensAndEthForTestAccounts(); - // todo: don't delete these between runs, do setup outside of test logic - delete offerItems; - delete considerationItems; - delete offerComponentsArray; - delete considerationComponentsArray; - delete fulfillments; - delete firstFulfillment; - delete secondFulfillment; - delete fulfillmentComponent; - delete fulfillmentComponents; - } - function setUp() public virtual override { super.setUp(); - delete offerItems; - delete considerationItems; - delete offerComponentsArray; - delete considerationComponentsArray; - delete fulfillments; vm.label(alice, "alice"); vm.label(bob, "bob"); @@ -136,24 +82,11 @@ contract BaseOrderTest is vm.label(address(this), "testContract"); _deployTestTokenContracts(); - accounts = [alice, bob, cal, address(this)]; erc20s = [token1, token2, token3]; erc721s = [test721_1, test721_2, test721_3]; erc1155s = [test1155_1, test1155_2, test1155_3]; - allTokens = [ - address(token1), - address(token2), - address(token3), - address(test721_1), - address(test721_2), - address(test721_3), - address(test1155_1), - address(test1155_2), - address(test1155_3) - ]; // allocate funds and tokens to test addresses - globalTokenId = 1; allocateTokensAndApprovals(address(this), uint128(MAX_INT)); allocateTokensAndApprovals(alice, uint128(MAX_INT)); allocateTokensAndApprovals(bob, uint128(MAX_INT)); @@ -168,257 +101,237 @@ contract BaseOrderTest is delete considerationComponents; } - function _configureConsiderationItem( - address payable recipient, - ItemType itemType, - uint256 identifier, - uint256 amt - ) internal { - if (itemType == ItemType.NATIVE) { - _configureEthConsiderationItem(recipient, amt); - } else if (itemType == ItemType.ERC20) { - _configureErc20ConsiderationItem(recipient, amt); - } else if (itemType == ItemType.ERC1155) { - _configureErc1155ConsiderationItem(recipient, identifier, amt); - } else { - _configureErc721ConsiderationItem(recipient, identifier); - } - } - - function _configureOfferItem( - ItemType itemType, - uint256 identifier, - uint256 amt - ) internal { - if (itemType == ItemType.NATIVE) { - _configureEthOfferItem(amt); - } else if (itemType == ItemType.ERC20) { - _configureERC20OfferItem(amt); - } else if (itemType == ItemType.ERC1155) { - _configureERC1155OfferItem(identifier, amt); - } else { - _configureERC721OfferItem(identifier); - } - } - - function _configureERC721OfferItem(uint256 tokenId) internal { - _configureOfferItem(ItemType.ERC721, address(test721_1), tokenId, 1, 1); + function _validateOrder( + Order memory order, + ConsiderationInterface _consideration + ) internal returns (bool) { + Order[] memory orders = new Order[](1); + orders[0] = order; + return _consideration.validate(orders); } - function _configureERC1155OfferItem(uint256 tokenId, uint256 amount) + function _prepareOrder(uint256 tokenId, uint256 totalConsiderationItems) internal + returns ( + Order memory order, + OrderParameters memory orderParameters, + bytes memory signature + ) { - _configureOfferItem( - ItemType.ERC1155, - address(test1155_1), - tokenId, - amount, - amount + test1155_1.mint(address(this), tokenId, 10); + + addErc1155OfferItem(tokenId, 10); + for (uint256 i = 0; i < totalConsiderationItems; i++) { + addErc20ConsiderationItem(alice, 10); + } + uint256 nonce = consideration.getCounter(address(this)); + + orderParameters = getOrderParameters( + payable(this), + OrderType.FULL_OPEN ); - } + OrderComponents memory orderComponents = toOrderComponents( + orderParameters, + nonce + ); + + bytes32 orderHash = consideration.getOrderHash(orderComponents); - function _configureERC20OfferItem(uint256 amount) internal { - _configureOfferItem(ItemType.ERC20, address(token1), 0, amount, amount); + signature = signOrder(consideration, alicePk, orderHash); + order = Order(orderParameters, signature); } - function _configureERC1155OfferItem( - uint256 tokenId, - uint256 startAmount, - uint256 endAmount - ) internal { - _configureOfferItem( - ItemType.ERC1155, - address(test1155_1), - tokenId, - startAmount, - endAmount + function _subtractAmountFromLengthInOrderCalldata( + bytes memory orderCalldata, + uint256 relativeOrderParametersOffset, + uint256 relativeItemsLengthOffset, + uint256 amtToSubtractFromLength + ) internal pure { + bytes32 lengthPtr = _getItemsLengthPointerInOrderCalldata( + orderCalldata, + relativeOrderParametersOffset, + relativeItemsLengthOffset ); + assembly { + let length := mload(lengthPtr) + mstore(lengthPtr, sub(length, amtToSubtractFromLength)) + } } - function _configureEthOfferItem(uint256 paymentAmount) internal { - _configureOfferItem( - ItemType.NATIVE, - address(0), - 0, - paymentAmount, - paymentAmount - ); + function _getItemsLengthPointerInOrderCalldata( + bytes memory orderCalldata, + uint256 relativeOrderParametersOffset, + uint256 relativeItemsLengthOffset + ) internal pure returns (bytes32 lengthPtr) { + assembly { + // Points to the order parameters in the order calldata. + let orderParamsOffsetPtr := add( + orderCalldata, + relativeOrderParametersOffset + ) + // Points to the items offset value. + // Note: itemsOffsetPtr itself is not the offset value; + // the value stored at itemsOffsetPtr is the offset value. + let itemsOffsetPtr := add( + orderParamsOffsetPtr, + relativeItemsLengthOffset + ) + // Value of the items offset, which is the offset of the items + // array relative to the start of order parameters. + let itemsOffsetValue := mload(itemsOffsetPtr) + + // The memory for an array will always start with a word + // indicating the length of the array, so length pointer + // can simply point to the start of the items array. + lengthPtr := add(orderParamsOffsetPtr, itemsOffsetValue) + } } - function _configureEthConsiderationItem( - address payable recipient, - uint256 paymentAmount - ) internal { - _configureConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - paymentAmount, - paymentAmount, - recipient + function _getItemsLengthAtOffsetInOrderCalldata( + bytes memory orderCalldata, + // Relative offset of start of order parameters + // in the order calldata. + uint256 relativeOrderParametersOffset, + // Relative offset of items pointer (which points to items' length) + // to the start of order parameters in order calldata. + uint256 relativeItemsLengthOffset + ) internal pure returns (uint256 length) { + bytes32 lengthPtr = _getItemsLengthPointerInOrderCalldata( + orderCalldata, + relativeOrderParametersOffset, + relativeItemsLengthOffset ); + assembly { + length := mload(lengthPtr) + } } - function _configureEthConsiderationItem( - address payable recipient, - uint256 startAmount, - uint256 endAmount + function _performTestFulfillOrderRevertInvalidArrayLength( + ConsiderationInterface _consideration, + Order memory order, + bytes memory fulfillOrderCalldata, + // Relative offset of start of order parameters + // in the order calldata. + uint256 relativeOrderParametersOffset, + // Relative offset of items pointer (which points to items' length) + // to the start of order parameters in order calldata. + uint256 relativeItemsLengthOffset, + uint256 originalItemsLength, + uint256 amtToSubtractFromItemsLength ) internal { - _configureConsiderationItem( - ItemType.NATIVE, - address(0), - 0, - startAmount, - endAmount, - recipient + assertTrue(_validateOrder(order, _consideration)); + + bool overwriteItemsLength = amtToSubtractFromItemsLength > 0; + if (overwriteItemsLength) { + // Get the array length from the calldata and + // store the length - amtToSubtractFromItemsLength in the calldata + // so that the length value does _not_ accurately represent the actual + // total array length. + _subtractAmountFromLengthInOrderCalldata( + fulfillOrderCalldata, + relativeOrderParametersOffset, + relativeItemsLengthOffset, + amtToSubtractFromItemsLength + ); + } + + uint256 finalItemsLength = _getItemsLengthAtOffsetInOrderCalldata( + fulfillOrderCalldata, + // Relative offset of start of order parameters + // in the order calldata. + relativeOrderParametersOffset, + // Relative offset of items + // pointer to the start of order parameters in order calldata. + relativeItemsLengthOffset ); - } - function _configureErc20ConsiderationItem( - address payable receiver, - uint256 paymentAmount - ) internal { - _configureConsiderationItem( - ItemType.ERC20, - address(token1), - 0, - paymentAmount, - paymentAmount, - receiver + assertEq( + finalItemsLength, + originalItemsLength - amtToSubtractFromItemsLength ); - } - function _configureErc721ConsiderationItem( - address payable recipient, - uint256 tokenId - ) internal { - _configureConsiderationItem( - ItemType.ERC721, - address(test721_1), - tokenId, - 1, - 1, - recipient + bool success = _callConsiderationFulfillOrderWithCalldata( + address(_consideration), + fulfillOrderCalldata ); + + // If overwriteItemsLength is True, the call should + // have failed (success should be False) and if overwriteItemsLength is False, + // the call should have succeeded (success should be True). + assertEq(success, !overwriteItemsLength); } - function _configureErc1155ConsiderationItem( - address payable recipient, - uint256 tokenId, - uint256 amount - ) internal { - _configureConsiderationItem( - ItemType.ERC1155, - address(test1155_1), - tokenId, - amount, - amount, - recipient - ); + function _callConsiderationFulfillOrderWithCalldata( + address considerationAddress, + bytes memory orderCalldata + ) internal returns (bool success) { + (success, ) = considerationAddress.call(orderCalldata); } - function _configureOfferItem( - ItemType itemType, - address token, - uint256 identifier, - uint256 startAmount, - uint256 endAmount - ) internal { - offerItem.itemType = itemType; - offerItem.token = token; - offerItem.identifierOrCriteria = identifier; - offerItem.startAmount = startAmount; - offerItem.endAmount = endAmount; - offerItems.push(offerItem); + function configureOrderParameters(address offerer) internal { + _configureOrderParameters( + offerer, + address(0), + bytes32(0), + globalSalt++, + false + ); } - function _configureConsiderationItem( - ItemType itemType, - address token, - uint256 identifier, - uint256 startAmount, - uint256 endAmount, - address payable recipient + function _configureOrderParameters( + address offerer, + address zone, + bytes32 zoneHash, + uint256 salt, + bool useConduit ) internal { - considerationItem.itemType = itemType; - considerationItem.token = token; - considerationItem.identifierOrCriteria = identifier; - considerationItem.startAmount = startAmount; - considerationItem.endAmount = endAmount; - considerationItem.recipient = recipient; - considerationItems.push(considerationItem); + bytes32 conduitKey = useConduit ? conduitKeyOne : bytes32(0); + baseOrderParameters.offerer = offerer; + baseOrderParameters.zone = zone; + baseOrderParameters.offer = offerItems; + baseOrderParameters.consideration = considerationItems; + baseOrderParameters.orderType = OrderType.FULL_OPEN; + baseOrderParameters.startTime = block.timestamp; + baseOrderParameters.endTime = block.timestamp + 1; + baseOrderParameters.zoneHash = zoneHash; + baseOrderParameters.salt = salt; + baseOrderParameters.conduitKey = conduitKey; + baseOrderParameters.totalOriginalConsiderationItems = considerationItems + .length; + } + + function _configureOrderParametersSetEndTime( + address offerer, + address zone, + uint256 endTime, + bytes32 zoneHash, + uint256 salt, + bool useConduit + ) internal { + _configureOrderParameters(offerer, zone, zoneHash, salt, useConduit); + baseOrderParameters.endTime = endTime; } /** - @dev deploy test token contracts + @dev configures order components based on order parameters in storage and counter param */ - function _deployTestTokenContracts() internal { - token1 = new TestERC20(); - token2 = new TestERC20(); - token3 = new TestERC20(); - test721_1 = new TestERC721(); - test721_2 = new TestERC721(); - test721_3 = new TestERC721(); - test1155_1 = new TestERC1155(); - test1155_2 = new TestERC1155(); - test1155_3 = new TestERC1155(); - vm.label(address(token1), "token1"); - vm.label(address(test721_1), "test721_1"); - vm.label(address(test1155_1), "test1155_1"); - - emit log("Deployed test token contracts"); - } - - /** - @dev allocate amount of each token, 1 of each 721, and 1, 5, and 10 of respective 1155s - */ - function allocateTokensAndApprovals(address _to, uint128 _amount) internal { - vm.deal(_to, _amount); - for (uint256 i = 0; i < erc20s.length; i++) { - erc20s[i].mint(_to, _amount); - } - emit log_named_address("Allocated tokens to", _to); - _setApprovals(_to); - } - - function _setApprovals(address _owner) internal { - vm.startPrank(_owner); - for (uint256 i = 0; i < erc20s.length; i++) { - erc20s[i].approve(address(consideration), MAX_INT); - erc20s[i].approve(address(referenceConsideration), MAX_INT); - erc20s[i].approve(address(conduit), MAX_INT); - erc20s[i].approve(address(referenceConduit), MAX_INT); - } - for (uint256 i = 0; i < erc721s.length; i++) { - erc721s[i].setApprovalForAll(address(consideration), true); - erc721s[i].setApprovalForAll(address(referenceConsideration), true); - erc721s[i].setApprovalForAll(address(conduit), true); - erc721s[i].setApprovalForAll(address(referenceConduit), true); - } - for (uint256 i = 0; i < erc1155s.length; i++) { - erc1155s[i].setApprovalForAll(address(consideration), true); - erc1155s[i].setApprovalForAll( - address(referenceConsideration), - true - ); - erc1155s[i].setApprovalForAll(address(conduit), true); - erc1155s[i].setApprovalForAll(address(referenceConduit), true); - } - - vm.stopPrank(); - emit log_named_address( - "Owner proxy approved for all tokens from", - _owner - ); - emit log_named_address( - "Consideration approved for all tokens from", - _owner - ); + function _configureOrderComponents(uint256 counter) internal { + baseOrderComponents.offerer = baseOrderParameters.offerer; + baseOrderComponents.zone = baseOrderParameters.zone; + baseOrderComponents.offer = baseOrderParameters.offer; + baseOrderComponents.consideration = baseOrderParameters.consideration; + baseOrderComponents.orderType = baseOrderParameters.orderType; + baseOrderComponents.startTime = baseOrderParameters.startTime; + baseOrderComponents.endTime = baseOrderParameters.endTime; + baseOrderComponents.zoneHash = baseOrderParameters.zoneHash; + baseOrderComponents.salt = baseOrderParameters.salt; + baseOrderComponents.conduitKey = baseOrderParameters.conduitKey; + baseOrderComponents.counter = counter; } function getMaxConsiderationValue() internal view returns (uint256) { uint256 value = 0; - for (uint256 i = 0; i < considerationItems.length; i++) { + for (uint256 i = 0; i < considerationItems.length; ++i) { uint256 amount = considerationItems[i].startAmount > considerationItems[i].endAmount ? considerationItems[i].startAmount @@ -429,11 +342,11 @@ contract BaseOrderTest is } /** - * @dev return OrderComponents for a given OrderParameters and offerer nonce + * @dev return OrderComponents for a given OrderParameters and offerer counter */ function getOrderComponents( OrderParameters memory parameters, - uint256 nonce + uint256 counter ) internal pure returns (OrderComponents memory) { return OrderComponents( @@ -447,60 +360,113 @@ contract BaseOrderTest is parameters.zoneHash, parameters.salt, parameters.conduitKey, - nonce + counter ); } - /** - * @dev reset written token storage slots to 0 and reinitialize uint128(MAX_INT) erc20 balances for 3 test accounts and this - */ - function _resetTokensAndEthForTestAccounts() internal { - _resetTokensStorage(); - _restoreERC20Balances(); - _restoreEthBalances(); - } - - function _restoreEthBalances() internal { - for (uint256 i = 0; i < accounts.length; i++) { - vm.deal(accounts[i], uint128(MAX_INT)); - } + function getOrderParameters(address payable offerer, OrderType orderType) + internal + returns (OrderParameters memory) + { + return + OrderParameters( + offerer, + address(0), + offerItems, + considerationItems, + orderType, + block.timestamp, + block.timestamp + 1, + bytes32(0), + globalSalt++, + bytes32(0), + considerationItems.length + ); } - function _resetTokensStorage() internal { - for (uint256 i = 0; i < allTokens.length; i++) { - _resetStorage(allTokens[i]); - } + function toOrderComponents(OrderParameters memory _params, uint256 nonce) + internal + pure + returns (OrderComponents memory) + { + return + OrderComponents( + _params.offerer, + _params.zone, + _params.offer, + _params.consideration, + _params.orderType, + _params.startTime, + _params.endTime, + _params.zoneHash, + _params.salt, + _params.conduitKey, + nonce + ); } - /** - * @dev restore erc20 balances for all accounts - */ - function _restoreERC20Balances() internal { - for (uint256 i = 0; i < accounts.length; i++) { - _restoreERC20BalancesForAddress(accounts[i]); - } + function toBasicOrderParameters( + Order memory _order, + BasicOrderType basicOrderType + ) internal pure returns (BasicOrderParameters memory) { + return + BasicOrderParameters( + _order.parameters.consideration[0].token, + _order.parameters.consideration[0].identifierOrCriteria, + _order.parameters.consideration[0].endAmount, + payable(_order.parameters.offerer), + _order.parameters.zone, + _order.parameters.offer[0].token, + _order.parameters.offer[0].identifierOrCriteria, + _order.parameters.offer[0].endAmount, + basicOrderType, + _order.parameters.startTime, + _order.parameters.endTime, + _order.parameters.zoneHash, + _order.parameters.salt, + _order.parameters.conduitKey, + _order.parameters.conduitKey, + 0, + new AdditionalRecipient[](0), + _order.signature + ); } - /** - * @dev restore all erc20 balances for a given address - */ - function _restoreERC20BalancesForAddress(address _who) internal { - for (uint256 i = 0; i < erc20s.length; i++) { - _restoreERC20Balance(RestoreERC20Balance(address(erc20s[i]), _who)); - } + function toBasicOrderParameters( + OrderComponents memory _order, + BasicOrderType basicOrderType, + bytes memory signature + ) internal pure returns (BasicOrderParameters memory) { + return + BasicOrderParameters( + _order.consideration[0].token, + _order.consideration[0].identifierOrCriteria, + _order.consideration[0].endAmount, + payable(_order.offerer), + _order.zone, + _order.offer[0].token, + _order.offer[0].identifierOrCriteria, + _order.offer[0].endAmount, + basicOrderType, + _order.startTime, + _order.endTime, + _order.zoneHash, + _order.salt, + _order.conduitKey, + _order.conduitKey, + 0, + new AdditionalRecipient[](0), + signature + ); } - /** - * @dev reset token balance for an address to uint128(MAX_INT) - */ - function _restoreERC20Balance( - RestoreERC20Balance memory restoreErc20Balance - ) internal { - stdstore - .target(restoreErc20Balance.token) - .sig("balanceOf(address)") - .with_key(restoreErc20Balance.who) - .checked_write(uint128(MAX_INT)); + ///@dev allow signing for this contract since it needs to be recipient of basic order to reenter on receive + function isValidSignature(bytes32, bytes memory) + external + pure + returns (bytes4) + { + return 0x1626ba7e; } receive() external payable virtual {} diff --git a/test/foundry/utils/DifferentialTest.sol b/test/foundry/utils/DifferentialTest.sol new file mode 100644 index 000000000..a002a19f7 --- /dev/null +++ b/test/foundry/utils/DifferentialTest.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; +import { Test } from "forge-std/Test.sol"; + +contract DifferentialTest is Test { + // slot where HEVM stores bool of whether or not an assertion has failed + bytes32 HEVM_FAILED_SLOT = + 0x6661696c65640000000000000000000000000000000000000000000000000000; + + // hash of the bytes surfaced by `revert RevertWithFailureStatus(false)` + bytes32 PASSING_HASH = + 0xf951c460268b64a0aabc103be9b020b90c4d14012c2d21f9c441a69438400a57; + + error RevertWithFailureStatus(bool status); + + ///@dev reverts after function body with the failure status, clearing all state changes made + modifier stateless() { + _; + revertWithFailureStatus(); + } + + function assertPass(bytes memory reason) internal view { + if (keccak256(reason) != PASSING_HASH) { + revert(); + } + } + + function revertWithFailureStatus() internal { + revert RevertWithFailureStatus(readHevmFailureSlot()); + } + + function readHevmFailureSlot() internal returns (bool) { + return vm.load(address(vm), HEVM_FAILED_SLOT) == bytes32(uint256(1)); + } +} diff --git a/test/foundry/utils/ERC1155Recipient.sol b/test/foundry/utils/ERC1155Recipient.sol index 17be10eaa..075d7d741 100644 --- a/test/foundry/utils/ERC1155Recipient.sol +++ b/test/foundry/utils/ERC1155Recipient.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { ERC1155TokenReceiver } from "@rari-capital/solmate/src/tokens/ERC1155.sol"; @@ -10,7 +10,7 @@ contract ERC1155Recipient is ERC1155TokenReceiver { uint256, uint256, bytes calldata - ) public pure override returns (bytes4) { + ) public virtual override returns (bytes4) { return ERC1155TokenReceiver.onERC1155Received.selector; } @@ -20,7 +20,7 @@ contract ERC1155Recipient is ERC1155TokenReceiver { uint256[] calldata, uint256[] calldata, bytes calldata - ) external pure override returns (bytes4) { + ) external virtual override returns (bytes4) { return ERC1155TokenReceiver.onERC1155BatchReceived.selector; } } diff --git a/test/foundry/utils/ERC721Recipient.sol b/test/foundry/utils/ERC721Recipient.sol index 04e97cdab..2a09d0f69 100644 --- a/test/foundry/utils/ERC721Recipient.sol +++ b/test/foundry/utils/ERC721Recipient.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { ERC721TokenReceiver } from "@rari-capital/solmate/src/tokens/ERC721.sol"; diff --git a/test/foundry/utils/ExternalCounter.sol b/test/foundry/utils/ExternalCounter.sol new file mode 100644 index 000000000..290563785 --- /dev/null +++ b/test/foundry/utils/ExternalCounter.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +contract ExternalCounter { + uint256 public value; + + function increment() external returns (uint256) { + return value++; + } +} diff --git a/test/foundry/utils/OfferConsiderationItemAdder.sol b/test/foundry/utils/OfferConsiderationItemAdder.sol new file mode 100644 index 000000000..989378b92 --- /dev/null +++ b/test/foundry/utils/OfferConsiderationItemAdder.sol @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import { ConsiderationItem, OfferItem, ItemType } from "../../../contracts/lib/ConsiderationStructs.sol"; +import { TestTokenMinter } from "./TestTokenMinter.sol"; + +contract OfferConsiderationItemAdder is TestTokenMinter { + OfferItem offerItem; + ConsiderationItem considerationItem; + OfferItem[] offerItems; + ConsiderationItem[] considerationItems; + + function addConsiderationItem( + address payable recipient, + ItemType itemType, + uint256 identifier, + uint256 amt + ) internal { + if (itemType == ItemType.NATIVE) { + addEthConsiderationItem(recipient, amt); + } else if (itemType == ItemType.ERC20) { + addErc20ConsiderationItem(recipient, amt); + } else if (itemType == ItemType.ERC1155) { + addErc1155ConsiderationItem(recipient, identifier, amt); + } else { + addErc721ConsiderationItem(recipient, identifier); + } + } + + function addOfferItem( + ItemType itemType, + uint256 identifier, + uint256 startAmount, + uint256 endAmount + ) internal { + if (itemType == ItemType.NATIVE) { + addEthOfferItem(startAmount, endAmount); + } else if (itemType == ItemType.ERC20) { + addErc20OfferItem(startAmount, endAmount); + } else if (itemType == ItemType.ERC1155) { + addErc1155OfferItem(identifier, startAmount, endAmount); + } else { + addErc721OfferItem(identifier); + } + } + + function addOfferItem( + ItemType itemType, + uint256 identifier, + uint256 amt + ) internal { + addOfferItem(itemType, identifier, amt, amt); + } + + function addErc721OfferItem(uint256 identifier) internal { + addErc721OfferItem(address(test721_1), identifier); + } + + function addErc721OfferItem(address token, uint256 identifier) internal { + addErc721OfferItem(token, identifier, 1, 1); + } + + function addErc721OfferItem( + address token, + uint256 identifier, + uint256 amount + ) internal { + addErc721OfferItem(token, identifier, amount, amount); + } + + function addErc721OfferItem( + address token, + uint256 identifier, + uint256 startAmount, + uint256 endAmount + ) internal { + addOfferItem( + ItemType.ERC721, + token, + identifier, + startAmount, + endAmount + ); + } + + function addErc1155OfferItem(uint256 tokenId, uint256 amount) internal { + addOfferItem( + ItemType.ERC1155, + address(test1155_1), + tokenId, + amount, + amount + ); + } + + function addErc20OfferItem(uint256 startAmount, uint256 endAmount) + internal + { + addOfferItem( + ItemType.ERC20, + address(token1), + 0, + startAmount, + endAmount + ); + } + + function addErc20OfferItem(uint256 amount) internal { + addErc20OfferItem(amount, amount); + } + + function addErc1155OfferItem( + uint256 tokenId, + uint256 startAmount, + uint256 endAmount + ) internal { + addOfferItem( + ItemType.ERC1155, + address(test1155_1), + tokenId, + startAmount, + endAmount + ); + } + + function addEthOfferItem(uint256 startAmount, uint256 endAmount) internal { + addOfferItem(ItemType.NATIVE, address(0), 0, startAmount, endAmount); + } + + function addEthOfferItem(uint256 paymentAmount) internal { + addEthOfferItem(paymentAmount, paymentAmount); + } + + function addEthConsiderationItem( + address payable recipient, + uint256 paymentAmount + ) internal { + addConsiderationItem( + recipient, + ItemType.NATIVE, + address(0), + 0, + paymentAmount, + paymentAmount + ); + } + + function addEthConsiderationItem( + address payable recipient, + uint256 startAmount, + uint256 endAmount + ) internal { + addConsiderationItem( + recipient, + ItemType.NATIVE, + address(0), + 0, + startAmount, + endAmount + ); + } + + function addErc20ConsiderationItem( + address payable receiver, + uint256 startAmount, + uint256 endAmount + ) internal { + addConsiderationItem( + receiver, + ItemType.ERC20, + address(token1), + 0, + startAmount, + endAmount + ); + } + + function addErc20ConsiderationItem( + address payable receiver, + uint256 paymentAmount + ) internal { + addErc20ConsiderationItem(receiver, paymentAmount, paymentAmount); + } + + function addErc721ConsiderationItem( + address payable recipient, + uint256 tokenId + ) internal { + addConsiderationItem( + recipient, + ItemType.ERC721, + address(test721_1), + tokenId, + 1, + 1 + ); + } + + function addErc1155ConsiderationItem( + address payable recipient, + uint256 tokenId, + uint256 amount + ) internal { + addConsiderationItem( + recipient, + ItemType.ERC1155, + address(test1155_1), + tokenId, + amount, + amount + ); + } + + function addOfferItem( + ItemType itemType, + address token, + uint256 identifier, + uint256 startAmount, + uint256 endAmount + ) internal { + offerItem.itemType = itemType; + offerItem.token = token; + offerItem.identifierOrCriteria = identifier; + offerItem.startAmount = startAmount; + offerItem.endAmount = endAmount; + offerItems.push(offerItem); + } + + function addConsiderationItem( + address payable recipient, + ItemType itemType, + address token, + uint256 identifier, + uint256 startAmount, + uint256 endAmount + ) internal { + considerationItem.itemType = itemType; + considerationItem.token = token; + considerationItem.identifierOrCriteria = identifier; + considerationItem.startAmount = startAmount; + considerationItem.endAmount = endAmount; + considerationItem.recipient = recipient; + considerationItems.push(considerationItem); + } +} diff --git a/test/foundry/utils/PseudoRandom.sol b/test/foundry/utils/PseudoRandom.sol new file mode 100644 index 000000000..dc438c569 --- /dev/null +++ b/test/foundry/utils/PseudoRandom.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +contract PseudoRandom { + bytes32 seedHash; + + constructor(bytes32 _seedHash) { + seedHash = _seedHash; + } + + function prandUint256() external returns (uint256) { + return uint256(updateSeedHash()); + } + + function prandBytes32() external returns (bytes32) { + return updateSeedHash(); + } + + function updateSeedHash() internal returns (bytes32) { + seedHash = keccak256(abi.encode(seedHash)); + return seedHash; + } +} diff --git a/test/foundry/utils/StructCopier.sol b/test/foundry/utils/StructCopier.sol new file mode 100644 index 000000000..ac8f08491 --- /dev/null +++ b/test/foundry/utils/StructCopier.sol @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; +import { BasicOrderParameters, CriteriaResolver, AdvancedOrder, AdditionalRecipient, OfferItem, Order, ConsiderationItem, Fulfillment, FulfillmentComponent, OrderParameters, OrderComponents } from "../../../contracts/lib/ConsiderationStructs.sol"; +import { ConsiderationInterface } from "../../../contracts/interfaces/ConsiderationInterface.sol"; + +contract StructCopier { + Order _tempOrder; + AdvancedOrder _tempAdvancedOrder; + FulfillmentComponent[] _tempFulfillmentComponents; + + function setBasicOrderParameters( + BasicOrderParameters storage dest, + BasicOrderParameters memory src + ) internal { + dest.considerationToken = src.considerationToken; + dest.considerationIdentifier = src.considerationIdentifier; + dest.considerationAmount = src.considerationAmount; + dest.offerer = src.offerer; + dest.zone = src.zone; + dest.offerToken = src.offerToken; + dest.offerIdentifier = src.offerIdentifier; + dest.offerAmount = src.offerAmount; + dest.basicOrderType = src.basicOrderType; + dest.startTime = src.endTime; + dest.endTime = src.endTime; + dest.zoneHash = src.zoneHash; + dest.salt = src.salt; + dest.offererConduitKey = src.offererConduitKey; + dest.fulfillerConduitKey = src.fulfillerConduitKey; + dest.totalOriginalAdditionalRecipients = src + .totalOriginalAdditionalRecipients; + setAdditionalRecipients( + dest.additionalRecipients, + src.additionalRecipients + ); + dest.signature = src.signature; + } + + function setOrderComponents( + OrderComponents storage dest, + OrderComponents memory src + ) internal { + dest.offerer = src.offerer; + dest.zone = src.zone; + setOfferItems(dest.offer, src.offer); + setConsiderationItems(dest.consideration, src.consideration); + dest.orderType = src.orderType; + dest.startTime = src.startTime; + dest.endTime = src.endTime; + dest.zoneHash = src.zoneHash; + dest.salt = src.salt; + dest.conduitKey = src.conduitKey; + dest.counter = src.counter; + } + + function setAdditionalRecipients( + AdditionalRecipient[] storage dest, + AdditionalRecipient[] memory src + ) internal { + while (dest.length != 0) { + dest.pop(); + } + for (uint256 i = 0; i < src.length; ++i) { + dest.push(src[i]); + } + } + + function setBytes32Array(bytes32[] storage dest, bytes32[] memory src) + internal + { + while (dest.length != 0) { + dest.pop(); + } + for (uint256 i = 0; i < src.length; ++i) { + dest.push(src[i]); + } + } + + function setCriteriaResolver( + CriteriaResolver storage dest, + CriteriaResolver memory src + ) internal { + dest.orderIndex = src.orderIndex; + dest.side = src.side; + dest.index = src.index; + dest.identifier = src.identifier; + setBytes32Array(dest.criteriaProof, src.criteriaProof); + } + + function setOrder(Order storage dest, Order memory src) internal { + setOrderParameters(dest.parameters, src.parameters); + dest.signature = src.signature; + } + + function setOrders(Order[] storage dest, Order[] memory src) internal { + delete _tempOrder; + while (dest.length != 0) { + dest.pop(); + } + for (uint256 i = 0; i < src.length; ++i) { + setOrder(_tempOrder, src[i]); + dest.push(_tempOrder); + } + delete _tempOrder; + } + + function setAdvancedOrder( + AdvancedOrder storage dest, + AdvancedOrder memory src + ) internal { + setOrderParameters(dest.parameters, src.parameters); + dest.numerator = src.numerator; + dest.denominator = src.denominator; + dest.signature = src.signature; + dest.extraData = src.extraData; + } + + function setAdvancedOrders( + AdvancedOrder[] storage dest, + AdvancedOrder[] memory src + ) internal { + // todo: delete might not work with nested non-empty arrays + delete _tempAdvancedOrder; + while (dest.length != 0) { + dest.pop(); + } + for (uint256 i = 0; i < src.length; ++i) { + setAdvancedOrder(_tempAdvancedOrder, src[i]); + dest.push(_tempAdvancedOrder); + } + delete _tempAdvancedOrder; + } + + function setOrderParameters( + OrderParameters storage dest, + OrderParameters memory src + ) internal { + dest.offerer = src.offerer; + dest.zone = src.zone; + setOfferItems(dest.offer, src.offer); + setConsiderationItems(dest.consideration, src.consideration); + dest.orderType = src.orderType; + dest.startTime = src.startTime; + dest.endTime = src.endTime; + dest.zoneHash = src.zoneHash; + dest.salt = src.salt; + dest.conduitKey = src.conduitKey; + dest.totalOriginalConsiderationItems = src + .totalOriginalConsiderationItems; + } + + function setOfferItems(OfferItem[] storage dest, OfferItem[] memory src) + internal + { + while (dest.length != 0) { + dest.pop(); + } + for (uint256 i = 0; i < src.length; ++i) { + dest.push(src[i]); + } + } + + function setConsiderationItems( + ConsiderationItem[] storage dest, + ConsiderationItem[] memory src + ) internal { + while (dest.length != 0) { + dest.pop(); + } + for (uint256 i = 0; i < src.length; ++i) { + dest.push(src[i]); + } + } + + function setFulfillment(Fulfillment storage dest, Fulfillment memory src) + internal + { + setFulfillmentComponents(dest.offerComponents, src.offerComponents); + setFulfillmentComponents( + dest.considerationComponents, + src.considerationComponents + ); + } + + function setFulfillments( + Fulfillment[] storage dest, + Fulfillment[] memory src + ) internal { + while (dest.length != 0) { + dest.pop(); + } + for (uint256 i = 0; i < src.length; ++i) { + dest.push(src[i]); + } + } + + function setFulfillmentComponents( + FulfillmentComponent[] storage dest, + FulfillmentComponent[] memory src + ) internal { + while (dest.length != 0) { + dest.pop(); + } + for (uint256 i = 0; i < src.length; ++i) { + dest.push(src[i]); + } + } + + function pushFulFillmentComponents( + FulfillmentComponent[][] storage dest, + FulfillmentComponent[] memory src + ) internal { + setFulfillmentComponents(_tempFulfillmentComponents, src); + dest.push(_tempFulfillmentComponents); + } + + function setFulfillmentComponentsArray( + FulfillmentComponent[][] storage dest, + FulfillmentComponent[][] memory src + ) internal { + while (dest.length != 0) { + dest.pop(); + } + for (uint256 i = 0; i < src.length; ++i) { + pushFulFillmentComponents(dest, src[i]); + } + } + + function toConsiderationItems( + OfferItem[] memory _offerItems, + address payable receiver + ) internal pure returns (ConsiderationItem[] memory) { + ConsiderationItem[] memory considerationItems = new ConsiderationItem[]( + _offerItems.length + ); + for (uint256 i = 0; i < _offerItems.length; ++i) { + considerationItems[i] = ConsiderationItem( + _offerItems[i].itemType, + _offerItems[i].token, + _offerItems[i].identifierOrCriteria, + _offerItems[i].startAmount, + _offerItems[i].endAmount, + receiver + ); + } + return considerationItems; + } + + function toOfferItems(ConsiderationItem[] memory _considerationItems) + internal + pure + returns (OfferItem[] memory) + { + OfferItem[] memory _offerItems = new OfferItem[]( + _considerationItems.length + ); + for (uint256 i = 0; i < _offerItems.length; i++) { + _offerItems[i] = OfferItem( + _considerationItems[i].itemType, + _considerationItems[i].token, + _considerationItems[i].identifierOrCriteria, + _considerationItems[i].startAmount, + _considerationItems[i].endAmount + ); + } + return _offerItems; + } + + function createMirrorOrderParameters( + OrderParameters memory orderParameters, + address payable offerer, + address zone, + bytes32 conduitKey + ) public pure returns (OrderParameters memory) { + OfferItem[] memory _offerItems = toOfferItems( + orderParameters.consideration + ); + ConsiderationItem[] memory _considerationItems = toConsiderationItems( + orderParameters.offer, + offerer + ); + + OrderParameters memory _mirrorOrderParameters = OrderParameters( + offerer, + zone, + _offerItems, + _considerationItems, + orderParameters.orderType, + orderParameters.startTime, + orderParameters.endTime, + orderParameters.zoneHash, + orderParameters.salt, + conduitKey, + _considerationItems.length + ); + return _mirrorOrderParameters; + } +} diff --git a/test/foundry/utils/TestTokenMinter.sol b/test/foundry/utils/TestTokenMinter.sol new file mode 100644 index 000000000..623d63aa1 --- /dev/null +++ b/test/foundry/utils/TestTokenMinter.sol @@ -0,0 +1,274 @@ +// SPDX-Identifier: MIT +pragma solidity >=0.8.7; + +import { TestERC1155 } from "../../../contracts/test/TestERC1155.sol"; +import { TestERC20 } from "../../../contracts/test/TestERC20.sol"; +import { TestERC721 } from "../../../contracts/test/TestERC721.sol"; +import { ERC721Recipient } from "./ERC721Recipient.sol"; +import { ERC1155Recipient } from "./ERC1155Recipient.sol"; +import { ItemType } from "../../../contracts/lib/ConsiderationEnums.sol"; +import { BaseConsiderationTest } from "./BaseConsiderationTest.sol"; +import { ERC721 } from "../token/ERC721.sol"; + +contract PreapprovedERC721 is ERC721 { + mapping(address => bool) public preapprovals; + + constructor(address[] memory preapproved) ERC721("", "") { + for (uint256 i = 0; i < preapproved.length; i++) { + preapprovals[preapproved[i]] = true; + } + } + + function mint(address to, uint256 amount) external returns (bool) { + _mint(to, amount); + return true; + } + + function isApprovedForAll(address owner, address operator) + public + view + override + returns (bool) + { + return + preapprovals[operator] || super.isApprovedForAll(owner, operator); + } + + function tokenURI(uint256) public pure override returns (string memory) { + return ""; + } +} + +contract TestTokenMinter is + BaseConsiderationTest, + ERC721Recipient, + ERC1155Recipient +{ + uint256 constant MAX_INT = ~uint256(0); + + uint256 internal alicePk = 0xa11ce; + uint256 internal bobPk = 0xb0b; + uint256 internal calPk = 0xca1; + address payable internal alice = payable(vm.addr(alicePk)); + address payable internal bob = payable(vm.addr(bobPk)); + address payable internal cal = payable(vm.addr(calPk)); + + TestERC20 internal token1; + TestERC20 internal token2; + TestERC20 internal token3; + + TestERC721 internal test721_1; + TestERC721 internal test721_2; + TestERC721 internal test721_3; + PreapprovedERC721 internal preapproved721; + + TestERC1155 internal test1155_1; + TestERC1155 internal test1155_2; + TestERC1155 internal test1155_3; + + TestERC20[] erc20s; + TestERC721[] erc721s; + TestERC1155[] erc1155s; + + address[] preapprovals; + + modifier only1155Receiver(address recipient) { + vm.assume(recipient != address(0)); + if (recipient.code.length > 0) { + try + ERC1155Recipient(recipient).onERC1155Received( + address(1), + address(1), + 1, + 1, + "" + ) + returns (bytes4 response) { + vm.assume(response == onERC1155Received.selector); + } catch (bytes memory reason) { + vm.assume(false); + } + } + _; + } + + function setUp() public virtual override { + super.setUp(); + + preapprovals = [ + address(consideration), + address(referenceConsideration), + address(conduit), + address(referenceConduit) + ]; + + vm.label(alice, "alice"); + vm.label(bob, "bob"); + vm.label(cal, "cal"); + + _deployTestTokenContracts(); + erc20s = [token1, token2, token3]; + erc721s = [test721_1, test721_2, test721_3]; + erc1155s = [test1155_1, test1155_2, test1155_3]; + + // allocate funds and tokens to test addresses + allocateTokensAndApprovals(address(this), uint128(MAX_INT)); + allocateTokensAndApprovals(alice, uint128(MAX_INT)); + allocateTokensAndApprovals(bob, uint128(MAX_INT)); + allocateTokensAndApprovals(cal, uint128(MAX_INT)); + } + + function mintErc721TokenTo(address to, uint256 id) internal { + mintErc721TokenTo(to, test721_1, id); + } + + function mintErc721TokenTo( + address to, + TestERC721 token, + uint256 id + ) internal { + token.mint(to, id); + } + + function mintTokensTo( + address to, + ItemType itemType, + uint256 amount + ) internal { + mintTokensTo(to, itemType, 1, amount); + } + + function mintTokensTo( + address to, + ItemType itemType, + uint256 id, + uint256 amount + ) internal { + if (itemType == ItemType.NATIVE) { + vm.deal(to, amount); + } else if (itemType == ItemType.ERC20) { + mintErc20TokensTo(to, amount); + } else if (itemType == ItemType.ERC1155) { + mintErc1155TokensTo(to, id, amount); + } else { + mintErc721TokenTo(to, id); + } + } + + function mintTokensTo( + address to, + ItemType itemType, + address token, + uint256 id, + uint256 amount + ) internal { + if (itemType == ItemType.NATIVE) { + vm.deal(to, amount); + } else if (itemType == ItemType.ERC20) { + mintErc20TokensTo(to, TestERC20(token), amount); + } else if (itemType == ItemType.ERC1155) { + mintErc1155TokensTo(to, TestERC1155(token), id, amount); + } else { + mintErc721TokenTo(to, TestERC721(token), id); + } + } + + function mintErc1155TokensTo( + address to, + uint256 id, + uint256 amount + ) internal { + mintErc1155TokensTo(to, test1155_1, id, amount); + } + + function mintErc1155TokensTo( + address to, + TestERC1155 token, + uint256 id, + uint256 amount + ) internal { + token.mint(to, id, amount); + } + + function mintErc20TokensTo(address to, uint256 amount) internal { + mintErc20TokensTo(to, token1, amount); + } + + function mintErc20TokensTo( + address to, + TestERC20 token, + uint256 amount + ) internal { + token.mint(to, amount); + } + + /** + @dev deploy test token contracts + */ + function _deployTestTokenContracts() internal { + token1 = new TestERC20(); + token2 = new TestERC20(); + token3 = new TestERC20(); + test721_1 = new TestERC721(); + test721_2 = new TestERC721(); + test721_3 = new TestERC721(); + test1155_1 = new TestERC1155(); + test1155_2 = new TestERC1155(); + test1155_3 = new TestERC1155(); + preapproved721 = new PreapprovedERC721(preapprovals); + + vm.label(address(token1), "token1"); + vm.label(address(test721_1), "test721_1"); + vm.label(address(test1155_1), "test1155_1"); + vm.label(address(preapproved721), "preapproved721"); + + emit log("Deployed test token contracts"); + } + + /** + @dev allocate amount of each token, 1 of each 721, and 1, 5, and 10 of respective 1155s + */ + function allocateTokensAndApprovals(address _to, uint128 _amount) internal { + vm.deal(_to, _amount); + for (uint256 i = 0; i < erc20s.length; ++i) { + erc20s[i].mint(_to, _amount); + } + emit log_named_address("Allocated tokens to", _to); + _setApprovals(_to); + } + + function _setApprovals(address _owner) internal virtual { + vm.startPrank(_owner); + for (uint256 i = 0; i < erc20s.length; ++i) { + erc20s[i].approve(address(consideration), MAX_INT); + erc20s[i].approve(address(referenceConsideration), MAX_INT); + erc20s[i].approve(address(conduit), MAX_INT); + erc20s[i].approve(address(referenceConduit), MAX_INT); + } + for (uint256 i = 0; i < erc721s.length; ++i) { + erc721s[i].setApprovalForAll(address(consideration), true); + erc721s[i].setApprovalForAll(address(referenceConsideration), true); + erc721s[i].setApprovalForAll(address(conduit), true); + erc721s[i].setApprovalForAll(address(referenceConduit), true); + } + for (uint256 i = 0; i < erc1155s.length; ++i) { + erc1155s[i].setApprovalForAll(address(consideration), true); + erc1155s[i].setApprovalForAll( + address(referenceConsideration), + true + ); + erc1155s[i].setApprovalForAll(address(conduit), true); + erc1155s[i].setApprovalForAll(address(referenceConduit), true); + } + + vm.stopPrank(); + emit log_named_address( + "Owner proxy approved for all tokens from", + _owner + ); + emit log_named_address( + "Consideration approved for all tokens from", + _owner + ); + } +} diff --git a/test/foundry/utils/reentrancy/ReentrantEnums.sol b/test/foundry/utils/reentrancy/ReentrantEnums.sol index 7c347ef3e..e3b03d725 100644 --- a/test/foundry/utils/reentrancy/ReentrantEnums.sol +++ b/test/foundry/utils/reentrancy/ReentrantEnums.sol @@ -1,5 +1,5 @@ //SPDX-License-Identifier: Unlicense -pragma solidity 0.8.13; +pragma solidity >=0.8.13; /** * @dev Enum of functions that set the reentrancy guard @@ -27,5 +27,5 @@ enum ReentryPoint { MatchAdvancedOrders, Cancel, Validate, - IncrementNonce + IncrementCounter } diff --git a/test/foundry/utils/reentrancy/ReentrantStructs.sol b/test/foundry/utils/reentrancy/ReentrantStructs.sol index 4488e6c51..0e089b552 100644 --- a/test/foundry/utils/reentrancy/ReentrantStructs.sol +++ b/test/foundry/utils/reentrancy/ReentrantStructs.sol @@ -1,5 +1,5 @@ //SPDX-License-Identifier: Unlicense -pragma solidity 0.8.13; +pragma solidity >=0.8.13; import { BasicOrderParameters, OfferItem, ConsiderationItem, OrderParameters, OrderComponents, Fulfillment, FulfillmentComponent, Execution, Order, AdvancedOrder, OrderStatus, CriteriaResolver } from "../../../../contracts/lib/ConsiderationStructs.sol"; struct FulfillBasicOrderParameters { diff --git a/test/index.js b/test/index.js index d7463e5f5..fabf05103 100644 --- a/test/index.js +++ b/test/index.js @@ -2,14 +2,15 @@ const { expect } = require("chai"); const { constants, - utils: { parseEther, keccak256, toUtf8Bytes, recoverAddress }, - Contract, + utils: { parseEther, keccak256, toUtf8Bytes }, } = require("ethers"); -const { ethers } = require("hardhat"); -const { faucet, whileImpersonating } = require("./utils/impersonate"); -const { deployContract } = require("./utils/contracts"); +const { ethers, network } = require("hardhat"); +const { + faucet, + whileImpersonating, + getWalletWithEther, +} = require("./utils/impersonate"); const { merkleTree } = require("./utils/criteria"); -const deployConstants = require("../constants/constants"); const { randomHex, random128, @@ -17,47 +18,45 @@ const { toKey, convertSignatureToEIP2098, getBasicOrderParameters, - getOfferOrConsiderationItem, getItemETH, toBN, randomBN, + toFulfillment, + toFulfillmentComponents, + getBasicOrderExecutions, + buildResolver, + buildOrderStatus, + defaultBuyNowMirrorFulfillment, + defaultAcceptOfferMirrorFulfillment, } = require("./utils/encoding"); -const { orderType } = require("../eip-712-types/order"); const { randomInt } = require("crypto"); -const { getCreate2Address } = require("ethers/lib/utils"); -const { tokensFixture } = require("./utils/fixtures"); +const { + fixtureERC20, + fixtureERC721, + fixtureERC1155, + seaportFixture, +} = require("./utils/fixtures"); +const { deployContract } = require("./utils/contracts"); -const VERSION = !process.env.REFERENCE ? "1" : "rc.1"; +const VERSION = !process.env.REFERENCE ? "1.1" : "rc.1.1"; const minRandom = (min) => randomBN(10).add(min); -const buildOrderStatus = (...arr) => { - const values = arr.map((v) => (typeof v === "number" ? toBN(v) : v)); - return ["isValidated", "isCancelled", "totalFilled", "totalSize"].reduce( - (obj, key, i) => ({ - ...obj, - [key]: values[i], - [i]: values[i], - }), - {} - ); -}; +const getCustomRevertSelector = (customErrorString) => + ethers.utils + .keccak256(ethers.utils.toUtf8Bytes(customErrorString)) + .slice(0, 10); describe(`Consideration (version: ${VERSION}) — initial test suite`, function () { const provider = ethers.provider; - let chainId; let zone; let marketplaceContract; let testERC20; let testERC721; let testERC1155; let testERC1155Two; - let tokenByType; let owner; - let domainData; let withBalanceChecks; - let simulateMatchOrders; - let simulateAdvancedMatchOrders; let EIP1271WalletFactory; let reenterer; let stubZone; @@ -66,1756 +65,96 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function let conduitOne; let conduitKeyOne; let directMarketplaceContract; - let conduitCodeHash; - - const resetTokens = async () => { - ({ testERC20, testERC721, testERC1155, testERC1155Two, tokenByType } = - await tokensFixture(owner)); - }; - - const buildResolver = ( - orderIndex, - side, - index, - identifier, - criteriaProof - ) => ({ - orderIndex, - side, - index, - identifier, - criteriaProof, - }); - - const getBasicOrderExecutions = (order, fulfiller, fulfillerConduitKey) => { - const { offerer, conduitKey, offer, consideration } = order.parameters; - const offerItem = offer[0]; - const considerationItem = consideration[0]; - const executions = [ - { - item: { - ...offerItem, - amount: offerItem.endAmount, - recipient: fulfiller, - }, - offerer: offerer, - conduitKey: conduitKey, - }, - { - item: { - ...considerationItem, - amount: considerationItem.endAmount, - }, - offerer: fulfiller, - conduitKey: fulfillerConduitKey, - }, - ]; - if (consideration.length > 1) { - for (const additionalRecipient of consideration.slice(1)) { - const execution = { - item: { - ...additionalRecipient, - amount: additionalRecipient.endAmount, - }, - offerer: fulfiller, - conduitKey: fulfillerConduitKey, - }; - if (additionalRecipient.itemType === offerItem.itemType) { - execution.offerer = offerer; - execution.conduitKey = conduitKey; - executions[0].item.amount = executions[0].item.amount.sub( - execution.item.amount - ); - } - executions.push(execution); - } - } - return executions; - }; - - const toFulfillmentComponents = (arr) => - arr.map(([orderIndex, itemIndex]) => ({ orderIndex, itemIndex })); - - const toFulfillment = (offerArr, considerationsArr) => ({ - offerComponents: toFulfillmentComponents(offerArr), - considerationComponents: toFulfillmentComponents(considerationsArr), - }); - - const set721ApprovalForAll = async ( - signer, - spender, - approved = true, - contract = testERC721 - ) => - expect(contract.connect(signer).setApprovalForAll(spender, approved)) - .to.emit(contract, "ApprovalForAll") - .withArgs(signer.address, spender, approved); - - const set1155ApprovalForAll = async ( - signer, - spender, - approved = true, - contract = testERC1155 - ) => - expect(contract.connect(signer).setApprovalForAll(spender, approved)) - .to.emit(contract, "ApprovalForAll") - .withArgs(signer.address, spender, approved); - - const mintAndApproveERC20 = async (signer, spender, tokenAmount) => { - // Offerer mints ERC20 - await testERC20.mint(signer.address, tokenAmount); - - // Offerer approves marketplace contract to tokens - await expect(testERC20.connect(signer).approve(spender, tokenAmount)) - .to.emit(testERC20, "Approval") - .withArgs(signer.address, spender, tokenAmount); - }; - - const mint721 = async (signer, id) => { - const nftId = id ? toBN(id) : randomBN(); - await testERC721.mint(signer.address, nftId); - return nftId; - }; - - const mint721s = async (signer, count) => { - const arr = []; - for (let i = 0; i < count; i++) arr.push(await mint721(signer)); - return arr; - }; - - const mintAndApprove721 = async (signer, spender, id) => { - await set721ApprovalForAll(signer, spender, true); - return mint721(signer, id); - }; - - const getTransferSender = (account, conduitKey) => { - if (!conduitKey || conduitKey === constants.HashZero) { - return account; - } - return getCreate2Address( - conduitController.address, - conduitKey, - conduitCodeHash - ); - }; - - const mint1155 = async ( - signer, - multiplier = 1, - token = testERC1155, - id = null, - amt = null - ) => { - const nftId = id ? toBN(id) : randomBN(); - const amount = amt ? toBN(amt) : toBN(randomBN(4)); - await token.mint(signer.address, nftId, amount.mul(multiplier)); - return { nftId, amount }; - }; - - const mintAndApprove1155 = async ( - signer, - spender, - multiplier = 1, - id = null, - amt = null - ) => { - const { nftId, amount } = await mint1155( - signer, - multiplier, - testERC1155, - id, - amt - ); - await set1155ApprovalForAll(signer, spender, true); - return { nftId, amount }; - }; - - const getTestItem721 = ( - identifierOrCriteria, - startAmount = 1, - endAmount = 1, - recipient, - token = testERC721.address - ) => - getOfferOrConsiderationItem( - 2, - token, - identifierOrCriteria, - startAmount, - endAmount, - recipient - ); - - const getTestItem721WithCriteria = ( - identifierOrCriteria, - startAmount = 1, - endAmount = 1, - recipient - ) => - getOfferOrConsiderationItem( - 4, - testERC721.address, - identifierOrCriteria, - startAmount, - endAmount, - recipient - ); - - const getTestItem1155WithCriteria = ( - identifierOrCriteria, - startAmount = 1, - endAmount = 1, - recipient - ) => - getOfferOrConsiderationItem( - 5, - testERC1155.address, - identifierOrCriteria, - startAmount, - endAmount, - recipient - ); - - const getTestItem20 = ( - startAmount = 50, - endAmount = 50, - recipient, - token = testERC20.address - ) => - getOfferOrConsiderationItem(1, token, 0, startAmount, endAmount, recipient); - - const getTestItem1155 = ( - identifierOrCriteria, - startAmount, - endAmount, - token = testERC1155.address, - recipient - ) => - getOfferOrConsiderationItem( - 3, - token, - identifierOrCriteria, - startAmount, - endAmount, - recipient - ); - - const getAndVerifyOrderHash = async (orderComponents) => { - const orderHash = await marketplaceContract.getOrderHash(orderComponents); - - const offerItemTypeString = - "OfferItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount)"; - const considerationItemTypeString = - "ConsiderationItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount,address recipient)"; - const orderComponentsPartialTypeString = - "OrderComponents(address offerer,address zone,OfferItem[] offer,ConsiderationItem[] consideration,uint8 orderType,uint256 startTime,uint256 endTime,bytes32 zoneHash,uint256 salt,bytes32 conduitKey,uint256 nonce)"; - const orderTypeString = `${orderComponentsPartialTypeString}${considerationItemTypeString}${offerItemTypeString}`; - - const offerItemTypeHash = keccak256(toUtf8Bytes(offerItemTypeString)); - const considerationItemTypeHash = keccak256( - toUtf8Bytes(considerationItemTypeString) - ); - const orderTypeHash = keccak256(toUtf8Bytes(orderTypeString)); - - const offerHash = keccak256( - "0x" + - orderComponents.offer - .map((offerItem) => { - return ethers.utils - .keccak256( - "0x" + - [ - offerItemTypeHash.slice(2), - offerItem.itemType.toString().padStart(64, "0"), - offerItem.token.slice(2).padStart(64, "0"), - toBN(offerItem.identifierOrCriteria) - .toHexString() - .slice(2) - .padStart(64, "0"), - toBN(offerItem.startAmount) - .toHexString() - .slice(2) - .padStart(64, "0"), - toBN(offerItem.endAmount) - .toHexString() - .slice(2) - .padStart(64, "0"), - ].join("") - ) - .slice(2); - }) - .join("") - ); - - const considerationHash = keccak256( - "0x" + - orderComponents.consideration - .map((considerationItem) => { - return ethers.utils - .keccak256( - "0x" + - [ - considerationItemTypeHash.slice(2), - considerationItem.itemType.toString().padStart(64, "0"), - considerationItem.token.slice(2).padStart(64, "0"), - toBN(considerationItem.identifierOrCriteria) - .toHexString() - .slice(2) - .padStart(64, "0"), - toBN(considerationItem.startAmount) - .toHexString() - .slice(2) - .padStart(64, "0"), - toBN(considerationItem.endAmount) - .toHexString() - .slice(2) - .padStart(64, "0"), - considerationItem.recipient.slice(2).padStart(64, "0"), - ].join("") - ) - .slice(2); - }) - .join("") - ); - - const derivedOrderHash = keccak256( - "0x" + - [ - orderTypeHash.slice(2), - orderComponents.offerer.slice(2).padStart(64, "0"), - orderComponents.zone.slice(2).padStart(64, "0"), - offerHash.slice(2), - considerationHash.slice(2), - orderComponents.orderType.toString().padStart(64, "0"), - toBN(orderComponents.startTime) - .toHexString() - .slice(2) - .padStart(64, "0"), - toBN(orderComponents.endTime) - .toHexString() - .slice(2) - .padStart(64, "0"), - orderComponents.zoneHash.slice(2), - orderComponents.salt.slice(2).padStart(64, "0"), - orderComponents.conduitKey.slice(2).padStart(64, "0"), - toBN(orderComponents.nonce).toHexString().slice(2).padStart(64, "0"), - ].join("") - ); - expect(orderHash).to.equal(derivedOrderHash); - - return orderHash; - }; - - // Returns signature - const signOrder = async (orderComponents, signer) => { - const signature = await signer._signTypedData( - domainData, - orderType, - orderComponents - ); - - const orderHash = await getAndVerifyOrderHash(orderComponents); - - const { domainSeparator } = await marketplaceContract.information(); - const digest = keccak256( - `0x1901${domainSeparator.slice(2)}${orderHash.slice(2)}` - ); - const recoveredAddress = recoverAddress(digest, signature); - - expect(recoveredAddress).to.equal(signer.address); - - return signature; - }; - - const createOrder = async ( - offerer, - zone, - offer, - consideration, - orderType, - criteriaResolvers, - timeFlag, - signer, - zoneHash = constants.HashZero, - conduitKey = constants.HashZero, - extraCheap = false - ) => { - const nonce = await marketplaceContract.getNonce(offerer.address); - - const salt = !extraCheap ? randomHex() : constants.HashZero; - const startTime = - timeFlag !== "NOT_STARTED" ? 0 : toBN("0xee00000000000000000000000000"); - const endTime = - timeFlag !== "EXPIRED" ? toBN("0xff00000000000000000000000000") : 1; - - const orderParameters = { - offerer: offerer.address, - zone: !extraCheap ? zone.address : constants.AddressZero, - offer, - consideration, - totalOriginalConsiderationItems: consideration.length, - orderType, - zoneHash, - salt, - conduitKey, - startTime, - endTime, - }; - - const orderComponents = { - ...orderParameters, - nonce, - }; - - const orderHash = await getAndVerifyOrderHash(orderComponents); - - const { isValidated, isCancelled, totalFilled, totalSize } = - await marketplaceContract.getOrderStatus(orderHash); - - expect(isCancelled).to.equal(false); - - const orderStatus = { - isValidated, - isCancelled, - totalFilled, - totalSize, - }; - - const flatSig = await signOrder(orderComponents, signer || offerer); - - const order = { - parameters: orderParameters, - signature: !extraCheap ? flatSig : convertSignatureToEIP2098(flatSig), - numerator: 1, // only used for advanced orders - denominator: 1, // only used for advanced orders - extraData: "0x", // only used for advanced orders - }; - - // How much ether (at most) needs to be supplied when fulfilling the order - const value = offer - .map((x) => - x.itemType === 0 - ? x.endAmount.gt(x.startAmount) - ? x.endAmount - : x.startAmount - : toBN(0) - ) - .reduce((a, b) => a.add(b), toBN(0)) - .add( - consideration - .map((x) => - x.itemType === 0 - ? x.endAmount.gt(x.startAmount) - ? x.endAmount - : x.startAmount - : toBN(0) - ) - .reduce((a, b) => a.add(b), toBN(0)) - ); - - return { - order, - orderHash, - value, - orderStatus, - orderComponents, - }; - }; - - const createMirrorBuyNowOrder = async ( - offerer, - zone, - order, - conduitKey = constants.HashZero - ) => { - const nonce = await marketplaceContract.getNonce(offerer.address); - const salt = randomHex(); - const startTime = order.parameters.startTime; - const endTime = order.parameters.endTime; - - const compressedOfferItems = []; - for (const { - itemType, - token, - identifierOrCriteria, - startAmount, - endAmount, - } of order.parameters.offer) { - if ( - !compressedOfferItems - .map((x) => `${x.itemType}+${x.token}+${x.identifierOrCriteria}`) - .includes(`${itemType}+${token}+${identifierOrCriteria}`) - ) { - compressedOfferItems.push({ - itemType, - token, - identifierOrCriteria, - startAmount: startAmount.eq(endAmount) - ? startAmount - : startAmount.sub(1), - endAmount: startAmount.eq(endAmount) ? endAmount : endAmount.sub(1), - }); - } else { - const index = compressedOfferItems - .map((x) => `${x.itemType}+${x.token}+${x.identifierOrCriteria}`) - .indexOf(`${itemType}+${token}+${identifierOrCriteria}`); - - compressedOfferItems[index].startAmount = compressedOfferItems[ - index - ].startAmount.add( - startAmount.eq(endAmount) ? startAmount : startAmount.sub(1) - ); - compressedOfferItems[index].endAmount = compressedOfferItems[ - index - ].endAmount.add( - startAmount.eq(endAmount) ? endAmount : endAmount.sub(1) - ); - } - } - - const compressedConsiderationItems = []; - for (const { - itemType, - token, - identifierOrCriteria, - startAmount, - endAmount, - recipient, - } of order.parameters.consideration) { - if ( - !compressedConsiderationItems - .map((x) => `${x.itemType}+${x.token}+${x.identifierOrCriteria}`) - .includes(`${itemType}+${token}+${identifierOrCriteria}`) - ) { - compressedConsiderationItems.push({ - itemType, - token, - identifierOrCriteria, - startAmount: startAmount.eq(endAmount) - ? startAmount - : startAmount.add(1), - endAmount: startAmount.eq(endAmount) ? endAmount : endAmount.add(1), - recipient, - }); - } else { - const index = compressedConsiderationItems - .map((x) => `${x.itemType}+${x.token}+${x.identifierOrCriteria}`) - .indexOf(`${itemType}+${token}+${identifierOrCriteria}`); - - compressedConsiderationItems[index].startAmount = - compressedConsiderationItems[index].startAmount.add( - startAmount.eq(endAmount) ? startAmount : startAmount.add(1) - ); - compressedConsiderationItems[index].endAmount = - compressedConsiderationItems[index].endAmount.add( - startAmount.eq(endAmount) ? endAmount : endAmount.add(1) - ); - } - } - - const orderParameters = { - offerer: offerer.address, - zone: zone.address, - offer: compressedConsiderationItems.map((x) => ({ ...x })), - consideration: compressedOfferItems.map((x) => ({ - ...x, - recipient: offerer.address, - })), - totalOriginalConsiderationItems: compressedOfferItems.length, - orderType: order.parameters.orderType, // FULL_OPEN - zoneHash: "0x".padEnd(66, "0"), - salt, - conduitKey, - startTime, - endTime, - }; - - const orderComponents = { - ...orderParameters, - nonce, - }; - - const flatSig = await signOrder(orderComponents, offerer); - - const mirrorOrderHash = await getAndVerifyOrderHash(orderComponents); - - const mirrorOrder = { - parameters: orderParameters, - signature: flatSig, - numerator: order.numerator, // only used for advanced orders - denominator: order.denominator, // only used for advanced orders - extraData: "0x", // only used for advanced orders - }; - - // How much ether (at most) needs to be supplied when fulfilling the order - const mirrorValue = orderParameters.consideration - .map((x) => - x.itemType === 0 - ? x.endAmount.gt(x.startAmount) - ? x.endAmount - : x.startAmount - : toBN(0) - ) - .reduce((a, b) => a.add(b), toBN(0)); - - return { - mirrorOrder, - mirrorOrderHash, - mirrorValue, - }; + let mintAndApproveERC20; + let getTestItem20; + let set721ApprovalForAll; + let mint721; + let mint721s; + let mintAndApprove721; + let getTestItem721; + let getTestItem721WithCriteria; + let set1155ApprovalForAll; + let mint1155; + let mintAndApprove1155; + let getTestItem1155WithCriteria; + let getTestItem1155; + let deployNewConduit; + let createTransferWithApproval; + let createOrder; + let createMirrorBuyNowOrder; + let createMirrorAcceptOfferOrder; + let checkExpectedEvents; + + const simulateMatchOrders = async (orders, fulfillments, caller, value) => { + return marketplaceContract + .connect(caller) + .callStatic.matchOrders(orders, fulfillments, { + value, + }); }; - const createMirrorAcceptOfferOrder = async ( - offerer, - zone, - order, + const simulateAdvancedMatchOrders = async ( + orders, criteriaResolvers, - conduitKey = constants.HashZero + fulfillments, + caller, + value ) => { - const nonce = await marketplaceContract.getNonce(offerer.address); - const salt = randomHex(); - const startTime = order.parameters.startTime; - const endTime = order.parameters.endTime; - - const orderParameters = { - offerer: offerer.address, - zone: zone.address, - offer: order.parameters.consideration - .filter((x) => x.itemType !== 1) - .map((x) => ({ - itemType: x.itemType < 4 ? x.itemType : x.itemType - 2, - token: x.token, - identifierOrCriteria: - x.itemType < 4 - ? x.identifierOrCriteria - : criteriaResolvers[0].identifier, - startAmount: x.startAmount, - endAmount: x.endAmount, - })), - consideration: order.parameters.offer.map((x) => ({ - itemType: x.itemType < 4 ? x.itemType : x.itemType - 2, - token: x.token, - identifierOrCriteria: - x.itemType < 4 - ? x.identifierOrCriteria - : criteriaResolvers[0].identifier, - recipient: offerer.address, - startAmount: toBN(x.endAmount).sub( - order.parameters.consideration - .filter( - (i) => - i.itemType < 2 && - i.itemType === x.itemType && - i.token === x.token - ) - .map((i) => i.endAmount) - .reduce((a, b) => a.add(b), toBN(0)) - ), - endAmount: toBN(x.endAmount).sub( - order.parameters.consideration - .filter( - (i) => - i.itemType < 2 && - i.itemType === x.itemType && - i.token === x.token - ) - .map((i) => i.endAmount) - .reduce((a, b) => a.add(b), toBN(0)) - ), - })), - totalOriginalConsiderationItems: order.parameters.offer.length, - orderType: 0, // FULL_OPEN - zoneHash: constants.HashZero, - salt, - conduitKey, - startTime, - endTime, - }; - - const orderComponents = { - ...orderParameters, - nonce, - }; - - const flatSig = await signOrder(orderComponents, offerer); - - const mirrorOrderHash = await getAndVerifyOrderHash(orderComponents); - - const mirrorOrder = { - parameters: orderParameters, - signature: flatSig, - numerator: 1, // only used for advanced orders - denominator: 1, // only used for advanced orders - extraData: "0x", // only used for advanced orders - }; - - // How much ether (at most) needs to be supplied when fulfilling the order - const mirrorValue = orderParameters.consideration - .map((x) => - x.itemType === 0 - ? x.endAmount.gt(x.startAmount) - ? x.endAmount - : x.startAmount - : toBN(0) - ) - .reduce((a, b) => a.add(b), toBN(0)); - - return { - mirrorOrder, - mirrorOrderHash, - mirrorValue, - }; + return marketplaceContract + .connect(caller) + .callStatic.matchAdvancedOrders(orders, criteriaResolvers, fulfillments, { + value, + }); }; - const deployNewConduit = async (owner) => { - // Create a conduit key with a random salt - const tempConduitKey = owner.address + randomHex(12).slice(2); - - const { conduit: tempConduitAddress } = await conduitController.getConduit( - tempConduitKey - ); - - await whileImpersonating(owner.address, provider, async () => { - await conduitController - .connect(owner) - .createConduit(tempConduitKey, owner.address); + after(async () => { + await network.provider.request({ + method: "hardhat_reset", }); - - const tempConduit = conduitImplementation.attach(tempConduitAddress); - return tempConduit; - }; - - // Deploys a new contract based on itemType - const deployContracts = async (itemType) => { - let tempContract; - - switch (itemType) { - case 0: - break; - case 1: // ERC20 - tempContract = await deployContract("TestERC20", owner); - break; - case 2: // ERC721 - case 4: // ERC721_WITH_CRITERIA - tempContract = await deployContract("TestERC721", owner); - break; - case 3: // ERC1155 - case 5: // ERC1155_WITH_CRITERIA - tempContract = await deployContract("TestERC1155", owner); - break; - } - return tempContract; - }; - - const checkTransferEvent = async ( - tx, - item, - { offerer, conduitKey, target } - ) => { - const { - itemType, - token, - identifier: id1, - identifierOrCriteria: id2, - amount, - recipient, - } = item; - const identifier = id1 || id2; - const sender = getTransferSender(offerer, conduitKey); - if ([1, 2, 5].includes(itemType)) { - const contract = new Contract( - token, - (itemType === 1 ? testERC20 : testERC721).interface, - provider - ); - await expect(tx) - .to.emit(contract, "Transfer") - .withArgs(offerer, recipient, itemType === 1 ? amount : identifier); - } else if ([3, 4].includes(itemType)) { - const contract = new Contract(token, testERC1155.interface, provider); - const operator = sender !== offerer ? sender : target; - await expect(tx) - .to.emit(contract, "TransferSingle") - .withArgs(operator, offerer, recipient, identifier, amount); - } - }; - - // Creates a transfer object after minting a random amount - // of tokens, and setting receiver's token approval based on itemType - const createTransferWithApproval = async ( - contract, - receiver, - itemType, - approvalAddress, - from, - to - ) => { - let identifier = 0; - let amount; - let token = contract.address; - - switch (itemType) { - case 0: - break; - case 1: // ERC20 - amount = minRandom(100); - await contract.mint(receiver.address, amount); - - // Receiver approves contract to transfer tokens - await whileImpersonating(receiver.address, provider, async () => { - await expect( - contract.connect(receiver).approve(approvalAddress, amount) - ) - .to.emit(contract, "Approval") - .withArgs(receiver.address, approvalAddress, amount); - }); - break; - case 2: // ERC721 - case 4: // ERC721_WITH_CRITERIA - amount = 1; - identifier = randomBN(); - await contract.mint(receiver.address, identifier); - - // Receiver approves contract to transfer tokens - await set721ApprovalForAll(receiver, approvalAddress, true, contract); - break; - case 3: // ERC1155 - case 5: // ERC1155_WITH_CRITERIA - identifier = random128(); - amount = minRandom(1); - await contract.mint(receiver.address, identifier, amount); - - // Receiver approves contract to transfer tokens - await set1155ApprovalForAll(receiver, approvalAddress, true, contract); - break; - } - return { itemType, token, from, to, identifier, amount }; - }; - - const checkExpectedEvents = async ( - tx, - receipt, - orderGroups, - standardExecutions, - criteriaResolvers = [], - shouldSkipAmountComparison = false, - multiplier = 1 - ) => { - const { timestamp } = await provider.getBlock(receipt.blockHash); - - if (standardExecutions && standardExecutions.length) { - for (const standardExecution of standardExecutions) { - const { item, offerer, conduitKey } = standardExecution; - await checkTransferEvent(tx, item, { - offerer, - conduitKey, - target: receipt.to, - }); - } - - // TODO: sum up executions and compare to orders to ensure that all the - // items (or partially-filled items) are accounted for - } - - if (criteriaResolvers && criteriaResolvers.length) { - for (const { orderIndex, side, index, identifier } of criteriaResolvers) { - const itemType = - orderGroups[orderIndex].order.parameters[ - side === 0 ? "offer" : "consideration" - ][index].itemType; - if (itemType < 4) { - console.error("APPLYING CRITERIA TO NON-CRITERIA-BASED ITEM"); - process.exit(1); - } - - orderGroups[orderIndex].order.parameters[ - side === 0 ? "offer" : "consideration" - ][index].itemType = itemType - 2; - orderGroups[orderIndex].order.parameters[ - side === 0 ? "offer" : "consideration" - ][index].identifierOrCriteria = identifier; - } - } - - for (const { - order, - orderHash, - fulfiller, - fulfillerConduitKey, - } of orderGroups) { - const duration = toBN(order.parameters.endTime).sub( - order.parameters.startTime - ); - const elapsed = toBN(timestamp).sub(order.parameters.startTime); - const remaining = duration.sub(elapsed); - - const marketplaceContractEvents = receipt.events - .filter((x) => x.address === marketplaceContract.address) - .map((x) => ({ - eventName: x.event, - eventSignature: x.eventSignature, - orderHash: x.args.orderHash, - offerer: x.args.offerer, - zone: x.args.zone, - fulfiller: x.args.fulfiller, - offer: x.args.offer.map((y) => ({ - itemType: y.itemType, - token: y.token, - identifier: y.identifier, - amount: y.amount, - })), - consideration: x.args.consideration.map((y) => ({ - itemType: y.itemType, - token: y.token, - identifier: y.identifier, - amount: y.amount, - recipient: y.recipient, - })), - })) - .filter((x) => x.orderHash === orderHash); - - expect(marketplaceContractEvents.length).to.equal(1); - - const event = marketplaceContractEvents[0]; - - expect(event.eventName).to.equal("OrderFulfilled"); - expect(event.eventSignature).to.equal( - "OrderFulfilled(" + - "bytes32,address,address,address,(" + - "uint8,address,uint256,uint256)[],(" + - "uint8,address,uint256,uint256,address)[])" - ); - expect(event.orderHash).to.equal(orderHash); - expect(event.offerer).to.equal(order.parameters.offerer); - expect(event.zone).to.equal(order.parameters.zone); - expect(event.fulfiller).to.equal(fulfiller); - - const { offerer, conduitKey, consideration, offer } = order.parameters; - const compareEventItems = async ( - item, - orderItem, - isConsiderationItem - ) => { - expect(item.itemType).to.equal( - orderItem.itemType > 3 ? orderItem.itemType - 2 : orderItem.itemType - ); - expect(item.token).to.equal(orderItem.token); - expect(item.token).to.equal(tokenByType[item.itemType].address); - if (orderItem.itemType < 4) { - // no criteria-based - expect(item.identifier).to.equal(orderItem.identifierOrCriteria); - } else { - console.error("CRITERIA-BASED EVENT VALIDATION NOT MET"); - process.exit(1); - } - - if (order.parameters.orderType === 0) { - // FULL_OPEN (no partial fills) - if ( - orderItem.startAmount.toString() === orderItem.endAmount.toString() - ) { - expect(item.amount.toString()).to.equal( - orderItem.endAmount.toString() - ); - } else { - expect(item.amount.toString()).to.equal( - toBN(orderItem.startAmount) - .mul(remaining) - .add(toBN(orderItem.endAmount).mul(elapsed)) - .add(isConsiderationItem ? duration.sub(1) : 0) - .div(duration) - .toString() - ); - } - } else { - if ( - orderItem.startAmount.toString() === orderItem.endAmount.toString() - ) { - expect(item.amount.toString()).to.equal( - orderItem.endAmount - .mul(order.numerator) - .div(order.denominator) - .toString() - ); - } else { - console.error("SLIDING AMOUNT NOT IMPLEMENTED YET"); - process.exit(1); - } - } - }; - - if (!standardExecutions || !standardExecutions.length) { - for (const item of consideration) { - const { startAmount, endAmount } = item; - let amount; - if (order.parameters.orderType === 0) { - amount = startAmount.eq(endAmount) - ? endAmount - : startAmount - .mul(remaining) - .add(endAmount.mul(elapsed)) - .add(duration.sub(1)) - .div(duration); - } else { - amount = endAmount.mul(order.numerator).div(order.denominator); - } - amount = amount.mul(multiplier); - - await checkTransferEvent( - tx, - { ...item, amount }, - { - offerer: receipt.from, - conduitKey: fulfillerConduitKey, - target: receipt.to, - } - ); - } - - for (const item of offer) { - const { startAmount, endAmount } = item; - let amount; - if (order.parameters.orderType === 0) { - amount = startAmount.eq(endAmount) - ? endAmount - : startAmount - .mul(remaining) - .add(endAmount.mul(elapsed)) - .div(duration); - } else { - amount = endAmount.mul(order.numerator).div(order.denominator); - } - amount = amount.mul(multiplier); - - await checkTransferEvent( - tx, - { ...item, amount, recipient: receipt.from }, - { - offerer, - conduitKey, - target: receipt.to, - } - ); - } - } - - expect(event.offer.length).to.equal(order.parameters.offer.length); - for (const [index, offer] of Object.entries(event.offer)) { - const offerItem = order.parameters.offer[index]; - await compareEventItems(offer, offerItem, false); - - const tokenEvents = receipt.events.filter( - (x) => x.address === offerItem.token - ); - - if (offer.itemType === 1) { - // ERC20 - // search for transfer - const transferLogs = tokenEvents - .map((x) => testERC20.interface.parseLog(x)) - .filter( - (x) => - x.signature === "Transfer(address,address,uint256)" && - x.args.from === event.offerer && - (fulfiller !== constants.AddressZero - ? x.args.to === fulfiller - : true) - ); - - expect(transferLogs.length).to.be.above(0); - for (const transferLog of transferLogs) { - // TODO: check each transferred amount - } - } else if (offer.itemType === 2) { - // ERC721 - // search for transfer - const transferLogs = tokenEvents - .map((x) => testERC721.interface.parseLog(x)) - .filter( - (x) => - x.signature === "Transfer(address,address,uint256)" && - x.args.from === event.offerer && - (fulfiller !== constants.AddressZero - ? x.args.to === fulfiller - : true) - ); - - expect(transferLogs.length).to.equal(1); - const transferLog = transferLogs[0]; - expect(transferLog.args.id.toString()).to.equal( - offer.identifier.toString() - ); - } else if (offer.itemType === 3) { - // search for transfer - const transferLogs = tokenEvents - .map((x) => testERC1155.interface.parseLog(x)) - .filter( - (x) => - (x.signature === - "TransferSingle(address,address,address,uint256,uint256)" && - x.args.from === event.offerer && - (fulfiller !== constants.AddressZero - ? x.args.to === fulfiller - : true)) || - (x.signature === - "TransferBatch(address,address,address,uint256[],uint256[])" && - x.args.from === event.offerer && - (fulfiller !== constants.AddressZero - ? x.args.to === fulfiller - : true)) - ); - - expect(transferLogs.length > 0).to.be.true; - - let found = false; - for (const transferLog of transferLogs) { - if ( - transferLog.signature === - "TransferSingle(address,address,address,uint256,uint256)" && - transferLog.args.id.toString() === offer.identifier.toString() && - (shouldSkipAmountComparison || - transferLog.args.amount.toString() === - offer.amount.mul(multiplier).toString()) - ) { - found = true; - break; - } - } - - expect(found).to.be.true; - } - } - - expect(event.consideration.length).to.equal( - order.parameters.consideration.length - ); - for (const [index, consideration] of Object.entries( - event.consideration - )) { - const considerationItem = order.parameters.consideration[index]; - await compareEventItems(consideration, considerationItem, true); - expect(consideration.recipient).to.equal(considerationItem.recipient); - - const tokenEvents = receipt.events.filter( - (x) => x.address === considerationItem.token - ); - - if (consideration.itemType === 1) { - // ERC20 - // search for transfer - const transferLogs = tokenEvents - .map((x) => testERC20.interface.parseLog(x)) - .filter( - (x) => - x.signature === "Transfer(address,address,uint256)" && - x.args.to === consideration.recipient - ); - - expect(transferLogs.length).to.be.above(0); - for (const transferLog of transferLogs) { - // TODO: check each transferred amount - } - } else if (consideration.itemType === 2) { - // ERC721 - // search for transfer - - const transferLogs = tokenEvents - .map((x) => testERC721.interface.parseLog(x)) - .filter( - (x) => - x.signature === "Transfer(address,address,uint256)" && - x.args.to === consideration.recipient - ); - - expect(transferLogs.length).to.equal(1); - const transferLog = transferLogs[0]; - expect(transferLog.args.id.toString()).to.equal( - consideration.identifier.toString() - ); - } else if (consideration.itemType === 3) { - // search for transfer - const transferLogs = tokenEvents - .map((x) => testERC1155.interface.parseLog(x)) - .filter( - (x) => - (x.signature === - "TransferSingle(address,address,address,uint256,uint256)" && - x.args.to === consideration.recipient) || - (x.signature === - "TransferBatch(address,address,address,uint256[],uint256[])" && - x.args.to === consideration.recipient) - ); - - expect(transferLogs.length > 0).to.be.true; - - let found = false; - for (const transferLog of transferLogs) { - if ( - transferLog.signature === - "TransferSingle(address,address,address,uint256,uint256)" && - transferLog.args.id.toString() === - consideration.identifier.toString() && - (shouldSkipAmountComparison || - transferLog.args.amount.toString() === - consideration.amount.mul(multiplier).toString()) - ) { - found = true; - break; - } - } - - expect(found).to.be.true; - } - } - } - }; - - const defaultBuyNowMirrorFulfillment = [ - [[[0, 0]], [[1, 0]]], - [[[1, 0]], [[0, 0]]], - [[[1, 0]], [[0, 1]]], - [[[1, 0]], [[0, 2]]], - ].map(([offerArr, considerationArr]) => - toFulfillment(offerArr, considerationArr) - ); - - const defaultAcceptOfferMirrorFulfillment = [ - [[[1, 0]], [[0, 0]]], - [[[0, 0]], [[1, 0]]], - [[[0, 0]], [[0, 1]]], - [[[0, 0]], [[0, 2]]], - ].map(([offerArr, considerationArr]) => - toFulfillment(offerArr, considerationArr) - ); + }); before(async () => { - const network = await provider.getNetwork(); - - chainId = network.chainId; - owner = new ethers.Wallet(randomHex(32), provider); await Promise.all( [owner].map((wallet) => faucet(wallet.address, provider)) ); - // Deploy keyless create2 deployer - await faucet(deployConstants.KEYLESS_CREATE2_DEPLOYER_ADDRESS, provider); - await provider.sendTransaction( - deployConstants.KEYLESS_CREATE2_DEPLOYMENT_TRANSACTION - ); - let deployedCode = await provider.getCode( - deployConstants.KEYLESS_CREATE2_ADDRESS - ); - expect(deployedCode).to.equal(deployConstants.KEYLESS_CREATE2_RUNTIME_CODE); - - let { gasLimit } = await provider.getBlock(); - - if (hre.__SOLIDITY_COVERAGE_RUNNING) { - gasLimit = ethers.BigNumber.from(300_000_000); - } - - // Deploy inefficient deployer through keyless - await owner.sendTransaction({ - to: deployConstants.KEYLESS_CREATE2_ADDRESS, - data: deployConstants.IMMUTABLE_CREATE2_FACTORY_CREATION_CODE, - gasLimit, - }); - deployedCode = await provider.getCode( - deployConstants.INEFFICIENT_IMMUTABLE_CREATE2_FACTORY_ADDRESS - ); - expect(ethers.utils.keccak256(deployedCode)).to.equal( - deployConstants.IMMUTABLE_CREATE2_FACTORY_RUNTIME_HASH - ); - - const inefficientFactory = await ethers.getContractAt( - "ImmutableCreate2FactoryInterface", - deployConstants.INEFFICIENT_IMMUTABLE_CREATE2_FACTORY_ADDRESS, - owner - ); - - // Deploy effecient deployer through inefficient deployer - await inefficientFactory - .connect(owner) - .safeCreate2( - deployConstants.IMMUTABLE_CREATE2_FACTORY_SALT, - deployConstants.IMMUTABLE_CREATE2_FACTORY_CREATION_CODE, - { - gasLimit, - } - ); - - deployedCode = await provider.getCode( - deployConstants.IMMUTABLE_CREATE2_FACTORY_ADDRESS - ); - expect(ethers.utils.keccak256(deployedCode)).to.equal( - deployConstants.IMMUTABLE_CREATE2_FACTORY_RUNTIME_HASH - ); - const create2Factory = await ethers.getContractAt( - "ImmutableCreate2FactoryInterface", - deployConstants.IMMUTABLE_CREATE2_FACTORY_ADDRESS, - owner - ); - - EIP1271WalletFactory = await ethers.getContractFactory("EIP1271Wallet"); - - reenterer = await deployContract("Reenterer", owner); - - if (process.env.REFERENCE) { - conduitImplementation = await ethers.getContractFactory( - "ReferenceConduit" - ); - conduitController = await deployContract("ConduitController", owner); - } else { - conduitImplementation = await ethers.getContractFactory("Conduit"); - - // Deploy conduit controller through efficient create2 factory - const conduitControllerFactory = await ethers.getContractFactory( - "ConduitController" - ); - - if (!hre.__SOLIDITY_COVERAGE_RUNNING) { - expect( - ethers.utils.keccak256(conduitControllerFactory.bytecode) - ).to.equal(deployConstants.CONDUIT_CONTROLLER_CREATION_HASH); - } - - const conduitControllerAddress = await create2Factory.findCreate2Address( - deployConstants.CONDUIT_CONTROLLER_CREATION_SALT, - conduitControllerFactory.bytecode - ); - - if (!hre.__SOLIDITY_COVERAGE_RUNNING) { - expect(conduitControllerAddress).to.equal( - deployConstants.CONDUIT_CONTROLLER_ADDRESS - ); - } - - await create2Factory.safeCreate2( - deployConstants.CONDUIT_CONTROLLER_CREATION_SALT, - conduitControllerFactory.bytecode, - { - gasLimit, - } - ); - - conduitController = await ethers.getContractAt( - "ConduitController", - conduitControllerAddress, - owner - ); - } - conduitCodeHash = keccak256(conduitImplementation.bytecode); - - conduitKeyOne = `${owner.address}000000000000000000000000`; - - await conduitController.createConduit(conduitKeyOne, owner.address); - - const { conduit: conduitOneAddress, exists } = - await conduitController.getConduit(conduitKeyOne); - - expect(exists).to.be.true; - - conduitOne = conduitImplementation.attach(conduitOneAddress); - - // Deploy marketplace contract through efficient create2 factory - const marketplaceContractFactory = await ethers.getContractFactory( - process.env.REFERENCE ? "ReferenceConsideration" : "Seaport" - ); - - directMarketplaceContract = await deployContract( - process.env.REFERENCE ? "ReferenceConsideration" : "Consideration", - owner, - conduitController.address - ); - - if (!hre.__SOLIDITY_COVERAGE_RUNNING && !process.env.REFERENCE) { - expect( - ethers.utils.keccak256( - marketplaceContractFactory.bytecode + - conduitController.address.slice(2).padStart(64, "0") - ) - ).to.equal(deployConstants.MARKETPLACE_CONTRACT_CREATION_HASH); - } - - const marketplaceContractAddress = await create2Factory.findCreate2Address( - deployConstants.MARKETPLACE_CONTRACT_CREATION_SALT, - marketplaceContractFactory.bytecode + - conduitController.address.slice(2).padStart(64, "0") - ); - - if (!hre.__SOLIDITY_COVERAGE_RUNNING && !process.env.REFERENCE) { - expect(marketplaceContractAddress).to.equal( - deployConstants.MARKETPLACE_CONTRACT_ADDRESS - ); - } - - const tx = await create2Factory.safeCreate2( - deployConstants.MARKETPLACE_CONTRACT_CREATION_SALT, - marketplaceContractFactory.bytecode + - conduitController.address.slice(2).padStart(64, "0"), - { - gasLimit, - } - ); - - const { gasUsed } = await tx.wait(); // as of now: 5_479_569 - - marketplaceContract = await ethers.getContractAt( - process.env.REFERENCE ? "ReferenceConsideration" : "Seaport", - marketplaceContractAddress, - owner - ); - - await conduitController - .connect(owner) - .updateChannel(conduitOne.address, marketplaceContract.address, true); - - await resetTokens(); - - stubZone = await deployContract("TestZone", owner); - - tokenByType = [ - { - address: constants.AddressZero, - }, // ETH + ({ + EIP1271WalletFactory, + reenterer, + conduitController, + conduitImplementation, + conduitKeyOne, + conduitOne, + deployNewConduit, testERC20, + mintAndApproveERC20, + getTestItem20, testERC721, + set721ApprovalForAll, + mint721, + mint721s, + mintAndApprove721, + getTestItem721, + getTestItem721WithCriteria, testERC1155, - ]; - - // Required for EIP712 signing - domainData = { - name: process.env.REFERENCE ? "Consideration" : "Seaport", - version: VERSION, - chainId: chainId, - verifyingContract: marketplaceContract.address, - }; - - withBalanceChecks = async ( - ordersArray, // TODO: include order statuses to account for partial fills - additonalPayouts, - criteriaResolvers, - fn, - multiplier = 1 - ) => { - const ordersClone = JSON.parse(JSON.stringify(ordersArray)); - for (const [i, order] of Object.entries(ordersClone)) { - order.parameters.startTime = ordersArray[i].parameters.startTime; - order.parameters.endTime = ordersArray[i].parameters.endTime; - - for (const [j, offerItem] of Object.entries(order.parameters.offer)) { - offerItem.startAmount = - ordersArray[i].parameters.offer[j].startAmount; - offerItem.endAmount = ordersArray[i].parameters.offer[j].endAmount; - } - - for (const [j, considerationItem] of Object.entries( - order.parameters.consideration - )) { - considerationItem.startAmount = - ordersArray[i].parameters.consideration[j].startAmount; - considerationItem.endAmount = - ordersArray[i].parameters.consideration[j].endAmount; - } - } - - if (criteriaResolvers) { - for (const { - orderIndex, - side, - index, - identifier, - } of criteriaResolvers) { - const itemType = - ordersClone[orderIndex].parameters[ - side === 0 ? "offer" : "consideration" - ][index].itemType; - if (itemType < 4) { - console.error("APPLYING CRITERIA TO NON-CRITERIA-BASED ITEM"); - process.exit(1); - } - - ordersClone[orderIndex].parameters[ - side === 0 ? "offer" : "consideration" - ][index].itemType = itemType - 2; - ordersClone[orderIndex].parameters[ - side === 0 ? "offer" : "consideration" - ][index].identifierOrCriteria = identifier; - } - } - - const allOfferedItems = ordersClone - .map((x) => - x.parameters.offer.map((offerItem) => ({ - ...offerItem, - account: x.parameters.offerer, - numerator: x.numerator, - denominator: x.denominator, - startTime: x.parameters.startTime, - endTime: x.parameters.endTime, - })) - ) - .flat(); - - const allReceivedItems = ordersClone - .map((x) => - x.parameters.consideration.map((considerationItem) => ({ - ...considerationItem, - numerator: x.numerator, - denominator: x.denominator, - startTime: x.parameters.startTime, - endTime: x.parameters.endTime, - })) - ) - .flat(); - - for (const offeredItem of allOfferedItems) { - if (offeredItem.itemType > 3) { - console.error("CRITERIA ON OFFERED ITEM NOT RESOLVED"); - process.exit(1); - } - - if (offeredItem.itemType === 0) { - // ETH - offeredItem.initialBalance = await provider.getBalance( - offeredItem.account - ); - } else if (offeredItem.itemType === 3) { - // ERC1155 - offeredItem.initialBalance = await tokenByType[ - offeredItem.itemType - ].balanceOf(offeredItem.account, offeredItem.identifierOrCriteria); - } else if (offeredItem.itemType < 4) { - offeredItem.initialBalance = await tokenByType[ - offeredItem.itemType - ].balanceOf(offeredItem.account); - } - - if (offeredItem.itemType === 2) { - // ERC721 - offeredItem.ownsItemBefore = - (await tokenByType[offeredItem.itemType].ownerOf( - offeredItem.identifierOrCriteria - )) === offeredItem.account; - } - } - - for (const receivedItem of allReceivedItems) { - if (receivedItem.itemType > 3) { - console.error( - "CRITERIA-BASED BALANCE RECEIVED CHECKS NOT IMPLEMENTED YET" - ); - process.exit(1); - } - - if (receivedItem.itemType === 0) { - // ETH - receivedItem.initialBalance = await provider.getBalance( - receivedItem.recipient - ); - } else if (receivedItem.itemType === 3) { - // ERC1155 - receivedItem.initialBalance = await tokenByType[ - receivedItem.itemType - ].balanceOf( - receivedItem.recipient, - receivedItem.identifierOrCriteria - ); - } else { - receivedItem.initialBalance = await tokenByType[ - receivedItem.itemType - ].balanceOf(receivedItem.recipient); - } - - if (receivedItem.itemType === 2) { - // ERC721 - receivedItem.ownsItemBefore = - (await tokenByType[receivedItem.itemType].ownerOf( - receivedItem.identifierOrCriteria - )) === receivedItem.recipient; - } - } - - const receipt = await fn(); - - const from = receipt.from; - const gasUsed = receipt.gasUsed; - - for (const offeredItem of allOfferedItems) { - if (offeredItem.account === from && offeredItem.itemType === 0) { - offeredItem.initialBalance = offeredItem.initialBalance.sub(gasUsed); - } - } - - for (const receivedItem of allReceivedItems) { - if (receivedItem.recipient === from && receivedItem.itemType === 0) { - receivedItem.initialBalance = - receivedItem.initialBalance.sub(gasUsed); - } - } - - for (const offeredItem of allOfferedItems) { - if (offeredItem.itemType > 3) { - console.error("CRITERIA-BASED BALANCE OFFERED CHECKS NOT MET"); - process.exit(1); - } - - if (offeredItem.itemType === 0) { - // ETH - offeredItem.finalBalance = await provider.getBalance( - offeredItem.account - ); - } else if (offeredItem.itemType === 3) { - // ERC1155 - offeredItem.finalBalance = await tokenByType[ - offeredItem.itemType - ].balanceOf(offeredItem.account, offeredItem.identifierOrCriteria); - } else if (offeredItem.itemType < 3) { - // TODO: criteria-based - offeredItem.finalBalance = await tokenByType[ - offeredItem.itemType - ].balanceOf(offeredItem.account); - } - - if (offeredItem.itemType === 2) { - // ERC721 - offeredItem.ownsItemAfter = - (await tokenByType[offeredItem.itemType].ownerOf( - offeredItem.identifierOrCriteria - )) === offeredItem.account; - } - } - - for (const receivedItem of allReceivedItems) { - if (receivedItem.itemType > 3) { - console.error("CRITERIA-BASED BALANCE RECEIVED CHECKS NOT MET"); - process.exit(1); - } - - if (receivedItem.itemType === 0) { - // ETH - receivedItem.finalBalance = await provider.getBalance( - receivedItem.recipient - ); - } else if (receivedItem.itemType === 3) { - // ERC1155 - receivedItem.finalBalance = await tokenByType[ - receivedItem.itemType - ].balanceOf( - receivedItem.recipient, - receivedItem.identifierOrCriteria - ); - } else { - receivedItem.finalBalance = await tokenByType[ - receivedItem.itemType - ].balanceOf(receivedItem.recipient); - } - - if (receivedItem.itemType === 2) { - // ERC721 - receivedItem.ownsItemAfter = - (await tokenByType[receivedItem.itemType].ownerOf( - receivedItem.identifierOrCriteria - )) === receivedItem.recipient; - } - } - - const { timestamp } = await provider.getBlock(receipt.blockHash); - - for (const offeredItem of allOfferedItems) { - const duration = toBN(offeredItem.endTime).sub(offeredItem.startTime); - const elapsed = toBN(timestamp).sub(offeredItem.startTime); - const remaining = duration.sub(elapsed); - - if (offeredItem.itemType < 4) { - // TODO: criteria-based - if (!additonalPayouts) { - expect( - offeredItem.initialBalance - .sub(offeredItem.finalBalance) - .toString() - ).to.equal( - toBN(offeredItem.startAmount) - .mul(remaining) - .add(toBN(offeredItem.endAmount).mul(elapsed)) - .div(duration) - .mul(offeredItem.numerator) - .div(offeredItem.denominator) - .mul(multiplier) - .toString() - ); - } else { - expect( - offeredItem.initialBalance - .sub(offeredItem.finalBalance) - .toString() - ).to.equal(additonalPayouts.add(offeredItem.endAmount).toString()); - } - } - - if (offeredItem.itemType === 2) { - // ERC721 - expect(offeredItem.ownsItemBefore).to.equal(true); - expect(offeredItem.ownsItemAfter).to.equal(false); - } - } - - for (const receivedItem of allReceivedItems) { - const duration = toBN(receivedItem.endTime).sub(receivedItem.startTime); - const elapsed = toBN(timestamp).sub(receivedItem.startTime); - const remaining = duration.sub(elapsed); - - expect( - receivedItem.finalBalance.sub(receivedItem.initialBalance).toString() - ).to.equal( - toBN(receivedItem.startAmount) - .mul(remaining) - .add(toBN(receivedItem.endAmount).mul(elapsed)) - .add(duration.sub(1)) - .div(duration) - .mul(receivedItem.numerator) - .div(receivedItem.denominator) - .mul(multiplier) - .toString() - ); - - if (receivedItem.itemType === 2) { - // ERC721 - expect(receivedItem.ownsItemBefore).to.equal(false); - expect(receivedItem.ownsItemAfter).to.equal(true); - } - } - - return receipt; - }; - - simulateMatchOrders = async (orders, fulfillments, caller, value) => { - return marketplaceContract - .connect(caller) - .callStatic.matchOrders(orders, fulfillments, { - value, - }); - }; - - simulateAdvancedMatchOrders = async ( - orders, - criteriaResolvers, - fulfillments, - caller, - value - ) => { - return marketplaceContract - .connect(caller) - .callStatic.matchAdvancedOrders( - orders, - criteriaResolvers, - fulfillments, - { - value, - } - ); - }; + set1155ApprovalForAll, + mint1155, + mintAndApprove1155, + getTestItem1155WithCriteria, + getTestItem1155, + testERC1155Two, + createTransferWithApproval, + marketplaceContract, + directMarketplaceContract, + stubZone, + createOrder, + createMirrorBuyNowOrder, + createMirrorAcceptOfferOrder, + withBalanceChecks, + checkExpectedEvents, + } = await seaportFixture(owner)); }); describe("Getter tests", async () => { @@ -1884,7 +223,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function }); describe("A single ERC721 is to be transferred", async () => { - describe("[Buy now] User fullfills a sell order for a single ERC721", async () => { + describe("[Buy now] User fulfills a sell order for a single ERC721", async () => { it("ERC721 <=> ETH (standard)", async () => { const nftId = await mintAndApprove721( seller, @@ -1919,7 +258,6 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function order, orderHash, fulfiller: buyer.address, - fulfillerConduitKey: toKey(false), }, ]); return receipt; @@ -2006,7 +344,6 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function order, orderHash, fulfiller: buyer.address, - fulfillerConduitKey: toKey(false), }, ]); @@ -2081,7 +418,54 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, null, async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); + const receipt = await (await tx).wait(); + await checkExpectedEvents(tx, receipt, [ + { + order, + orderHash, + fulfiller: buyer.address, + }, + ]); + + return receipt; + }); + }); + it("ERC721 <=> ETH (standard with restricted order, specified recipient and extra data)", async () => { + const nftId = await mintAndApprove721( + seller, + marketplaceContract.address + ); + + const offer = [getTestItem721(nftId)]; + + const consideration = [ + getItemETH(parseEther("10"), parseEther("10"), seller.address), + getItemETH(parseEther("1"), parseEther("1"), zone.address), + ]; + + const { order, orderHash, value } = await createOrder( + seller, + stubZone, + offer, + consideration, + 2 // FULL_RESTRICTED + ); + + order.extraData = "0x1234"; + + await withBalanceChecks([order], 0, null, async () => { + const tx = marketplaceContract + .connect(buyer) + .fulfillAdvancedOrder(order, [], toKey(false), owner.address, { value, }); const receipt = await (await tx).wait(); @@ -2090,6 +474,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function order, orderHash, fulfiller: buyer.address, + recipient: owner.address, }, ]); @@ -2294,7 +679,9 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function const offer = [getTestItem721(nftId)]; - const consideration = [getItemETH(toBN(1), toBN(1), seller.address)]; + const consideration = [ + getItemETH(toBN(1), toBN(1), constants.AddressZero), + ]; const { order, orderHash, value } = await createOrder( seller, @@ -2362,9 +749,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, null, async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents(tx, receipt, [ { @@ -2410,9 +803,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, null, async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents(tx, receipt, [ { @@ -3286,6 +1685,182 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function return receipt; }); }); + it("ERC721 <=> ERC20 (EIP-1271 signature on non-ECDSA 64 bytes)", async () => { + const sellerContract = await deployContract( + "EIP1271Wallet", + seller, + seller.address + ); + + // Seller mints nft to contract + const nftId = await mint721(sellerContract); + + // Seller approves marketplace contract to transfer NFT + await expect( + sellerContract + .connect(seller) + .approveNFT(testERC721.address, marketplaceContract.address) + ) + .to.emit(testERC721, "ApprovalForAll") + .withArgs( + sellerContract.address, + marketplaceContract.address, + true + ); + + // Buyer mints ERC20 + const tokenAmount = minRandom(100); + await mintAndApproveERC20( + buyer, + marketplaceContract.address, + tokenAmount + ); + + const offer = [getTestItem721(nftId)]; + + const consideration = [ + getTestItem20( + tokenAmount.sub(100), + tokenAmount.sub(100), + sellerContract.address + ), + getTestItem20(50, 50, zone.address), + getTestItem20(50, 50, owner.address), + ]; + + const { order, orderHash } = await createOrder( + sellerContract, + zone, + offer, + consideration, + 0, // FULL_OPEN + [], + null, + seller + ); + + // Compute the digest based on the order hash + const { domainSeparator } = await marketplaceContract.information(); + const digest = keccak256( + `0x1901${domainSeparator.slice(2)}${orderHash.slice(2)}` + ); + + const signature = `0x`.padEnd(130, "f"); + + const basicOrderParameters = { + ...getBasicOrderParameters( + 2, // ERC20ForERC721 + order + ), + signature, + }; + + await withBalanceChecks([order], 0, null, async () => { + const tx = marketplaceContract + .connect(buyer) + .fulfillBasicOrder(basicOrderParameters); + const receipt = await (await tx).wait(); + await checkExpectedEvents(tx, receipt, [ + { + order, + orderHash, + fulfiller: buyer.address, + }, + ]); + + return receipt; + }); + }); + it("ERC721 <=> ERC20 (EIP-1271 signature on non-ECDSA 65 bytes)", async () => { + const sellerContract = await deployContract( + "EIP1271Wallet", + seller, + seller.address + ); + + // Seller mints nft to contract + const nftId = await mint721(sellerContract); + + // Seller approves marketplace contract to transfer NFT + await expect( + sellerContract + .connect(seller) + .approveNFT(testERC721.address, marketplaceContract.address) + ) + .to.emit(testERC721, "ApprovalForAll") + .withArgs( + sellerContract.address, + marketplaceContract.address, + true + ); + + // Buyer mints ERC20 + const tokenAmount = minRandom(100); + await mintAndApproveERC20( + buyer, + marketplaceContract.address, + tokenAmount + ); + + const offer = [getTestItem721(nftId)]; + + const consideration = [ + getTestItem20( + tokenAmount.sub(100), + tokenAmount.sub(100), + sellerContract.address + ), + getTestItem20(50, 50, zone.address), + getTestItem20(50, 50, owner.address), + ]; + + const { order, orderHash } = await createOrder( + sellerContract, + zone, + offer, + consideration, + 0, // FULL_OPEN + [], + null, + seller + ); + + // Compute the digest based on the order hash + const { domainSeparator } = await marketplaceContract.information(); + const digest = keccak256( + `0x1901${domainSeparator.slice(2)}${orderHash.slice(2)}` + ); + + await sellerContract.registerDigest(digest, true); + + const signature = `0x`.padEnd(132, "f"); + + const basicOrderParameters = { + ...getBasicOrderParameters( + 2, // ERC20ForERC721 + order + ), + signature, + }; + + await withBalanceChecks([order], 0, null, async () => { + const tx = marketplaceContract + .connect(buyer) + .fulfillBasicOrder(basicOrderParameters); + const receipt = await (await tx).wait(); + await checkExpectedEvents(tx, receipt, [ + { + order, + orderHash, + fulfiller: buyer.address, + }, + ]); + + return receipt; + }); + + await sellerContract.registerDigest(digest, false); + }); it("ERC721 <=> ERC20 (basic, EIP-1271 signature w/ non-standard length)", async () => { // Seller mints nft to contract const nftId = await mint721(sellerContract); @@ -4046,7 +2621,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function }); describe("A single ERC1155 is to be transferred", async () => { - describe("[Buy now] User fullfills a sell order for a single ERC1155", async () => { + describe("[Buy now] User fulfills a sell order for a single ERC1155", async () => { it("ERC1155 <=> ETH (standard)", async () => { // Seller mints nft const { nftId, amount } = await mintAndApprove1155( @@ -5138,7 +3713,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function }); }); - describe("Validate, cancel, and increment nonce flows", async () => { + describe("Validate, cancel, and increment counter flows", async () => { let seller; let buyer; @@ -5189,18 +3764,36 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function order.signature = "0x"; if (!process.env.REFERENCE) { + const expectedRevertReason = + getCustomRevertSelector("InvalidSignature()"); + + let tx = await marketplaceContract + .connect(buyer) + .populateTransaction.fulfillOrder(order, toKey(false), { + value, + }); + let returnData = await provider.call(tx); + expect(returnData).to.equal(expectedRevertReason); + await expect( marketplaceContract .connect(buyer) .fulfillOrder(order, toKey(false), { value, }) - ).to.be.revertedWith("InvalidSigner"); + ).to.be.reverted; // cannot validate it with no signature from a random account - await expect( - marketplaceContract.connect(owner).validate([order]) - ).to.be.revertedWith("InvalidSigner"); + await expect(marketplaceContract.connect(owner).validate([order])).to + .be.reverted; + + tx = await marketplaceContract + .connect(owner) + .populateTransaction.fulfillOrder(order, toKey(false), { + value, + }); + returnData = await provider.call(tx); + expect(returnData).to.equal(expectedRevertReason); } else { await expect( marketplaceContract @@ -5294,18 +3887,34 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function if (!process.env.REFERENCE) { // cannot fill it with no signature yet + const expectedRevertReason = + getCustomRevertSelector("InvalidSignature()"); + + let tx = await marketplaceContract + .connect(buyer) + .populateTransaction.fulfillOrder(order, toKey(false), { + value, + }); + let returnData = await provider.call(tx); + expect(returnData).to.equal(expectedRevertReason); + await expect( marketplaceContract .connect(buyer) .fulfillOrder(order, toKey(false), { value, }) - ).to.be.revertedWith("InvalidSigner"); + ).to.be.reverted; // cannot validate it with no signature from a random account - await expect( - marketplaceContract.connect(owner).validate([order]) - ).to.be.revertedWith("InvalidSigner"); + tx = await marketplaceContract + .connect(owner) + .populateTransaction.validate([order]); + returnData = await provider.call(tx); + expect(returnData).to.equal(expectedRevertReason); + + await expect(marketplaceContract.connect(owner).validate([order])).to + .be.reverted; } else { // cannot fill it with no signature yet await expect( @@ -5388,20 +3997,36 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function buildOrderStatus(false, false, 0, 0) ); - if (!process.env.REFERENCE) { - // cannot fill it with no signature yet + if (!process.env.REFERENCE) { + // cannot fill it with no signature yet + const expectedRevertReason = + getCustomRevertSelector("InvalidSignature()"); + + let tx = await marketplaceContract + .connect(buyer) + .populateTransaction.fulfillOrder(order, toKey(false), { + value, + }); + let returnData = await provider.call(tx); + expect(returnData).to.equal(expectedRevertReason); + await expect( marketplaceContract .connect(buyer) .fulfillOrder(order, toKey(false), { value, }) - ).to.be.revertedWith("InvalidSigner"); + ).to.be.reverted; + + tx = await marketplaceContract + .connect(owner) + .populateTransaction.validate([order]); + returnData = await provider.call(tx); + expect(returnData).to.equal(expectedRevertReason); // cannot validate it with no signature from a random account - await expect( - marketplaceContract.connect(owner).validate([order]) - ).to.be.revertedWith("InvalidSigner"); + await expect(marketplaceContract.connect(owner).validate([order])).to + .be.reverted; } else { // cannot fill it with no signature yet await expect( @@ -5679,11 +4304,10 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function buildOrderStatus(false, true, 0, 0) ); }); - it.skip("Can cancel an order signed with a nonce ahead of the current nonce", async () => {}); }); - describe("Increment Nonce", async () => { - it("Can increment the nonce", async () => { + describe("Increment Counter", async () => { + it("Can increment the counter", async () => { // Seller mints nft const nftId = await mintAndApprove721( seller, @@ -5706,27 +4330,38 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function 0 // FULL_OPEN ); - const nonce = await marketplaceContract.getNonce(seller.address); - expect(nonce).to.equal(0); - expect(orderComponents.nonce).to.equal(nonce); + const counter = await marketplaceContract.getCounter(seller.address); + expect(counter).to.equal(0); + expect(orderComponents.counter).to.equal(counter); - // can increment the nonce - await expect(marketplaceContract.connect(seller).incrementNonce()) - .to.emit(marketplaceContract, "NonceIncremented") + // can increment the counter + await expect(marketplaceContract.connect(seller).incrementCounter()) + .to.emit(marketplaceContract, "CounterIncremented") .withArgs(1, seller.address); - const newNonce = await marketplaceContract.getNonce(seller.address); - expect(newNonce).to.equal(1); + const newCounter = await marketplaceContract.getCounter(seller.address); + expect(newCounter).to.equal(1); if (!process.env.REFERENCE) { // Cannot fill order anymore + const expectedRevertReason = + getCustomRevertSelector("InvalidSigner()"); + + let tx = await marketplaceContract + .connect(buyer) + .populateTransaction.fulfillOrder(order, toKey(false), { + value, + }); + let returnData = await provider.call(tx); + expect(returnData).to.equal(expectedRevertReason); + await expect( marketplaceContract .connect(buyer) .fulfillOrder(order, toKey(false), { value, }) - ).to.be.revertedWith("InvalidSigner"); + ).to.be.reverted; } else { // Cannot fill order anymore await expect( @@ -5751,9 +4386,9 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function value = newOrderDetails.value; orderComponents = newOrderDetails.orderComponents; - expect(orderComponents.nonce).to.equal(newNonce); + expect(orderComponents.counter).to.equal(newCounter); - // Can fill order with new nonce + // Can fill order with new counter await withBalanceChecks([order], 0, null, async () => { const tx = marketplaceContract .connect(buyer) @@ -5773,7 +4408,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function return receipt; }); }); - it("Can increment the nonce and implicitly cancel a validated order", async () => { + it("Can increment the counter and implicitly cancel a validated order", async () => { // Seller mints nft const nftId = await mintAndApprove721( seller, @@ -5796,31 +4431,42 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function 0 // FULL_OPEN ); - const nonce = await marketplaceContract.getNonce(seller.address); - expect(nonce).to.equal(0); - expect(orderComponents.nonce).to.equal(nonce); + const counter = await marketplaceContract.getCounter(seller.address); + expect(counter).to.equal(0); + expect(orderComponents.counter).to.equal(counter); await expect(marketplaceContract.connect(owner).validate([order])) .to.emit(marketplaceContract, "OrderValidated") .withArgs(orderHash, seller.address, zone.address); - // can increment the nonce - await expect(marketplaceContract.connect(seller).incrementNonce()) - .to.emit(marketplaceContract, "NonceIncremented") + // can increment the counter + await expect(marketplaceContract.connect(seller).incrementCounter()) + .to.emit(marketplaceContract, "CounterIncremented") .withArgs(1, seller.address); - const newNonce = await marketplaceContract.getNonce(seller.address); - expect(newNonce).to.equal(1); + const newCounter = await marketplaceContract.getCounter(seller.address); + expect(newCounter).to.equal(1); if (!process.env.REFERENCE) { // Cannot fill order anymore + const expectedRevertReason = + getCustomRevertSelector("InvalidSigner()"); + + let tx = await marketplaceContract + .connect(buyer) + .populateTransaction.fulfillOrder(order, toKey(false), { + value, + }); + let returnData = await provider.call(tx); + expect(returnData).to.equal(expectedRevertReason); + await expect( marketplaceContract .connect(buyer) .fulfillOrder(order, toKey(false), { value, }) - ).to.be.revertedWith("InvalidSigner"); + ).to.be.reverted; } else { // Cannot fill order anymore await expect( @@ -5845,9 +4491,9 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function value = newOrderDetails.value; orderComponents = newOrderDetails.orderComponents; - expect(orderComponents.nonce).to.equal(newNonce); + expect(orderComponents.counter).to.equal(newCounter); - // Can fill order with new nonce + // Can fill order with new counter await withBalanceChecks([order], 0, null, async () => { const tx = marketplaceContract .connect(buyer) @@ -5867,7 +4513,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function return receipt; }); }); - it("Can increment the nonce as the zone and implicitly cancel a validated order", async () => { + it("Can increment the counter as the zone and implicitly cancel a validated order", async () => { // Seller mints nft const nftId = await mintAndApprove721( seller, @@ -5890,31 +4536,42 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function 0 // FULL_OPEN ); - const nonce = await marketplaceContract.getNonce(seller.address); - expect(nonce).to.equal(0); - expect(orderComponents.nonce).to.equal(nonce); + const counter = await marketplaceContract.getCounter(seller.address); + expect(counter).to.equal(0); + expect(orderComponents.counter).to.equal(counter); await expect(marketplaceContract.connect(owner).validate([order])) .to.emit(marketplaceContract, "OrderValidated") .withArgs(orderHash, seller.address, zone.address); - // can increment the nonce as the offerer - await expect(marketplaceContract.connect(seller).incrementNonce()) - .to.emit(marketplaceContract, "NonceIncremented") + // can increment the counter as the offerer + await expect(marketplaceContract.connect(seller).incrementCounter()) + .to.emit(marketplaceContract, "CounterIncremented") .withArgs(1, seller.address); - const newNonce = await marketplaceContract.getNonce(seller.address); - expect(newNonce).to.equal(1); + const newCounter = await marketplaceContract.getCounter(seller.address); + expect(newCounter).to.equal(1); if (!process.env.REFERENCE) { // Cannot fill order anymore + const expectedRevertReason = + getCustomRevertSelector("InvalidSigner()"); + + let tx = await marketplaceContract + .connect(buyer) + .populateTransaction.fulfillOrder(order, toKey(false), { + value, + }); + let returnData = await provider.call(tx); + expect(returnData).to.equal(expectedRevertReason); + await expect( marketplaceContract .connect(buyer) .fulfillOrder(order, toKey(false), { value, }) - ).to.be.revertedWith("InvalidSigner"); + ).to.be.reverted; } else { // Cannot fill order anymore await expect( @@ -5939,9 +4596,9 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function value = newOrderDetails.value; orderComponents = newOrderDetails.orderComponents; - expect(orderComponents.nonce).to.equal(newNonce); + expect(orderComponents.counter).to.equal(newCounter); - // Can fill order with new nonce + // Can fill order with new counter await withBalanceChecks([order], 0, null, async () => { const tx = marketplaceContract .connect(buyer) @@ -5961,7 +4618,6 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function return receipt; }); }); - it.skip("Can increment nonce and activate an order signed with a nonce ahead of the current nonce", async () => {}); }); }); @@ -6016,9 +4672,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -6050,9 +4712,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -6110,9 +4778,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks(ordersClone, 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -6172,9 +4846,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -6206,9 +4886,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -6266,9 +4952,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks(ordersClone, 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -6470,14 +5162,187 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function ], executions ); - return receipt3; + orderStatus = await marketplaceContract.getOrderStatus(orderHash); + + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(true, false, 10, 10) + ); }); - const orderStatus = await marketplaceContract.getOrderStatus(orderHash); + it("Simplifies fraction when numerator/denominator would overflow", async () => { + const numer1 = toBN(2).pow(100); + const denom1 = toBN(2).pow(101); + const numer2 = toBN(2).pow(20); + const denom2 = toBN(2).pow(22); + const amt = 8; + await mintAndApproveERC20(buyer, marketplaceContract.address, amt); + // Seller mints nft + const { nftId } = await mintAndApprove1155( + seller, + marketplaceContract.address, + 10000, + undefined, + amt + ); + + const offer = [getTestItem1155(nftId, amt, amt)]; - expect({ ...orderStatus }).to.deep.equal( - buildOrderStatus(true, false, 10, 10) - ); + const consideration = [getTestItem20(amt, amt, seller.address)]; + const { order, orderHash, value } = await createOrder( + seller, + undefined, + offer, + consideration, + 1, // PARTIAL_OPEN + undefined, + undefined, + undefined, + undefined, + undefined, + true + ); + let orderStatus = await marketplaceContract.getOrderStatus(orderHash); + + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(false, false, 0, 0) + ); + + // 1/2 + order.numerator = numer1; + order.denominator = denom1; + + await withBalanceChecks([order], 0, [], async () => { + const tx = marketplaceContract + .connect(buyer) + .fulfillAdvancedOrder(order, [], toKey(false), buyer.address, { + value, + }); + const receipt = await (await tx).wait(); + await checkExpectedEvents( + tx, + receipt, + [ + { + order, + orderHash, + fulfiller: buyer.address, + fulfillerConduitKey: toKey(false), + }, + ], + null, + [] + ); + + return receipt; + }); + + orderStatus = await marketplaceContract.getOrderStatus(orderHash); + + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(true, false, numer1, denom1) + ); + + order.numerator = numer2; + order.denominator = denom2; + + await marketplaceContract + .connect(buyer) + .fulfillAdvancedOrder(order, [], toKey(false), buyer.address, { + value, + }); + + orderStatus = await marketplaceContract.getOrderStatus(orderHash); + + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(true, false, toBN(3), toBN(4)) + ); + }); + + it("Reverts when numerator/denominator overflow", async () => { + const prime1 = toBN(2).pow(7).sub(1); + const prime2 = toBN(2).pow(61).sub(1); + const prime3 = toBN(2).pow(107).sub(1); + const amt = prime1.mul(prime2).mul(prime3); + await mintAndApproveERC20(buyer, marketplaceContract.address, amt); + // Seller mints nft + const { nftId } = await mintAndApprove1155( + seller, + marketplaceContract.address, + 10000, + undefined, + amt + ); + + const offer = [getTestItem1155(nftId, amt, amt)]; + + const consideration = [getTestItem20(amt, amt, seller.address)]; + const { order, orderHash, value } = await createOrder( + seller, + undefined, + offer, + consideration, + 1, // PARTIAL_OPEN + undefined, + undefined, + undefined, + undefined, + undefined, + true + ); + let orderStatus = await marketplaceContract.getOrderStatus(orderHash); + + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(false, false, 0, 0) + ); + + // 1/2 + order.numerator = 1; + order.denominator = prime2; + + await withBalanceChecks([order], 0, [], async () => { + const tx = marketplaceContract + .connect(buyer) + .fulfillAdvancedOrder(order, [], toKey(false), buyer.address, { + value, + }); + const receipt = await (await tx).wait(); + await checkExpectedEvents( + tx, + receipt, + [ + { + order, + orderHash, + fulfiller: buyer.address, + fulfillerConduitKey: toKey(false), + }, + ], + null, + [] + ); + + return receipt; + }); + + orderStatus = await marketplaceContract.getOrderStatus(orderHash); + + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(true, false, toBN(1), prime2) + ); + + order.numerator = prime1; + order.denominator = prime3; + + await expect( + marketplaceContract + .connect(buyer) + .fulfillAdvancedOrder(order, [], toKey(false), buyer.address, { + value, + }) + ).to.be.revertedWith( + "0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)" + ); + }); }); describe("Criteria-based orders", async () => { @@ -6516,9 +5381,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, criteriaResolvers, async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + criteriaResolvers, + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -6571,9 +5442,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, criteriaResolvers, async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + criteriaResolvers, + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -6630,9 +5507,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, criteriaResolvers, async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + criteriaResolvers, + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -6866,8 +5749,6 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await testERC721.mint(seller.address, secondNFTId); await testERC721.mint(seller.address, thirdNFTId); - const tokenIds = [nftId, secondNFTId, thirdNFTId]; - // Seller approves marketplace contract to transfer NFTs await set721ApprovalForAll(seller, marketplaceContract.address, true); @@ -6976,8 +5857,13 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await set721ApprovalForAll(buyer, marketplaceContract.address, true); const { root, proofs } = merkleTree(tokenIds); - - const offer = [getItemETH(parseEther("10"), parseEther("10"))]; + const tokenAmount = minRandom(100); + await mintAndApproveERC20( + seller, + marketplaceContract.address, + tokenAmount + ); + const offer = [getTestItem20(tokenAmount, tokenAmount)]; const consideration = [ getTestItem721WithCriteria(root, toBN(1), toBN(1), seller.address), @@ -7003,9 +5889,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + criteriaResolvers, + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -7034,8 +5926,13 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await set1155ApprovalForAll(buyer, marketplaceContract.address, true); const { root, proofs } = merkleTree([nftId]); - - const offer = [getItemETH(parseEther("10"), parseEther("10"))]; + const tokenAmount = minRandom(100); + await mintAndApproveERC20( + seller, + marketplaceContract.address, + tokenAmount + ); + const offer = [getTestItem20(tokenAmount, tokenAmount)]; const consideration = [ getTestItem1155WithCriteria(root, toBN(1), toBN(1), seller.address), @@ -7061,9 +5958,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + criteriaResolvers, + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -7086,12 +5989,17 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function }); it("Criteria-based wildcard consideration item (standard)", async () => { // buyer mints nft - const nftId = await mint721(buyer); - - // Seller approves marketplace contract to transfer NFTs - await set721ApprovalForAll(buyer, marketplaceContract.address, true); - - const offer = [getItemETH(parseEther("10"), parseEther("10"))]; + const nftId = await mintAndApprove721( + buyer, + marketplaceContract.address + ); + const tokenAmount = minRandom(100); + await mintAndApproveERC20( + seller, + marketplaceContract.address, + tokenAmount + ); + const offer = [getTestItem20(tokenAmount, tokenAmount)]; const consideration = [ getTestItem721WithCriteria( @@ -7120,9 +6028,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + criteriaResolvers, + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -7414,9 +6328,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -7500,9 +6420,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -7610,167 +6536,6 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function buildOrderStatus(true, false, 1, 1) ); }); - it.skip("Ascending consideration amount (match)", async () => {}); - it.skip("Ascending amount + partial fill (standard)", async () => {}); - it.skip("Ascending amount + partial fill (match)", async () => {}); - it.skip("Descending offer amount (standard)", async () => { - // Seller mints nft - const nftId = randomBN(); - const endAmount = toBN(randomBN(2)); - const startAmount = endAmount.div(2); - - await testERC1155.mint(seller.address, nftId, endAmount.mul(10)); - - // Seller approves marketplace contract to transfer NFTs - - await set1155ApprovalForAll(seller, marketplaceContract.address, true); - - const offer = [ - getTestItem1155(nftId, startAmount, endAmount, undefined), - ]; - - const consideration = [ - getItemETH(parseEther("10"), parseEther("10"), seller.address), - getItemETH(parseEther("1"), parseEther("1"), zone.address), - getItemETH(parseEther("1"), parseEther("1"), owner.address), - ]; - - const { order, orderHash, value } = await createOrder( - seller, - zone, - offer, - consideration, - 0 // FULL_OPEN - ); - - let orderStatus = await marketplaceContract.getOrderStatus(orderHash); - - expect({ ...orderStatus }).to.deep.equal( - buildOrderStatus(false, false, 0, 0) - ); - - await withBalanceChecks([order], 0, [], async () => { - const tx = marketplaceContract - .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); - const receipt = await (await tx).wait(); - await checkExpectedEvents( - tx, - receipt, - [ - { - order, - orderHash, - fulfiller: buyer.address, - fulfillerConduitKey: toKey(false), - }, - ], - null, - [] - ); - - return receipt; - }); - - orderStatus = await marketplaceContract.getOrderStatus(orderHash); - - expect({ ...orderStatus }).to.deep.equal( - buildOrderStatus(true, false, 1, 1) - ); - }); - it.skip("Descending consideration amount (standard)", async () => { - // Seller mints ERC20 - const tokenAmount = toBN(random128()); - await mintAndApproveERC20( - seller, - marketplaceContract.address, - tokenAmount - ); - - // Buyer mints nft - const nftId = randomBN(); - const endAmount = toBN(randomBN(2)); - const startAmount = endAmount.div(2); - - await testERC1155.mint(buyer.address, nftId, endAmount.mul(10)); - - // Buyer approves marketplace contract to transfer NFTs - await set1155ApprovalForAll(buyer, marketplaceContract.address, true); - - // Buyer needs to approve marketplace to transfer ERC20 tokens too (as it's a standard fulfillment) - await expect( - testERC20 - .connect(buyer) - .approve(marketplaceContract.address, tokenAmount) - ) - .to.emit(testERC20, "Approval") - .withArgs(buyer.address, marketplaceContract.address, tokenAmount); - - const offer = [getTestItem20(tokenAmount, tokenAmount)]; - - const consideration = [ - getTestItem1155( - nftId, - startAmount, - endAmount, - undefined, - seller.address - ), - getTestItem20(50, 50, zone.address), - getTestItem20(50, 50, owner.address), - ]; - - const { order, orderHash, value } = await createOrder( - seller, - zone, - offer, - consideration, - 0 // FULL_OPEN - ); - - let orderStatus = await marketplaceContract.getOrderStatus(orderHash); - - expect({ ...orderStatus }).to.deep.equal( - buildOrderStatus(false, false, 0, 0) - ); - - await withBalanceChecks([order], 0, [], async () => { - const tx = marketplaceContract - .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); - const receipt = await (await tx).wait(); - await checkExpectedEvents( - tx, - receipt, - [ - { - order, - orderHash, - fulfiller: buyer.address, - fulfillerConduitKey: toKey(false), - }, - ], - null, - [] - ); - - return receipt; - }); - - orderStatus = await marketplaceContract.getOrderStatus(orderHash); - - expect({ ...orderStatus }).to.deep.equal( - buildOrderStatus(true, false, 1, 1) - ); - }); - it.skip("Descending offer amount (match)", async () => {}); - it.skip("Descending consideration amount (match)", async () => {}); - it.skip("Descending amount + partial fill (standard)", async () => {}); - it.skip("Descending amount + partial fill (match)", async () => {}); }); describe("Sequenced Orders", async () => { @@ -8332,7 +7097,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function true ); - // TODO: inlcude balance checks on the duplicate ERC20 transfers + // TODO: include balance checks on the duplicate ERC20 transfers return receipt; }); @@ -8976,21 +7741,76 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function expect(executions.length).to.equal(7); - await marketplaceContract - .connect(owner) - .matchOrders([order, mirrorOrder], fulfillments, { - value, - }); + await marketplaceContract + .connect(owner) + .matchOrders([order, mirrorOrder], fulfillments, { + value, + }); + }); + }); + + describe("Fulfill Available Orders", async () => { + it("Can fulfill a single order via fulfillAvailableOrders", async () => { + // Seller mints nft + const nftId = await mintAndApprove721( + seller, + marketplaceContract.address, + 10 + ); + + const offer = [getTestItem721(nftId)]; + + const consideration = [ + getItemETH(parseEther("10"), parseEther("10"), seller.address), + getItemETH(parseEther("1"), parseEther("1"), zone.address), + getItemETH(parseEther("1"), parseEther("1"), owner.address), + ]; + + const { order, orderHash, value } = await createOrder( + seller, + zone, + offer, + consideration, + 0 // FULL_OPEN + ); + + const offerComponents = [toFulfillmentComponents([[0, 0]])]; + + const considerationComponents = [[[0, 0]], [[0, 1]], [[0, 2]]].map( + toFulfillmentComponents + ); + + await withBalanceChecks([order], 0, null, async () => { + const tx = marketplaceContract + .connect(buyer) + .fulfillAvailableOrders( + [order], + offerComponents, + considerationComponents, + toKey(false), + 100, + { + value, + } + ); + const receipt = await (await tx).wait(); + await checkExpectedEvents(tx, receipt, [ + { + order, + orderHash, + fulfiller: buyer.address, + }, + ]); + + return receipt; + }); }); - }); - - describe("Fulfill Available Orders", async () => { - it("Can fulfill a single order via fulfillAvailableOrders", async () => { + it("Can fulfill a single order via fulfillAvailableAdvancedOrders", async () => { // Seller mints nft const nftId = await mintAndApprove721( seller, marketplaceContract.address, - 10 + 11 ); const offer = [getTestItem721(nftId)]; @@ -9009,20 +7829,20 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function 0 // FULL_OPEN ); - const offerComponents = [toFulfillmentComponents([[0, 0]])]; + const offerComponents = [[[0, 0]]]; - const considerationComponents = [[[0, 0]], [[0, 1]], [[0, 2]]].map( - toFulfillmentComponents - ); + const considerationComponents = [[[0, 0]], [[0, 1]], [[0, 2]]]; await withBalanceChecks([order], 0, null, async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAvailableOrders( + .fulfillAvailableAdvancedOrders( [order], + [], offerComponents, considerationComponents, toKey(false), + constants.AddressZero, 100, { value, @@ -9040,12 +7860,11 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function return receipt; }); }); - it("Can fulfill a single order via fulfillAvailableAdvancedOrders", async () => { + it("Can fulfill a single order via fulfillAvailableAdvancedOrders with recipient specified", async () => { // Seller mints nft const nftId = await mintAndApprove721( seller, - marketplaceContract.address, - 11 + marketplaceContract.address ); const offer = [getTestItem721(nftId)]; @@ -9053,7 +7872,6 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function const consideration = [ getItemETH(parseEther("10"), parseEther("10"), seller.address), getItemETH(parseEther("1"), parseEther("1"), zone.address), - getItemETH(parseEther("1"), parseEther("1"), owner.address), ]; const { order, orderHash, value } = await createOrder( @@ -9066,7 +7884,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function const offerComponents = [[[0, 0]]]; - const considerationComponents = [[[0, 0]], [[0, 1]], [[0, 2]]]; + const considerationComponents = [[[0, 0]], [[0, 1]]]; await withBalanceChecks([order], 0, null, async () => { const tx = marketplaceContract @@ -9077,6 +7895,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function offerComponents, considerationComponents, toKey(false), + owner.address, 100, { value, @@ -9088,6 +7907,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function order, orderHash, fulfiller: buyer.address, + recipient: owner.address, }, ]); @@ -9273,6 +8093,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function offerComponents, considerationComponents, toKey(false), + constants.AddressZero, 100, { value: value.mul(2), @@ -9489,6 +8310,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function offerComponents, considerationComponents, toKey(false), + constants.AddressZero, 1, { value: value.mul(2), @@ -9791,6 +8613,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function offerComponents, considerationComponents, toKey(false), + constants.AddressZero, 100, { value: value.mul(4), @@ -9909,6 +8732,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function offerComponents, considerationComponents, toKey(false), + constants.AddressZero, 100, { value: value.mul(2), @@ -10313,7 +9137,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function }); // Create/Approve X amount of ERC20s - let erc20Transfer = await createTransferWithApproval( + const erc20Transfer = await createTransferWithApproval( testERC20, seller, 1, @@ -10323,7 +9147,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function ); // Create/Approve Y amount of ERC721s - let erc721Transfer = await createTransferWithApproval( + const erc721Transfer = await createTransferWithApproval( testERC721, seller, 2, @@ -10333,7 +9157,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function ); // Create/Approve Z amount of ERC1155s - let erc1155Transfer = await createTransferWithApproval( + const erc1155Transfer = await createTransferWithApproval( testERC1155, seller, 3, @@ -10373,26 +9197,26 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function }); // Get 3 Numbers that's value adds to Item Amount and minimum 1. - let itemsToCreate = 64; - let numERC20s = randomInt(itemsToCreate - 2); - let numEC721s = Math.max(1, randomInt(itemsToCreate - numERC20s - 1)); - let numERC1155s = Math.max(1, itemsToCreate - numERC20s - numEC721s); + const itemsToCreate = 64; + const numERC20s = Math.max(1, randomInt(itemsToCreate - 2)); + const numEC721s = Math.max(1, randomInt(itemsToCreate - numERC20s - 1)); + const numERC1155s = Math.max(1, itemsToCreate - numERC20s - numEC721s); - let erc20Contracts = [numERC20s]; - let erc20Transfers = [numERC20s]; + const erc20Contracts = [numERC20s]; + const erc20Transfers = [numERC20s]; - let erc721Contracts = [numEC721s]; - let erc721Transfers = [numEC721s]; + const erc721Contracts = [numEC721s]; + const erc721Transfers = [numEC721s]; - let erc1155Contracts = [numERC1155s]; - let erc1155Transfers = [numERC1155s]; + const erc1155Contracts = [numERC1155s]; + const erc1155Transfers = [numERC1155s]; // Create numERC20s amount of ERC20 objects for (let i = 0; i < numERC20s; i++) { // Deploy Contract - let tempERC20Contract = await deployContracts(1); + const { testERC20: tempERC20Contract } = await fixtureERC20(owner); // Create/Approve X amount of ERC20s - let erc20Transfer = await createTransferWithApproval( + const erc20Transfer = await createTransferWithApproval( tempERC20Contract, seller, 1, @@ -10407,9 +9231,9 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function // Create numEC721s amount of ERC20 objects for (let i = 0; i < numEC721s; i++) { // Deploy Contract - let tempERC721Contract = await deployContracts(2); + const { testERC721: tempERC721Contract } = await fixtureERC721(owner); // Create/Approve numEC721s amount of ERC721s - let erc721Transfer = await createTransferWithApproval( + const erc721Transfer = await createTransferWithApproval( tempERC721Contract, seller, 2, @@ -10424,9 +9248,11 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function // Create numERC1155s amount of ERC1155 objects for (let i = 0; i < numERC1155s; i++) { // Deploy Contract - let tempERC1155Contract = await deployContracts(3); + const { testERC1155: tempERC1155Contract } = await fixtureERC1155( + owner + ); // Create/Approve numERC1155s amount of ERC1155s - let erc1155Transfer = await createTransferWithApproval( + const erc1155Transfer = await createTransferWithApproval( tempERC1155Contract, seller, 3, @@ -10438,8 +9264,14 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function erc1155Transfers[i] = erc1155Transfer; } - let transfers = erc20Transfers.concat(erc721Transfers, erc1155Transfers); - let contracts = erc20Contracts.concat(erc721Contracts, erc1155Contracts); + const transfers = erc20Transfers.concat( + erc721Transfers, + erc1155Transfers + ); + const contracts = erc20Contracts.concat( + erc721Contracts, + erc1155Contracts + ); // Send the transfers await tempConduit.connect(seller).execute(transfers); @@ -10534,6 +9366,111 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function ).to.be.revertedWith("NoContract"); }); + it("ERC1155 batch transfer reverts with revert data if it has sufficient gas", async () => { + // Owner updates conduit channel to allow seller access + await whileImpersonating(owner.address, provider, async () => { + await conduitController + .connect(owner) + .updateChannel(tempConduit.address, seller.address, true); + }); + + await expect( + tempConduit.connect(seller).executeWithBatch1155( + [], + [ + { + token: testERC1155.address, + from: seller.address, + to: buyer.address, + ids: [1], + amounts: [1], + }, + ] + ) + ).to.be.revertedWith("NOT_AUTHORIZED"); + }); + if (!process.env.REFERENCE) { + it("ERC1155 batch transfer sends no data", async () => { + const receiver = await deployContract("ERC1155BatchRecipient", owner); + // Owner updates conduit channel to allow seller access + await whileImpersonating(owner.address, provider, async () => { + await conduitController + .connect(owner) + .updateChannel(tempConduit.address, seller.address, true); + }); + + const { nftId, amount } = await mint1155(owner, 2); + + const { nftId: secondNftId, amount: secondAmount } = await mint1155( + owner, + 2 + ); + const { nftId: thirdNftId, amount: thirdAmount } = await mint1155( + owner, + 2 + ); + + await testERC1155.mint(seller.address, nftId, amount.mul(2)); + await testERC1155.mint( + seller.address, + secondNftId, + secondAmount.mul(2) + ); + await testERC1155.mint(seller.address, thirdNftId, thirdAmount.mul(2)); + await set1155ApprovalForAll(seller, tempConduit.address, true); + + await tempConduit.connect(seller).executeWithBatch1155( + [], + [ + { + token: testERC1155.address, + from: seller.address, + to: receiver.address, + ids: [nftId, secondNftId, thirdNftId], + amounts: [amount, secondAmount, thirdAmount], + }, + { + token: testERC1155.address, + from: seller.address, + to: receiver.address, + ids: [secondNftId, nftId], + amounts: [secondAmount, amount], + }, + ] + ); + }); + + it("ERC1155 batch transfer reverts with generic error if it has insufficient gas to copy revert data", async () => { + const receiver = await deployContract( + "ExcessReturnDataRecipient", + owner + ); + // Owner updates conduit channel to allow seller access + await whileImpersonating(owner.address, provider, async () => { + await conduitController + .connect(owner) + .updateChannel(tempConduit.address, seller.address, true); + }); + + await expect( + tempConduit.connect(seller).executeWithBatch1155( + [], + [ + { + token: receiver.address, + from: seller.address, + to: receiver.address, + ids: [1], + amounts: [1], + }, + ] + ) + ).to.be.revertedWith( + `ERC1155BatchTransferGenericFailure("${receiver.address}", "${seller.address}", "${receiver.address}", [1], [1])` + ); + }); + } + it("Makes batch transfer 1155 items through a conduit", async () => { const tempConduitKey = owner.address + "ff00000000000000000000f1"; @@ -10846,21 +9783,27 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function }); it("Reverts when attempting to execute transfers on a conduit when not called from a channel", async () => { - await expect(conduitOne.connect(owner).execute([])).to.be.revertedWith( - "ChannelClosed" - ); + let expectedRevertReason = + getCustomRevertSelector("ChannelClosed(address)") + + owner.address.slice(2).padStart(64, "0").toLowerCase(); + + let tx = await conduitOne.connect(owner).populateTransaction.execute([]); + let returnData = await provider.call(tx); + expect(returnData).to.equal(expectedRevertReason); + + await expect(conduitOne.connect(owner).execute([])).to.be.reverted; }); it("Reverts when attempting to execute with 1155 transfers on a conduit when not called from a channel", async () => { await expect( conduitOne.connect(owner).executeWithBatch1155([], []) - ).to.be.revertedWith("ChannelClosed"); + ).to.be.revertedWith("ChannelClosed", owner); }); it("Reverts when attempting to execute batch 1155 transfers on a conduit when not called from a channel", async () => { await expect( conduitOne.connect(owner).executeBatch1155([]) - ).to.be.revertedWith("ChannelClosed"); + ).to.be.revertedWith("ChannelClosed", owner); }); it("Retrieves the owner of a conduit", async () => { @@ -10954,9 +9897,11 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function expect(isOpen).to.be.true; // No-op - await conduitController - .connect(owner) - .updateChannel(conduitOne.address, marketplaceContract.address, true); + await expect( + conduitController + .connect(owner) + .updateChannel(conduitOne.address, marketplaceContract.address, true) + ).to.be.reverted; // ChannelStatusAlreadySet isOpen = await conduitController.getChannelStatus( conduitOne.address, @@ -11049,21 +9994,13 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( conduitOne.connect(seller).executeWithBatch1155( [ - { - itemType: 1, // ERC20 - token: testERC20.address, - from: buyer.address, - to: seller.address, - identifier: 0, - amount: 0, - }, { itemType: 0, // NATIVE (invalid) token: constants.AddressZero, from: conduitOne.address, to: seller.address, identifier: 0, - amount: 1, + amount: 0, }, ], [] @@ -11122,73 +10059,372 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function conduitOne.address ); - await expect( - conduitController - .connect(owner) - .transferOwnership(seller.address, buyer.address) - ).to.be.revertedWith("NoConduit"); + await expect( + conduitController + .connect(owner) + .transferOwnership(seller.address, buyer.address) + ).to.be.revertedWith("NoConduit"); + + let potentialOwner = await conduitController.getPotentialOwner( + conduitOne.address + ); + expect(potentialOwner).to.equal(constants.AddressZero); + + await conduitController.transferOwnership( + conduitOne.address, + buyer.address + ); + + potentialOwner = await conduitController.getPotentialOwner( + conduitOne.address + ); + expect(potentialOwner).to.equal(buyer.address); + + await expect( + conduitController + .connect(owner) + .transferOwnership(conduitOne.address, buyer.address) + ).to.be.revertedWith( + "NewPotentialOwnerAlreadySet", + conduitOne.address, + buyer.address + ); + + await expect( + conduitController + .connect(buyer) + .cancelOwnershipTransfer(conduitOne.address) + ).to.be.revertedWith("CallerIsNotOwner", conduitOne.address); + + await expect( + conduitController.connect(owner).cancelOwnershipTransfer(seller.address) + ).to.be.revertedWith("NoConduit"); + + await conduitController.cancelOwnershipTransfer(conduitOne.address); + + potentialOwner = await conduitController.getPotentialOwner( + conduitOne.address + ); + expect(potentialOwner).to.equal(constants.AddressZero); + + await expect( + conduitController + .connect(owner) + .cancelOwnershipTransfer(conduitOne.address) + ).to.be.revertedWith("NoPotentialOwnerCurrentlySet", conduitOne.address); + + await conduitController.transferOwnership( + conduitOne.address, + buyer.address + ); + + potentialOwner = await conduitController.getPotentialOwner( + conduitOne.address + ); + expect(potentialOwner).to.equal(buyer.address); + + await expect( + conduitController.connect(buyer).acceptOwnership(seller.address) + ).to.be.revertedWith("NoConduit"); + + await expect( + conduitController.connect(seller).acceptOwnership(conduitOne.address) + ).to.be.revertedWith("CallerIsNotNewPotentialOwner", conduitOne.address); + + await conduitController + .connect(buyer) + .acceptOwnership(conduitOne.address); + + potentialOwner = await conduitController.getPotentialOwner( + conduitOne.address + ); + expect(potentialOwner).to.equal(constants.AddressZero); + + const ownerOf = await conduitController.ownerOf(conduitOne.address); + expect(ownerOf).to.equal(buyer.address); + }); + }); + + describe("TransferHelper tests", async () => { + let sender; + let recipient; + let senderContract; + let recipientContract; + let tempTransferHelper; + let tempConduit; + let tempConduitKey; + + beforeEach(async () => { + // Setup basic buyer/seller wallets with ETH + sender = new ethers.Wallet(randomHex(32), provider); + recipient = new ethers.Wallet(randomHex(32), provider); + zone = new ethers.Wallet(randomHex(32), provider); + + senderContract = await EIP1271WalletFactory.deploy(sender.address); + recipientContract = await EIP1271WalletFactory.deploy(recipient.address); + + tempConduitKey = owner.address + randomHex(12).slice(2); + tempConduit = await deployNewConduit(owner, tempConduitKey); + + await Promise.all( + [sender, recipient, zone, senderContract, recipientContract].map( + (wallet) => faucet(wallet.address, provider) + ) + ); + + // Deploy a new TransferHelper with the tempConduitController address + const transferHelperFactory = await ethers.getContractFactory( + "TransferHelper" + ); + tempTransferHelper = await transferHelperFactory.deploy( + conduitController.address + ); + + await whileImpersonating(owner.address, provider, async () => { + await conduitController + .connect(owner) + .updateChannel(tempConduit.address, tempTransferHelper.address, true); + }); + }); + + it("Executes transfers (many token types) with a conduit", async () => { + // Get 3 Numbers that's value adds to Item Amount and minimum 1. + const itemsToCreate = 10; + const numERC20s = Math.max(1, randomInt(itemsToCreate - 2)); + const numEC721s = Math.max(1, randomInt(itemsToCreate - numERC20s - 1)); + const numERC1155s = Math.max(1, itemsToCreate - numERC20s - numEC721s); + + const erc20Contracts = [numERC20s]; + const erc20Transfers = [numERC20s]; + + const erc721Contracts = [numEC721s]; + const erc721Transfers = [numEC721s]; + + const erc1155Contracts = [numERC1155s]; + const erc1155Transfers = [numERC1155s]; + + // Create numERC20s amount of ERC20 objects + for (let i = 0; i < numERC20s; i++) { + // Deploy Contract + const { testERC20: tempERC20Contract } = await fixtureERC20(owner); + // Create/Approve X amount of ERC20s + const erc20Transfer = await createTransferWithApproval( + tempERC20Contract, + sender, + 1, + tempConduit.address + ); + erc20Contracts[i] = tempERC20Contract; + erc20Transfers[i] = erc20Transfer; + } + + // Create numEC721s amount of ERC20 objects + for (let i = 0; i < numEC721s; i++) { + // Deploy Contract + const { testERC721: tempERC721Contract } = await fixtureERC721(owner); + // Create/Approve numEC721s amount of ERC721s + const erc721Transfer = await createTransferWithApproval( + tempERC721Contract, + sender, + 2, + tempConduit.address + ); + erc721Contracts[i] = tempERC721Contract; + erc721Transfers[i] = erc721Transfer; + } + + // Create numERC1155s amount of ERC1155 objects + for (let i = 0; i < numERC1155s; i++) { + // Deploy Contract + const { testERC1155: tempERC1155Contract } = await fixtureERC1155( + owner + ); + // Create/Approve numERC1155s amount of ERC1155s + const erc1155Transfer = await createTransferWithApproval( + tempERC1155Contract, + sender, + 3, + tempConduit.address + ); + erc1155Contracts[i] = tempERC1155Contract; + erc1155Transfers[i] = erc1155Transfer; + } + + const transfers = erc20Transfers.concat( + erc721Transfers, + erc1155Transfers + ); + const contracts = erc20Contracts.concat( + erc721Contracts, + erc1155Contracts + ); + // Send the bulk transfers + await tempTransferHelper + .connect(sender) + .bulkTransfer(transfers, recipient.address, tempConduitKey); + // Loop through all transfer to do ownership/balance checks + for (let i = 0; i < transfers.length; i++) { + // Get Itemtype, token, amount, identifier + const { itemType, amount, identifier } = transfers[i]; + const token = contracts[i]; + + switch (itemType) { + case 1: // ERC20 + // Check balance + expect(await token.balanceOf(sender.address)).to.equal(0); + expect(await token.balanceOf(recipient.address)).to.equal(amount); + break; + case 2: // ERC721 + case 4: // ERC721_WITH_CRITERIA + expect(await token.ownerOf(identifier)).to.equal(recipient.address); + break; + case 3: // ERC1155 + case 5: // ERC1155_WITH_CRITERIA + // Check balance + expect(await token.balanceOf(sender.address, identifier)).to.equal( + 0 + ); + expect( + await token.balanceOf(recipient.address, identifier) + ).to.equal(amount); + break; + } + } + }); - let potentialOwner = await conduitController.getPotentialOwner( - conduitOne.address - ); - expect(potentialOwner).to.equal(constants.AddressZero); + it("Executes transfers (many token types) without a conduit", async () => { + // Get 3 Numbers that's value adds to Item Amount and minimum 1. + const itemsToCreate = 10; + const numERC20s = Math.max(1, randomInt(itemsToCreate - 2)); + const numEC721s = Math.max(1, randomInt(itemsToCreate - numERC20s - 1)); + const numERC1155s = Math.max(1, itemsToCreate - numERC20s - numEC721s); - await conduitController.transferOwnership( - conduitOne.address, - buyer.address - ); + const erc20Contracts = [numERC20s]; + const erc20Transfers = [numERC20s]; - potentialOwner = await conduitController.getPotentialOwner( - conduitOne.address - ); - expect(potentialOwner).to.equal(buyer.address); + const erc721Contracts = [numEC721s]; + const erc721Transfers = [numEC721s]; - await expect( - conduitController - .connect(buyer) - .cancelOwnershipTransfer(conduitOne.address) - ).to.be.revertedWith("CallerIsNotOwner", conduitOne.address); + const erc1155Contracts = [numERC1155s]; + const erc1155Transfers = [numERC1155s]; - await expect( - conduitController.connect(owner).cancelOwnershipTransfer(seller.address) - ).to.be.revertedWith("NoConduit"); + // Create numERC20s amount of ERC20 objects + for (let i = 0; i < numERC20s; i++) { + // Deploy Contract + const { testERC20: tempERC20Contract } = await fixtureERC20(owner); + // Create/Approve X amount of ERC20s + const erc20Transfer = await createTransferWithApproval( + tempERC20Contract, + sender, + 1, + tempTransferHelper.address + ); + erc20Contracts[i] = tempERC20Contract; + erc20Transfers[i] = erc20Transfer; + } - await conduitController.cancelOwnershipTransfer(conduitOne.address); + // Create numEC721s amount of ERC20 objects + for (let i = 0; i < numEC721s; i++) { + // Deploy Contract + const { testERC721: tempERC721Contract } = await fixtureERC721(owner); + // Create/Approve numEC721s amount of ERC721s + const erc721Transfer = await createTransferWithApproval( + tempERC721Contract, + sender, + 2, + tempTransferHelper.address + ); + erc721Contracts[i] = tempERC721Contract; + erc721Transfers[i] = erc721Transfer; + } - potentialOwner = await conduitController.getPotentialOwner( - conduitOne.address - ); - expect(potentialOwner).to.equal(constants.AddressZero); + // Create numERC1155s amount of ERC1155 objects + for (let i = 0; i < numERC1155s; i++) { + // Deploy Contract + const { testERC1155: tempERC1155Contract } = await fixtureERC1155( + owner + ); + // Create/Approve numERC1155s amount of ERC1155s + const erc1155Transfer = await createTransferWithApproval( + tempERC1155Contract, + sender, + 3, + tempTransferHelper.address + ); + erc1155Contracts[i] = tempERC1155Contract; + erc1155Transfers[i] = erc1155Transfer; + } - await conduitController.transferOwnership( - conduitOne.address, - buyer.address + const transfers = erc20Transfers.concat( + erc721Transfers, + erc1155Transfers ); - - potentialOwner = await conduitController.getPotentialOwner( - conduitOne.address + const contracts = erc20Contracts.concat( + erc721Contracts, + erc1155Contracts ); - expect(potentialOwner).to.equal(buyer.address); + // Send the bulk transfers + await tempTransferHelper + .connect(sender) + .bulkTransfer( + transfers, + recipient.address, + ethers.utils.formatBytes32String("") + ); + // Loop through all transfer to do ownership/balance checks + for (let i = 0; i < transfers.length; i++) { + // Get Itemtype, token, amount, identifier + const { itemType, amount, identifier } = transfers[i]; + const token = contracts[i]; - await expect( - conduitController.connect(buyer).acceptOwnership(seller.address) - ).to.be.revertedWith("NoConduit"); + switch (itemType) { + case 1: // ERC20 + // Check balance + expect(await token.balanceOf(sender.address)).to.equal(0); + expect(await token.balanceOf(recipient.address)).to.equal(amount); + break; + case 2: // ERC721 + case 4: // ERC721_WITH_CRITERIA + expect(await token.ownerOf(identifier)).to.equal(recipient.address); + break; + case 3: // ERC1155 + case 5: // ERC1155_WITH_CRITERIA + // Check balance + expect(await token.balanceOf(sender.address, identifier)).to.equal( + 0 + ); + expect( + await token.balanceOf(recipient.address, identifier) + ).to.equal(amount); + break; + } + } + }); + it("Reverts on native token transfers", async () => { + const ethTransferHelperItems = [ + { + itemType: 0, + token: ethers.constants.AddressZero, + identifier: 0, + amount: 10, + }, + { + itemType: 0, + token: ethers.constants.AddressZero, + identifier: 0, + amount: 20, + }, + ]; await expect( - conduitController.connect(seller).acceptOwnership(conduitOne.address) - ).to.be.revertedWith("CallerIsNotNewPotentialOwner", conduitOne.address); - - await conduitController - .connect(buyer) - .acceptOwnership(conduitOne.address); - - potentialOwner = await conduitController.getPotentialOwner( - conduitOne.address - ); - expect(potentialOwner).to.equal(constants.AddressZero); - - const ownerOf = await conduitController.ownerOf(conduitOne.address); - expect(ownerOf).to.equal(buyer.address); + tempTransferHelper + .connect(sender) + .bulkTransfer( + ethTransferHelperItems, + recipient.address, + ethers.utils.formatBytes32String("") + ) + ).to.be.revertedWith("InvalidItemType"); }); }); @@ -11251,9 +10487,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith("BadFraction"); orderStatus = await marketplaceContract.getOrderStatus(orderHash); @@ -11268,9 +10510,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith("BadFraction"); orderStatus = await marketplaceContract.getOrderStatus(orderHash); @@ -11285,9 +10533,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith("BadFraction"); orderStatus = await marketplaceContract.getOrderStatus(orderHash); @@ -11302,9 +10556,108 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); + const receipt = await (await tx).wait(); + await checkExpectedEvents( + tx, + receipt, + [ + { + order, + orderHash, + fulfiller: buyer.address, + fulfillerConduitKey: toKey(false), + }, + ], + null, + [] + ); + + return receipt; + }); + + orderStatus = await marketplaceContract.getOrderStatus(orderHash); + + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(true, false, 1, 2) + ); + }); + it("Reverts on inexact fraction amounts", async () => { + // Seller mints nft + const { nftId, amount } = await mintAndApprove1155( + seller, + marketplaceContract.address, + 10000 + ); + + const offer = [getTestItem1155(nftId, amount.mul(10), amount.mul(10))]; + + const consideration = [ + getItemETH(amount.mul(1000), amount.mul(1000), seller.address), + getItemETH(amount.mul(10), amount.mul(10), zone.address), + getItemETH(amount.mul(20), amount.mul(20), owner.address), + ]; + + const { order, orderHash, value } = await createOrder( + seller, + zone, + offer, + consideration, + 1 // PARTIAL_OPEN + ); + + let orderStatus = await marketplaceContract.getOrderStatus(orderHash); + + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(false, false, 0, 0) + ); + + order.numerator = 1; + order.denominator = 8191; + + await expect( + marketplaceContract + .connect(buyer) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) + ).to.be.revertedWith("InexactFraction"); + + orderStatus = await marketplaceContract.getOrderStatus(orderHash); + + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(false, false, 0, 0) + ); + + order.numerator = 1; + order.denominator = 2; + + await withBalanceChecks([order], 0, [], async () => { + const tx = marketplaceContract + .connect(buyer) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -11330,7 +10683,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function buildOrderStatus(true, false, 1, 2) ); }); - it("Reverts on inexact fraction amounts", async () => { + it("Reverts on partial fill attempt when not supported by order", async () => { // Seller mints nft const { nftId, amount } = await mintAndApprove1155( seller, @@ -11351,7 +10704,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function zone, offer, consideration, - 1 // PARTIAL_OPEN + 0 // FULL_OPEN ); let orderStatus = await marketplaceContract.getOrderStatus(orderHash); @@ -11361,15 +10714,21 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function ); order.numerator = 1; - order.denominator = 8191; + order.denominator = 2; await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) - ).to.be.revertedWith("InexactFraction"); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) + ).to.be.revertedWith("PartialFillsNotEnabledForOrder"); orderStatus = await marketplaceContract.getOrderStatus(orderHash); @@ -11378,14 +10737,20 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function ); order.numerator = 1; - order.denominator = 2; + order.denominator = 1; await withBalanceChecks([order], 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -11408,10 +10773,10 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function orderStatus = await marketplaceContract.getOrderStatus(orderHash); expect({ ...orderStatus }).to.deep.equal( - buildOrderStatus(true, false, 1, 2) + buildOrderStatus(true, false, 1, 1) ); }); - it("Reverts on partial fill attempt when not supported by order", async () => { + it("Reverts on partially filled order via basic fulfillment", async () => { // Seller mints nft const { nftId, amount } = await mintAndApprove1155( seller, @@ -11432,7 +10797,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function zone, offer, consideration, - 0 // FULL_OPEN + 1 // PARTIAL_OPEN ); let orderStatus = await marketplaceContract.getOrderStatus(orderHash); @@ -11444,29 +10809,18 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function order.numerator = 1; order.denominator = 2; - await expect( - marketplaceContract - .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) - ).to.be.revertedWith("PartialFillsNotEnabledForOrder"); - - orderStatus = await marketplaceContract.getOrderStatus(orderHash); - - expect({ ...orderStatus }).to.deep.equal( - buildOrderStatus(false, false, 0, 0) - ); - - order.numerator = 1; - order.denominator = 1; - await withBalanceChecks([order], 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -11489,10 +10843,23 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function orderStatus = await marketplaceContract.getOrderStatus(orderHash); expect({ ...orderStatus }).to.deep.equal( - buildOrderStatus(true, false, 1, 1) + buildOrderStatus(true, false, 1, 2) + ); + + const basicOrderParameters = getBasicOrderParameters( + 1, // EthForERC1155 + order ); + + await expect( + marketplaceContract + .connect(buyer) + .fulfillBasicOrder(basicOrderParameters, { + value, + }) + ).to.be.revertedWith(`OrderPartiallyFilled("${orderHash}")`); }); - it("Reverts on partially filled order via basic fulfillment", async () => { + it("Reverts on fully filled order", async () => { // Seller mints nft const { nftId, amount } = await mintAndApprove1155( seller, @@ -11523,14 +10890,20 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function ); order.numerator = 1; - order.denominator = 2; + order.denominator = 1; await withBalanceChecks([order], 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -11553,7 +10926,47 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function orderStatus = await marketplaceContract.getOrderStatus(orderHash); expect({ ...orderStatus }).to.deep.equal( - buildOrderStatus(true, false, 1, 2) + buildOrderStatus(true, false, 1, 1) + ); + + await expect( + marketplaceContract + .connect(buyer) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) + ).to.be.revertedWith(`OrderAlreadyFilled("${orderHash}")`); + }); + it("Reverts on non-zero unused item parameters (identifier set on native, basic)", async () => { + // Seller mints nft + const { nftId, amount } = await mintAndApprove1155( + seller, + marketplaceContract.address, + 10000 + ); + + const offer = [getTestItem1155(nftId, amount.mul(10), amount.mul(10))]; + + const consideration = [ + getItemETH(amount.mul(1000), amount.mul(1000), seller.address), + getItemETH(amount.mul(10), amount.mul(10), zone.address), + getItemETH(amount.mul(20), amount.mul(20), owner.address), + ]; + + consideration[0].identifierOrCriteria = amount; + + const { order, orderHash, value } = await createOrder( + seller, + zone, + offer, + consideration, + 0 // FULL_OPEN ); const basicOrderParameters = getBasicOrderParameters( @@ -11567,9 +10980,106 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function .fulfillBasicOrder(basicOrderParameters, { value, }) - ).to.be.revertedWith(`OrderPartiallyFilled("${orderHash}")`); + ).to.be.revertedWith(`UnusedItemParameters`); + + let orderStatus = await marketplaceContract.getOrderStatus(orderHash); + + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(false, false, 0, 0) + ); }); - it("Reverts on fully filled order", async () => { + it("Reverts on non-zero unused item parameters (identifier set on ERC20, basic)", async () => { + // Seller mints nft + const { nftId, amount } = await mintAndApprove1155( + seller, + marketplaceContract.address, + 10000 + ); + + const offer = [getTestItem1155(nftId, amount.mul(10), amount.mul(10))]; + + const consideration = [ + getTestItem20(amount.mul(1000), amount.mul(1000), seller.address), + getTestItem20(amount.mul(10), amount.mul(10), zone.address), + getTestItem20(amount.mul(20), amount.mul(20), owner.address), + ]; + + consideration[0].identifierOrCriteria = amount; + + const { order, orderHash, value } = await createOrder( + seller, + zone, + offer, + consideration, + 0 // FULL_OPEN + ); + + const basicOrderParameters = getBasicOrderParameters( + 3, // ERC20ForERC1155 + order + ); + + await expect( + marketplaceContract + .connect(buyer) + .fulfillBasicOrder(basicOrderParameters, { + value, + }) + ).to.be.revertedWith(`UnusedItemParameters`); + + let orderStatus = await marketplaceContract.getOrderStatus(orderHash); + + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(false, false, 0, 0) + ); + }); + it("Reverts on non-zero unused item parameters (token set on native, standard)", async () => { + // Seller mints nft + const { nftId, amount } = await mintAndApprove1155( + seller, + marketplaceContract.address, + 10000 + ); + + const offer = [getTestItem1155(nftId, amount.mul(10), amount.mul(10))]; + + const consideration = [ + getItemETH(amount.mul(1000), amount.mul(1000), seller.address), + getItemETH(amount.mul(10), amount.mul(10), zone.address), + getItemETH(amount.mul(20), amount.mul(20), owner.address), + ]; + + consideration[0].token = seller.address; + + const { order, orderHash, value } = await createOrder( + seller, + zone, + offer, + consideration, + 0 // FULL_OPEN + ); + + let orderStatus = await marketplaceContract.getOrderStatus(orderHash); + + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(false, false, 0, 0) + ); + + await expect( + marketplaceContract + .connect(buyer) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) + ).to.be.revertedWith(`UnusedItemParameters`); + }); + it("Reverts on non-zero unused item parameters (identifier set on native, standard)", async () => { // Seller mints nft const { nftId, amount } = await mintAndApprove1155( seller, @@ -11585,12 +11095,60 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function getItemETH(amount.mul(20), amount.mul(20), owner.address), ]; + consideration[0].identifierOrCriteria = amount; + + const { order, orderHash, value } = await createOrder( + seller, + zone, + offer, + consideration, + 0 // FULL_OPEN + ); + + let orderStatus = await marketplaceContract.getOrderStatus(orderHash); + + expect({ ...orderStatus }).to.deep.equal( + buildOrderStatus(false, false, 0, 0) + ); + + await expect( + marketplaceContract + .connect(buyer) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) + ).to.be.revertedWith(`UnusedItemParameters`); + }); + it("Reverts on non-zero unused item parameters (identifier set on ERC20, standard)", async () => { + // Seller mints nft + const { nftId, amount } = await mintAndApprove1155( + seller, + marketplaceContract.address, + 10000 + ); + + const offer = [getTestItem1155(nftId, amount.mul(10), amount.mul(10))]; + + const consideration = [ + getTestItem20(amount.mul(1000), amount.mul(1000), seller.address), + getTestItem20(amount.mul(10), amount.mul(10), zone.address), + getTestItem20(amount.mul(20), amount.mul(20), owner.address), + ]; + + consideration[0].identifierOrCriteria = amount; + const { order, orderHash, value } = await createOrder( seller, zone, offer, consideration, - 1 // PARTIAL_OPEN + 0 // FULL_OPEN ); let orderStatus = await marketplaceContract.getOrderStatus(orderHash); @@ -11599,47 +11157,19 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function buildOrderStatus(false, false, 0, 0) ); - order.numerator = 1; - order.denominator = 1; - - await withBalanceChecks([order], 0, [], async () => { - const tx = marketplaceContract - .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); - const receipt = await (await tx).wait(); - await checkExpectedEvents( - tx, - receipt, - [ - { - order, - orderHash, - fulfiller: buyer.address, - fulfillerConduitKey: toKey(false), - }, - ], - null, - [] - ); - - return receipt; - }); - - orderStatus = await marketplaceContract.getOrderStatus(orderHash); - - expect({ ...orderStatus }).to.deep.equal( - buildOrderStatus(true, false, 1, 1) - ); - await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) - ).to.be.revertedWith(`OrderAlreadyFilled("${orderHash}")`); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) + ).to.be.revertedWith(`UnusedItemParameters`); }); it("Reverts on inadequate consideration items", async () => { // Seller mints nft @@ -11679,9 +11209,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith("MissingOriginalConsiderationItems"); }); it("Reverts on invalid submitter when required by order", async () => { @@ -11800,24 +11336,45 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function order ); + let expectedRevertReason = + getCustomRevertSelector("BadSignatureV(uint8)") + + "1".padStart(64, "0"); + + let tx = await marketplaceContract + .connect(buyer) + .populateTransaction.fulfillBasicOrder(basicOrderParameters, { + value, + }); + let returnData = await provider.call(tx); + expect(returnData).to.equal(expectedRevertReason); + await expect( marketplaceContract .connect(buyer) .fulfillBasicOrder(basicOrderParameters, { value, }) - ).to.be.revertedWith("BadSignatureV(1)"); + ).to.be.reverted; // construct an invalid signature basicOrderParameters.signature = "0x".padEnd(130, "f") + "1c"; + expectedRevertReason = getCustomRevertSelector("InvalidSigner()"); + + tx = await marketplaceContract + .connect(buyer) + .populateTransaction.fulfillBasicOrder(basicOrderParameters, { + value, + }); + expect(provider.call(tx)).to.be.revertedWith("InvalidSigner"); + await expect( marketplaceContract .connect(buyer) .fulfillBasicOrder(basicOrderParameters, { value, }) - ).to.be.revertedWith("InvalidSignature"); + ).to.be.reverted; basicOrderParameters.signature = originalSignature; @@ -12026,11 +11583,21 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function ); if (!process.env.REFERENCE) { + const expectedRevertReason = getCustomRevertSelector( + "BadContractSignature()" + ); + + let tx = await marketplaceContract + .connect(buyer) + .populateTransaction.fulfillBasicOrder(basicOrderParameters); + let returnData = await provider.call(tx); + expect(returnData).to.equal(expectedRevertReason); + await expect( marketplaceContract .connect(buyer) .fulfillBasicOrder(basicOrderParameters) - ).to.be.revertedWith("InvalidSigner"); + ).to.be.reverted; } else { await expect( marketplaceContract @@ -12092,17 +11659,29 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith(`InvalidRestrictedOrder("${orderHash}")`); } else { await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.reverted; } }); @@ -12180,17 +11759,29 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith(`InvalidRestrictedOrder("${orderHash}")`); } else { await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.reverted; } }); @@ -12909,6 +12500,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function offerComponents, considerationComponents, toKey(false), + constants.AddressZero, 100, { value, @@ -12961,6 +12553,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function offerComponents, considerationComponents, toKey(false), + constants.AddressZero, 100, { value, @@ -13013,6 +12606,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function offerComponents, considerationComponents, toKey(false), + constants.AddressZero, 100, { value, @@ -13027,22 +12621,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function marketplaceContract.address ); - const offer = [ - { - itemType: 0, // ETH - token: constants.AddressZero, - identifierOrCriteria: 0, // ignored for ETH - startAmount: parseEther("1"), - endAmount: parseEther("1"), - }, - { - itemType: 2, // ERC721 - token: testERC721.address, - identifierOrCriteria: nftId, - startAmount: toBN(1), - endAmount: toBN(1), - }, - ]; + const offer = [getTestItem721(nftId), getTestItem20(1, 1)]; const consideration = [ getItemETH(parseEther("10"), parseEther("10"), seller.address), @@ -13076,6 +12655,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function offerComponents, considerationComponents, toKey(false), + constants.AddressZero, 100, { value, @@ -13125,6 +12705,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function offerComponents, considerationComponents, toKey(false), + constants.AddressZero, 100, { value, @@ -13179,6 +12760,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function offerComponents, considerationComponents, toKey(false), + constants.AddressZero, 100, { value, @@ -13296,6 +12878,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function offerComponents, considerationComponents, toKey(false), + constants.AddressZero, 100, { value: value.mul(3), @@ -13344,9 +12927,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + criteriaResolvers, + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith("OrderCriteriaResolverOutOfRange"); criteriaResolvers = [ @@ -13356,9 +12945,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + criteriaResolvers, + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith("OfferCriteriaResolverOutOfRange"); criteriaResolvers = [ @@ -13368,9 +12963,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + criteriaResolvers, + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith("ConsiderationCriteriaResolverOutOfRange"); criteriaResolvers = [ @@ -13380,9 +12981,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, criteriaResolvers, async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + criteriaResolvers, + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -13535,9 +13142,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + criteriaResolvers, + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith("UnresolvedConsiderationCriteria"); criteriaResolvers = [ @@ -13547,9 +13160,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + criteriaResolvers, + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith("UnresolvedOfferCriteria"); criteriaResolvers = [ @@ -13560,9 +13179,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, criteriaResolvers, async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + criteriaResolvers, + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -13716,9 +13341,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + criteriaResolvers, + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith("CriteriaNotEnabledForItem"); }); if (process.env.REFERENCE) { @@ -13784,7 +13415,84 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function }); } it("Reverts on offer amount overflow", async () => { - const testERC20Two = await deployContracts(1); + const { testERC20: testERC20Two } = await fixtureERC20(owner); + // Buyer mints nfts + const nftId = await mintAndApprove721( + buyer, + marketplaceContract.address + ); + + await testERC20Two.mint(seller.address, constants.MaxUint256); + // Seller approves marketplace contract to transfer NFTs + await testERC20Two + .connect(seller) + .approve(marketplaceContract.address, constants.MaxUint256); + + const offer = [ + getTestItem20( + constants.MaxUint256, + constants.MaxUint256, + undefined, + testERC20Two.address + ), + getTestItem20( + constants.MaxUint256, + constants.MaxUint256, + undefined, + testERC20Two.address + ), + ]; + + const consideration = [getTestItem721(nftId, 1, 1, seller.address)]; + + const offer2 = [getTestItem721(nftId, 1, 1)]; + const consideration2 = [ + getTestItem20( + constants.MaxUint256, + constants.MaxUint256, + buyer.address, + testERC20Two.address + ), + ]; + + const fulfillments = [ + toFulfillment( + [ + [0, 0], + [0, 1], + ], + [[1, 0]] + ), + toFulfillment([[1, 0]], [[0, 0]]), + ]; + + const { order } = await createOrder( + seller, + zone, + offer, + consideration, + 1 + ); + + const { order: order2 } = await createOrder( + buyer, + zone, + offer2, + consideration2, + 1 + ); + + await expect( + marketplaceContract + .connect(owner) + .matchAdvancedOrders([order, order2], [], fulfillments) + ).to.be.revertedWith( + "panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)" + ); + }); + + it("Reverts on offer amount overflow when another amount is 0", async () => { + const { testERC20: testERC20Two } = await fixtureERC20(owner); // Buyer mints nfts const nftId = await mintAndApprove721( buyer, @@ -13810,6 +13518,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function undefined, testERC20Two.address ), + getTestItem20(0, 0, undefined, testERC20Two.address), ]; const consideration = [getTestItem721(nftId, 1, 1, seller.address)]; @@ -13829,6 +13538,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function [ [0, 0], [0, 1], + [0, 2], ], [[1, 0]] ), @@ -13859,8 +13569,9 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function "panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)" ); }); + it("Reverts on consideration amount overflow", async () => { - const testERC20Two = await deployContracts(1); + const { testERC20: testERC20Two } = await fixtureERC20(owner); // Buyer mints nfts const nftId = await mintAndApprove721( buyer, @@ -13935,6 +13646,88 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function "panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)" ); }); + + it("Reverts on consideration amount overflow when another amount is 0", async () => { + const { testERC20: testERC20Two } = await fixtureERC20(owner); + // Buyer mints nfts + const nftId = await mintAndApprove721( + seller, + marketplaceContract.address + ); + + await testERC20Two.mint(buyer.address, constants.MaxUint256); + // Seller approves marketplace contract to transfer NFTs + await testERC20Two + .connect(buyer) + .approve(marketplaceContract.address, constants.MaxUint256); + + const offer = [getTestItem721(nftId, 1, 1)]; + + const consideration = [ + getTestItem20( + constants.MaxUint256, + constants.MaxUint256, + seller.address, + testERC20Two.address + ), + getTestItem20( + constants.MaxUint256, + constants.MaxUint256, + seller.address, + testERC20Two.address + ), + getTestItem20(0, 0, seller.address, testERC20Two.address), + ]; + + const offer2 = [ + getTestItem20( + constants.MaxUint256, + constants.MaxUint256, + undefined, + testERC20Two.address + ), + ]; + const consideration2 = [getTestItem721(nftId, 1, 1, buyer.address)]; + + const fulfillments = [ + toFulfillment( + [[1, 0]], + [ + [0, 0], + [0, 1], + [0, 2], + ] + ), + toFulfillment([[0, 0]], [[1, 0]]), + ]; + + const { order } = await createOrder( + seller, + zone, + offer, + consideration, + 1 + ); + + const { order: order2 } = await createOrder( + buyer, + zone, + offer2, + consideration2, + 1 + ); + + await expect( + marketplaceContract.matchAdvancedOrders( + [order, order2], + [], + fulfillments + ) + ).to.be.revertedWith( + "panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)" + ); + }); + it("Reverts on invalid criteria proof", async () => { // Seller mints nfts const nftId = randomBN(); @@ -13979,9 +13772,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + criteriaResolvers, + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith("InvalidProof"); criteriaResolvers[0].identifier = @@ -13990,9 +13789,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, criteriaResolvers, async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, criteriaResolvers, toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + criteriaResolvers, + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -14469,31 +14274,17 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function return receipt; }); }); - it("Reverts when not enough ether is supplied as offer item (standard)", async () => { + it("Reverts when not enough ether is supplied as offer item (match)", async () => { // NOTE: this is a ridiculous scenario, buyer is paying the seller's offer - - // buyer mints nft - const nftId = await mintAndApprove721( - buyer, - marketplaceContract.address - ); - const offer = [getItemETH(parseEther("10"), parseEther("10"))]; const consideration = [ - { - itemType: 2, // ERC721 - token: testERC721.address, - identifierOrCriteria: nftId, - startAmount: toBN(1), - endAmount: toBN(1), - recipient: seller.address, - }, + getItemETH(parseEther("1"), parseEther("1"), seller.address), getItemETH(parseEther("1"), parseEther("1"), zone.address), getItemETH(parseEther("1"), parseEther("1"), owner.address), ]; - const { order, orderHash } = await createOrder( + const { order, orderHash, value } = await createOrder( seller, zone, offer, @@ -14501,41 +14292,44 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function 0 // FULL_OPEN ); + const { mirrorOrder, mirrorOrderHash } = await createMirrorBuyNowOrder( + buyer, + zone, + order + ); + + const fulfillments = defaultBuyNowMirrorFulfillment; + + const executions = await simulateMatchOrders( + [order, mirrorOrder], + fulfillments, + owner, + value + ); + + expect(executions.length).to.equal(4); + await expect( - marketplaceContract.connect(buyer).fulfillOrder(order, toKey(false), { - value: toBN(1), - }) + marketplaceContract + .connect(buyer) + .matchOrders([order, mirrorOrder], fulfillments, { + value: toBN(1), + }) ).to.be.revertedWith("InsufficientEtherSupplied"); await expect( - marketplaceContract.connect(buyer).fulfillOrder(order, toKey(false), { - value: parseEther("9.999999"), - }) + marketplaceContract + .connect(buyer) + .matchOrders([order, mirrorOrder], fulfillments, { + value: parseEther("9.999999"), + }) ).to.be.revertedWith("InsufficientEtherSupplied"); - await withBalanceChecks( - [order], - parseEther("10").mul(-1), - null, - async () => { - const tx = marketplaceContract - .connect(buyer) - .fulfillOrder(order, toKey(false), { - value: parseEther("12"), - }); - const receipt = await (await tx).wait(); - await checkExpectedEvents(tx, receipt, [ - { - order, - orderHash, - fulfiller: buyer.address, - fulfillerConduitKey: toKey(false), - }, - ]); - - return receipt; - } - ); + await marketplaceContract + .connect(buyer) + .matchOrders([order, mirrorOrder], fulfillments, { + value: parseEther("13"), + }); }); it("Reverts when not enough ether is supplied (standard + advanced)", async () => { // Seller mints nft @@ -14570,9 +14364,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value: toBN(1), - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value: toBN(1), + } + ) ).to.be.revertedWith("InsufficientEtherSupplied"); orderStatus = await marketplaceContract.getOrderStatus(orderHash); @@ -14584,9 +14384,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value: value.sub(1), - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value: value.sub(1), + } + ) ).to.be.revertedWith("InsufficientEtherSupplied"); orderStatus = await marketplaceContract.getOrderStatus(orderHash); @@ -14599,9 +14405,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value: value.add(1), - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value: value.add(1), + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -14915,9 +14727,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.reverted; // panic code thrown by underlying 721 let orderStatus = await marketplaceContract.getOrderStatus(orderHash); @@ -14938,9 +14756,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -14986,9 +14810,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith("NOT_AUTHORIZED"); }); it("Reverts when 1155 token transfer reverts (via conduit)", async () => { @@ -15017,9 +14847,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith(`NOT_AUTHORIZED`); }); @@ -15090,9 +14926,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function const { order: initialOrder, value } = await setup(); const baseGas = await marketplaceContract .connect(buyer) - .estimateGas.fulfillAdvancedOrder(initialOrder, [], conduitKeyOne, { - value, - }); + .estimateGas.fulfillAdvancedOrder( + initialOrder, + [], + conduitKeyOne, + constants.AddressZero, + { + value, + } + ); // TODO: clean *this* up const { order } = await setup(); @@ -15100,10 +14942,16 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], conduitKeyOne, { - value, - gasLimit: baseGas.add(74000), - }) + .fulfillAdvancedOrder( + order, + [], + conduitKeyOne, + constants.AddressZero, + { + value, + gasLimit: baseGas.add(74000), + } + ) ).to.be.revertedWith("InvalidCallToConduit"); }); } @@ -15149,9 +14997,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith("MissingItemAmount"); }); it("Reverts when ERC20 tokens return falsey values", async () => { @@ -15200,9 +15054,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.reverted; // TODO: hardhat can't find error msg on IR pipeline let orderStatus = await marketplaceContract.getOrderStatus(orderHash); @@ -15218,9 +15078,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -15291,9 +15157,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }); + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -15377,9 +15249,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], conduitKeyOne, { - value, - }) + .fulfillAdvancedOrder( + order, + [], + conduitKeyOne, + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith( `BadReturnValueFromERC20OnTransfer("${testERC20.address}", "${ buyer.address @@ -15389,9 +15267,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], conduitKeyOne, { - value, - }) + .fulfillAdvancedOrder( + order, + [], + conduitKeyOne, + constants.AddressZero, + { + value, + } + ) ).to.be.reverted; } @@ -15406,9 +15290,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], conduitKeyOne, { - value, - }); + .fulfillAdvancedOrder( + order, + [], + conduitKeyOne, + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -15487,7 +15377,7 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], badKey, { + .fulfillAdvancedOrder(order, [], badKey, constants.AddressZero, { value, }) ).to.be.revertedWith("InvalidConduit", badKey, missingConduit); @@ -15501,9 +15391,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await withBalanceChecks([order], 0, [], async () => { const tx = marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], conduitKeyOne, { - value, - }); + .fulfillAdvancedOrder( + order, + [], + conduitKeyOne, + constants.AddressZero, + { + value, + } + ); const receipt = await (await tx).wait(); await checkExpectedEvents( tx, @@ -15700,9 +15596,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.reverted; // TODO: look into the revert reason more thoroughly // Transaction reverted: function returned an unexpected amount of data }); @@ -15724,7 +15626,13 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { value }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { value } + ) ).to.be.revertedWith(`NoContract("${buyer.address}")`); }); it("Reverts when 1155 account with no code is supplied", async () => { @@ -15749,9 +15657,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith(`NoContract("${constants.AddressZero}")`); }); it("Reverts when 1155 account with no code is supplied (via conduit)", async () => { @@ -15781,9 +15695,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith(`NoContract("${constants.AddressZero}")`); }); it("Reverts when non-token account is supplied as the token", async () => { @@ -15815,9 +15735,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith( `TokenTransferGenericFailure("${marketplaceContract.address}", "${ buyer.address @@ -15853,9 +15779,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], conduitKeyOne, { - value, - }) + .fulfillAdvancedOrder( + order, + [], + conduitKeyOne, + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith( `TokenTransferGenericFailure("${marketplaceContract.address}", "${ buyer.address @@ -15885,9 +15817,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.revertedWith( `TokenTransferGenericFailure("${marketplaceContract.address}", "${ seller.address @@ -15897,9 +15835,15 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function await expect( marketplaceContract .connect(buyer) - .fulfillAdvancedOrder(order, [], toKey(false), { - value, - }) + .fulfillAdvancedOrder( + order, + [], + toKey(false), + constants.AddressZero, + { + value, + } + ) ).to.be.reverted; } }); @@ -16221,25 +16165,170 @@ describe(`Consideration (version: ${VERSION}) — initial test suite`, function ).to.be.reverted; } }); - it.skip("Reverts on reentrancy (test all the other permutations)", async () => {}); }); - }); - describe("Auctions for single nft items", async () => { - describe("English auction", async () => {}); - describe("Dutch auction", async () => {}); - }); + describe("ETH offer items", async () => { + let ethAmount; + const tokenAmount = minRandom(100); + let offer; + let consideration; + let seller; + let buyer; - // Is this a thing? - describe("Auctions for mixed item bundles", async () => { - describe("English auction", async () => {}); - describe("Dutch auction", async () => {}); - }); + before(async () => { + ethAmount = parseEther("1"); + seller = await getWalletWithEther(); + buyer = await getWalletWithEther(); + zone = new ethers.Wallet(randomHex(32), provider); + offer = [getItemETH(ethAmount, ethAmount)]; + consideration = [ + getTestItem20(tokenAmount, tokenAmount, seller.address), + ]; + }); - describe("Multiple nfts being sold or bought", async () => { - describe("Bundles", async () => {}); - describe("Partial fills", async () => {}); - }); + it("fulfillOrder reverts if any offer item is ETH", async () => { + const { order, value } = await createOrder( + seller, + zone, + offer, + consideration, + 0 // FULL_OPEN + ); + + await expect( + marketplaceContract + .connect(buyer) + .fulfillOrder(order, toKey(false), { value }) + ).to.be.revertedWith("InvalidNativeOfferItem"); + }); + + it("fulfillAdvancedOrder reverts if any offer item is ETH", async () => { + const { order } = await createOrder( + seller, + zone, + offer, + consideration, + 0 // FULL_OPEN + ); + + await expect( + marketplaceContract + .connect(buyer) + .fulfillAdvancedOrder(order, [], toKey(false), buyer.address, { + value: ethAmount, + }) + ).to.be.revertedWith("InvalidNativeOfferItem"); + }); + + it("fulfillAvailableOrders reverts if any offer item is ETH", async () => { + const { order } = await createOrder( + seller, + zone, + offer, + consideration, + 0 // FULL_OPEN + ); + + await expect( + marketplaceContract + .connect(buyer) + .fulfillAvailableOrders( + [order], + [[[0, 0]]], + [[[0, 0]]], + toKey(false), + 100, + { value: ethAmount } + ) + ).to.be.revertedWith("InvalidNativeOfferItem"); + }); + + it("fulfillAvailableAdvancedOrders reverts if any offer item is ETH", async () => { + const { order } = await createOrder( + seller, + zone, + offer, + consideration, + 0 // FULL_OPEN + ); + + await expect( + marketplaceContract + .connect(buyer) + .fulfillAvailableAdvancedOrders( + [order], + [], + [[[0, 0]]], + [[[0, 0]]], + toKey(false), + buyer.address, + 100, + { value: ethAmount } + ) + ).to.be.revertedWith("InvalidNativeOfferItem"); + }); + + it("matchOrders allows fulfilling with native offer items", async () => { + await mintAndApproveERC20( + buyer, + marketplaceContract.address, + tokenAmount + ); + + const { order } = await createOrder( + seller, + zone, + offer, + consideration, + 0 // FULL_OPEN + ); + const { mirrorOrder } = await createMirrorBuyNowOrder( + buyer, + zone, + order + ); + const fulfillments = [ + toFulfillment([[0, 0]], [[1, 0]]), + toFulfillment([[1, 0]], [[0, 0]]), + ]; + + await marketplaceContract + .connect(owner) + .matchOrders([order, mirrorOrder], fulfillments, { + value: ethAmount, + }); + }); + + it("matchAdvancedOrders allows fulfilling with native offer items", async () => { + await mintAndApproveERC20( + buyer, + marketplaceContract.address, + tokenAmount + ); + + const { order } = await createOrder( + seller, + zone, + offer, + consideration, + 0 // FULL_OPEN + ); + const { mirrorOrder } = await createMirrorBuyNowOrder( + buyer, + zone, + order + ); + const fulfillments = [ + toFulfillment([[0, 0]], [[1, 0]]), + toFulfillment([[1, 0]], [[0, 0]]), + ]; - // Etc this is a brain dump + await marketplaceContract + .connect(owner) + .matchAdvancedOrders([order, mirrorOrder], [], fulfillments, { + value: ethAmount, + }); + }); + }); + }); }); diff --git a/test/utils/contracts.ts b/test/utils/contracts.ts index 071c2c689..8eefbc74e 100644 --- a/test/utils/contracts.ts +++ b/test/utils/contracts.ts @@ -1,24 +1,27 @@ import { ethers } from "hardhat"; import { Contract } from "ethers"; import { JsonRpcSigner } from "@ethersproject/providers"; -import * as dotenv from 'dotenv'; +import * as dotenv from "dotenv"; dotenv.config(); -export async function deployContract( +export const deployContract = async ( name: string, signer: JsonRpcSigner, ...args: any[] -): Promise { +): Promise => { const references = new Map([ - ["Consideration", "ReferenceConsideration"], - ["Conduit", "ReferenceConduit"], - ["ConduitController", "ReferenceConduitController"], - ]); + ["Consideration", "ReferenceConsideration"], + ["Conduit", "ReferenceConduit"], + ["ConduitController", "ReferenceConduitController"], + ]); - const nameWithReference = (process.env.REFERENCE && references.has(name)) ? references.get(name) || name : name; + const nameWithReference = + process.env.REFERENCE && references.has(name) + ? references.get(name) || name + : name; const f = await ethers.getContractFactory(nameWithReference, signer); const c = await f.deploy(...args); return c as C; -} +}; diff --git a/test/utils/criteria.js b/test/utils/criteria.js index 18806f42d..56149f6d5 100644 --- a/test/utils/criteria.js +++ b/test/utils/criteria.js @@ -2,7 +2,7 @@ const { ethers } = require("ethers"); const { bufferToHex, keccak256 } = require("ethereumjs-util"); const merkleTree = (tokenIds) => { - let elements = tokenIds + const elements = tokenIds .map((tokenId) => Buffer.from(tokenId.toHexString().slice(2).padStart(64, "0"), "hex") ) @@ -45,7 +45,7 @@ const getLayers = (elements) => { } const layers = []; - layers.push(elements); + layers.push(elements.map((el) => keccak256(el))); // Get next layer until we reach the root while (layers[layers.length - 1].length > 1) { diff --git a/test/utils/encoding.ts b/test/utils/encoding.ts index b9bec40f7..8bfa67c5a 100644 --- a/test/utils/encoding.ts +++ b/test/utils/encoding.ts @@ -1,7 +1,19 @@ import { randomBytes as nodeRandomBytes } from "crypto"; import { utils, BigNumber, constants, ContractTransaction } from "ethers"; -import { getAddress } from "ethers/lib/utils"; -import { BasicOrderParameters, BigNumberish, Order } from "./types"; +import { getAddress, keccak256, toUtf8Bytes } from "ethers/lib/utils"; +import { + BasicOrderParameters, + BigNumberish, + ConsiderationItem, + CriteriaResolver, + FulfillmentComponent, + OfferItem, + Order, + OrderComponents, +} from "./types"; + +export { BigNumberish }; + const SeededRNG = require("./seeded-rng"); const GAS_REPORT_MODE = process.env.REPORT_GAS; @@ -99,23 +111,45 @@ export const getBasicOrderParameters = ( ], }); -export const getOfferOrConsiderationItem = ( +export const getOfferOrConsiderationItem = < + RecipientType extends string | undefined = undefined +>( itemType: number = 0, token: string = constants.AddressZero, identifierOrCriteria: BigNumberish = 0, startAmount: BigNumberish = 1, endAmount: BigNumberish = 1, - recipient?: string -) => ({ - ...{ + recipient?: RecipientType +): RecipientType extends string ? ConsiderationItem : OfferItem => { + const offerItem: OfferItem = { itemType, token, identifierOrCriteria: toBN(identifierOrCriteria), startAmount: toBN(startAmount), endAmount: toBN(endAmount), - }, - ...(recipient ? { recipient } : {}), -}); + }; + if (typeof recipient === "string") { + return { + ...offerItem, + recipient: recipient as string, + } as ConsiderationItem; + } + return offerItem as any; +}; + +export const buildOrderStatus = ( + ...arr: Array +) => { + const values = arr.map((v) => (typeof v === "number" ? toBN(v) : v)); + return ["isValidated", "isCancelled", "totalFilled", "totalSize"].reduce( + (obj, key, i) => ({ + ...obj, + [key]: values[i], + [i]: values[i], + }), + {} + ); +}; export const getItemETH = ( startAmount: BigNumberish = 1, @@ -146,3 +180,197 @@ export const getItem721 = ( endAmount, recipient ); + +export const toFulfillmentComponents = ( + arr: number[][] +): FulfillmentComponent[] => + arr.map(([orderIndex, itemIndex]) => ({ orderIndex, itemIndex })); + +export const toFulfillment = ( + offerArr: number[][], + considerationsArr: number[][] +): { + offerComponents: FulfillmentComponent[]; + considerationComponents: FulfillmentComponent[]; +} => ({ + offerComponents: toFulfillmentComponents(offerArr), + considerationComponents: toFulfillmentComponents(considerationsArr), +}); + +export const buildResolver = ( + orderIndex: number, + side: 0 | 1, + index: number, + identifier: BigNumber, + criteriaProof: string[] +): CriteriaResolver => ({ + orderIndex, + side, + index, + identifier, + criteriaProof, +}); + +export const calculateOrderHash = (orderComponents: OrderComponents) => { + const offerItemTypeString = + "OfferItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount)"; + const considerationItemTypeString = + "ConsiderationItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount,address recipient)"; + const orderComponentsPartialTypeString = + "OrderComponents(address offerer,address zone,OfferItem[] offer,ConsiderationItem[] consideration,uint8 orderType,uint256 startTime,uint256 endTime,bytes32 zoneHash,uint256 salt,bytes32 conduitKey,uint256 counter)"; + const orderTypeString = `${orderComponentsPartialTypeString}${considerationItemTypeString}${offerItemTypeString}`; + + const offerItemTypeHash = keccak256(toUtf8Bytes(offerItemTypeString)); + const considerationItemTypeHash = keccak256( + toUtf8Bytes(considerationItemTypeString) + ); + const orderTypeHash = keccak256(toUtf8Bytes(orderTypeString)); + + const offerHash = keccak256( + "0x" + + orderComponents.offer + .map((offerItem) => { + return keccak256( + "0x" + + [ + offerItemTypeHash.slice(2), + offerItem.itemType.toString().padStart(64, "0"), + offerItem.token.slice(2).padStart(64, "0"), + toBN(offerItem.identifierOrCriteria) + .toHexString() + .slice(2) + .padStart(64, "0"), + toBN(offerItem.startAmount) + .toHexString() + .slice(2) + .padStart(64, "0"), + toBN(offerItem.endAmount) + .toHexString() + .slice(2) + .padStart(64, "0"), + ].join("") + ).slice(2); + }) + .join("") + ); + + const considerationHash = keccak256( + "0x" + + orderComponents.consideration + .map((considerationItem) => { + return keccak256( + "0x" + + [ + considerationItemTypeHash.slice(2), + considerationItem.itemType.toString().padStart(64, "0"), + considerationItem.token.slice(2).padStart(64, "0"), + toBN(considerationItem.identifierOrCriteria) + .toHexString() + .slice(2) + .padStart(64, "0"), + toBN(considerationItem.startAmount) + .toHexString() + .slice(2) + .padStart(64, "0"), + toBN(considerationItem.endAmount) + .toHexString() + .slice(2) + .padStart(64, "0"), + considerationItem.recipient.slice(2).padStart(64, "0"), + ].join("") + ).slice(2); + }) + .join("") + ); + + const derivedOrderHash = keccak256( + "0x" + + [ + orderTypeHash.slice(2), + orderComponents.offerer.slice(2).padStart(64, "0"), + orderComponents.zone.slice(2).padStart(64, "0"), + offerHash.slice(2), + considerationHash.slice(2), + orderComponents.orderType.toString().padStart(64, "0"), + toBN(orderComponents.startTime) + .toHexString() + .slice(2) + .padStart(64, "0"), + toBN(orderComponents.endTime).toHexString().slice(2).padStart(64, "0"), + orderComponents.zoneHash.slice(2), + orderComponents.salt.slice(2).padStart(64, "0"), + orderComponents.conduitKey.slice(2).padStart(64, "0"), + toBN(orderComponents.counter).toHexString().slice(2).padStart(64, "0"), + ].join("") + ); + + return derivedOrderHash; +}; + +export const getBasicOrderExecutions = ( + order: Order, + fulfiller: string, + fulfillerConduitKey: string +) => { + const { offerer, conduitKey, offer, consideration } = order.parameters; + const offerItem = offer[0]; + const considerationItem = consideration[0]; + const executions = [ + { + item: { + ...offerItem, + amount: offerItem.endAmount, + recipient: fulfiller, + }, + offerer: offerer, + conduitKey: conduitKey, + }, + { + item: { + ...considerationItem, + amount: considerationItem.endAmount, + }, + offerer: fulfiller, + conduitKey: fulfillerConduitKey, + }, + ]; + if (consideration.length > 1) { + for (const additionalRecipient of consideration.slice(1)) { + const execution = { + item: { + ...additionalRecipient, + amount: additionalRecipient.endAmount, + }, + offerer: fulfiller, + conduitKey: fulfillerConduitKey, + }; + if (additionalRecipient.itemType === offerItem.itemType) { + execution.offerer = offerer; + execution.conduitKey = conduitKey; + executions[0].item.amount = executions[0].item.amount.sub( + execution.item.amount + ); + } + executions.push(execution); + } + } + return executions; +}; + +export const defaultBuyNowMirrorFulfillment = [ + [[[0, 0]], [[1, 0]]], + [[[1, 0]], [[0, 0]]], + [[[1, 0]], [[0, 1]]], + [[[1, 0]], [[0, 2]]], +].map(([offerArr, considerationArr]) => + toFulfillment(offerArr, considerationArr) +); + +export const defaultAcceptOfferMirrorFulfillment = [ + [[[1, 0]], [[0, 0]]], + [[[0, 0]], [[1, 0]]], + [[[0, 0]], [[0, 1]]], + [[[0, 0]], [[0, 2]]], +].map(([offerArr, considerationArr]) => + toFulfillment(offerArr, considerationArr) +); diff --git a/test/utils/fixtures.ts b/test/utils/fixtures.ts deleted file mode 100644 index 54042f971..000000000 --- a/test/utils/fixtures.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { JsonRpcSigner } from "@ethersproject/providers"; -import { constants } from "ethers"; -import { deployContract } from "./contracts"; - -export const fixtureERC20 = (signer: JsonRpcSigner) => - deployContract("TestERC20", signer); - -export const fixtureERC721 = (signer: JsonRpcSigner) => - deployContract("TestERC721", signer); - -export const fixtureERC1155 = (signer: JsonRpcSigner) => - deployContract("TestERC1155", signer); - -export const tokensFixture = async (signer: JsonRpcSigner) => { - const testERC20 = await fixtureERC20(signer); - const testERC721 = await fixtureERC721(signer); - const testERC1155 = await fixtureERC1155(signer); - const testERC1155Two = await fixtureERC1155(signer); - const tokenByType = [ - { - address: constants.AddressZero, - }, // ETH - testERC20, - testERC721, - testERC1155, - ]; - - return { - testERC20, - testERC721, - testERC1155, - testERC1155Two, - tokenByType, - }; -}; diff --git a/test/utils/fixtures/conduit.ts b/test/utils/fixtures/conduit.ts new file mode 100644 index 000000000..2c4d3a42e --- /dev/null +++ b/test/utils/fixtures/conduit.ts @@ -0,0 +1,116 @@ +/* eslint-disable camelcase */ +import { expect } from "chai"; +import { constants, Wallet } from "ethers"; +import { getCreate2Address, keccak256 } from "ethers/lib/utils"; +import hre, { ethers } from "hardhat"; +import { + ConduitControllerInterface, + ImmutableCreate2FactoryInterface, +} from "../../../typechain-types"; +import { deployContract } from "../contracts"; +import { randomHex } from "../encoding"; +import { whileImpersonating } from "../impersonate"; + +const deployConstants = require("../../../constants/constants"); + +export const conduitFixture = async ( + create2Factory: ImmutableCreate2FactoryInterface, + owner: Wallet +) => { + let conduitController: ConduitControllerInterface; + let conduitImplementation: any; + if (process.env.REFERENCE) { + conduitImplementation = await ethers.getContractFactory("ReferenceConduit"); + conduitController = await deployContract("ConduitController", owner as any); + } else { + conduitImplementation = await ethers.getContractFactory("Conduit"); + + // Deploy conduit controller through efficient create2 factory + const conduitControllerFactory = await ethers.getContractFactory( + "ConduitController" + ); + + const conduitControllerAddress = await create2Factory.findCreate2Address( + deployConstants.CONDUIT_CONTROLLER_CREATION_SALT, + conduitControllerFactory.bytecode + ); + + let { gasLimit } = await ethers.provider.getBlock("latest"); + + if ((hre as any).__SOLIDITY_COVERAGE_RUNNING) { + gasLimit = ethers.BigNumber.from(300_000_000); + } + await create2Factory.safeCreate2( + deployConstants.CONDUIT_CONTROLLER_CREATION_SALT, + conduitControllerFactory.bytecode, + { + gasLimit, + } + ); + + conduitController = (await ethers.getContractAt( + "ConduitController", + conduitControllerAddress, + owner + )) as any; + } + const conduitCodeHash = keccak256(conduitImplementation.bytecode); + + const conduitKeyOne = `${owner.address}000000000000000000000000`; + + await conduitController.createConduit(conduitKeyOne, owner.address); + + const { conduit: conduitOneAddress, exists } = + await conduitController.getConduit(conduitKeyOne); + + // eslint-disable-next-line no-unused-expressions + expect(exists).to.be.true; + + const conduitOne = conduitImplementation.attach(conduitOneAddress); + + const getTransferSender = (account: string, conduitKey: string) => { + if (!conduitKey || conduitKey === constants.HashZero) { + return account; + } + return getCreate2Address( + conduitController.address, + conduitKey, + conduitCodeHash + ); + }; + + const deployNewConduit = async (owner: Wallet, conduitKey?: string) => { + // Create a conduit key with a random salt + const assignedConduitKey = + conduitKey || owner.address + randomHex(12).slice(2); + + const { conduit: tempConduitAddress } = await conduitController.getConduit( + assignedConduitKey + ); + + await whileImpersonating(owner.address, ethers.provider, async () => { + await expect( + conduitController + .connect(owner) + .createConduit(assignedConduitKey, constants.AddressZero) + ).to.be.revertedWith("InvalidInitialOwner"); + + await conduitController + .connect(owner) + .createConduit(assignedConduitKey, owner.address); + }); + + const tempConduit = conduitImplementation.attach(tempConduitAddress); + return tempConduit; + }; + + return { + conduitController, + conduitImplementation, + conduitCodeHash, + conduitKeyOne, + conduitOne, + getTransferSender, + deployNewConduit, + }; +}; diff --git a/test/utils/fixtures/create2.ts b/test/utils/fixtures/create2.ts new file mode 100644 index 000000000..95c0d0e49 --- /dev/null +++ b/test/utils/fixtures/create2.ts @@ -0,0 +1,73 @@ +import { expect } from "chai"; +import { Wallet } from "ethers"; +import hre, { ethers } from "hardhat"; +import { ImmutableCreate2FactoryInterface } from "../../../typechain-types"; +import { faucet } from "../impersonate"; + +const deployConstants = require("../../../constants/constants"); + +export const create2FactoryFixture = async (owner: Wallet) => { + // Deploy keyless create2 deployer + await faucet( + deployConstants.KEYLESS_CREATE2_DEPLOYER_ADDRESS, + ethers.provider + ); + await ethers.provider.sendTransaction( + deployConstants.KEYLESS_CREATE2_DEPLOYMENT_TRANSACTION + ); + let deployedCode = await ethers.provider.getCode( + deployConstants.KEYLESS_CREATE2_ADDRESS + ); + expect(deployedCode).to.equal(deployConstants.KEYLESS_CREATE2_RUNTIME_CODE); + + let { gasLimit } = await ethers.provider.getBlock("latest"); + + if ((hre as any).__SOLIDITY_COVERAGE_RUNNING) { + gasLimit = ethers.BigNumber.from(300_000_000); + } + + // Deploy inefficient deployer through keyless + await owner.sendTransaction({ + to: deployConstants.KEYLESS_CREATE2_ADDRESS, + data: deployConstants.IMMUTABLE_CREATE2_FACTORY_CREATION_CODE, + gasLimit, + }); + deployedCode = await ethers.provider.getCode( + deployConstants.INEFFICIENT_IMMUTABLE_CREATE2_FACTORY_ADDRESS + ); + expect(ethers.utils.keccak256(deployedCode)).to.equal( + deployConstants.IMMUTABLE_CREATE2_FACTORY_RUNTIME_HASH + ); + + const inefficientFactory = await ethers.getContractAt( + "ImmutableCreate2FactoryInterface", + deployConstants.INEFFICIENT_IMMUTABLE_CREATE2_FACTORY_ADDRESS, + owner + ); + + // Deploy effecient deployer through inefficient deployer + await inefficientFactory + .connect(owner) + .safeCreate2( + deployConstants.IMMUTABLE_CREATE2_FACTORY_SALT, + deployConstants.IMMUTABLE_CREATE2_FACTORY_CREATION_CODE, + { + gasLimit, + } + ); + + deployedCode = await ethers.provider.getCode( + deployConstants.IMMUTABLE_CREATE2_FACTORY_ADDRESS + ); + expect(ethers.utils.keccak256(deployedCode)).to.equal( + deployConstants.IMMUTABLE_CREATE2_FACTORY_RUNTIME_HASH + ); + const create2Factory: ImmutableCreate2FactoryInterface = + await ethers.getContractAt( + "ImmutableCreate2FactoryInterface", + deployConstants.IMMUTABLE_CREATE2_FACTORY_ADDRESS, + owner + ); + + return create2Factory; +}; diff --git a/test/utils/fixtures/index.ts b/test/utils/fixtures/index.ts new file mode 100644 index 000000000..0dad7240c --- /dev/null +++ b/test/utils/fixtures/index.ts @@ -0,0 +1,835 @@ +/* eslint-disable no-unused-expressions */ +import { expect } from "chai"; +import { + BigNumber, + constants, + Contract, + ContractReceipt, + ContractTransaction, + Wallet, +} from "ethers"; +import { ethers } from "hardhat"; +import { deployContract } from "../contracts"; +import { toBN } from "../encoding"; +import { AdvancedOrder, CriteriaResolver } from "../types"; +import { conduitFixture } from "./conduit"; +import { create2FactoryFixture } from "./create2"; +import { marketplaceFixture } from "./marketplace"; +import { tokensFixture } from "./tokens"; + +export { conduitFixture } from "./conduit"; +export { + fixtureERC20, + fixtureERC721, + fixtureERC1155, + tokensFixture, +} from "./tokens"; + +const { provider } = ethers; + +export const seaportFixture = async (owner: Wallet) => { + const EIP1271WalletFactory = await ethers.getContractFactory("EIP1271Wallet"); + const reenterer = await deployContract("Reenterer", owner as any); + const { chainId } = await provider.getNetwork(); + const create2Factory = await create2FactoryFixture(owner); + const { + conduitController, + conduitImplementation, + conduitKeyOne, + conduitOne, + getTransferSender, + deployNewConduit, + } = await conduitFixture(create2Factory, owner); + + const { + testERC20, + mintAndApproveERC20, + getTestItem20, + testERC721, + set721ApprovalForAll, + mint721, + mint721s, + mintAndApprove721, + getTestItem721, + getTestItem721WithCriteria, + testERC1155, + set1155ApprovalForAll, + mint1155, + mintAndApprove1155, + getTestItem1155WithCriteria, + getTestItem1155, + testERC1155Two, + tokenByType, + createTransferWithApproval, + } = await tokensFixture(owner as any); + + const { + marketplaceContract, + directMarketplaceContract, + stubZone, + domainData, + signOrder, + createOrder, + createMirrorBuyNowOrder, + createMirrorAcceptOfferOrder, + } = await marketplaceFixture( + create2Factory, + conduitController, + conduitOne, + chainId, + owner + ); + + const withBalanceChecks = async ( + ordersArray: AdvancedOrder[], // TODO: include order statuses to account for partial fills + additionalPayouts: 0 | BigNumber, + criteriaResolvers: CriteriaResolver[] = [], + fn: () => Promise, + multiplier = 1 + ) => { + const ordersClone: AdvancedOrder[] = JSON.parse( + JSON.stringify(ordersArray as any) + ) as any; + for (const [i, order] of Object.entries(ordersClone) as any as [ + number, + AdvancedOrder + ][]) { + order.parameters.startTime = ordersArray[i].parameters.startTime; + order.parameters.endTime = ordersArray[i].parameters.endTime; + + for (const [j, offerItem] of Object.entries( + order.parameters.offer + ) as any) { + offerItem.startAmount = ordersArray[i].parameters.offer[j].startAmount; + offerItem.endAmount = ordersArray[i].parameters.offer[j].endAmount; + } + + for (const [j, considerationItem] of Object.entries( + order.parameters.consideration + ) as any) { + considerationItem.startAmount = + ordersArray[i].parameters.consideration[j].startAmount; + considerationItem.endAmount = + ordersArray[i].parameters.consideration[j].endAmount; + } + } + + if (criteriaResolvers) { + for (const { orderIndex, side, index, identifier } of criteriaResolvers) { + const itemType = + ordersClone[orderIndex].parameters[ + side === 0 ? "offer" : "consideration" + ][index].itemType; + if (itemType < 4) { + console.error("APPLYING CRITERIA TO NON-CRITERIA-BASED ITEM"); + process.exit(1); + } + + ordersClone[orderIndex].parameters[ + side === 0 ? "offer" : "consideration" + ][index].itemType = itemType - 2; + ordersClone[orderIndex].parameters[ + side === 0 ? "offer" : "consideration" + ][index].identifierOrCriteria = identifier; + } + } + + const allOfferedItems = ordersClone + .map((x) => + x.parameters.offer.map((offerItem) => ({ + ...offerItem, + account: x.parameters.offerer, + numerator: x.numerator, + denominator: x.denominator, + startTime: x.parameters.startTime, + endTime: x.parameters.endTime, + })) + ) + .flat(); + + const allReceivedItems = ordersClone + .map((x) => + x.parameters.consideration.map((considerationItem) => ({ + ...considerationItem, + numerator: x.numerator, + denominator: x.denominator, + startTime: x.parameters.startTime, + endTime: x.parameters.endTime, + })) + ) + .flat(); + + for (const offeredItem of allOfferedItems as any[]) { + if (offeredItem.itemType > 3) { + console.error("CRITERIA ON OFFERED ITEM NOT RESOLVED"); + process.exit(1); + } + + if (offeredItem.itemType === 0) { + // ETH + offeredItem.initialBalance = await provider.getBalance( + offeredItem.account + ); + } else if (offeredItem.itemType === 3) { + // ERC1155 + offeredItem.initialBalance = await tokenByType[ + offeredItem.itemType + ].balanceOf(offeredItem.account, offeredItem.identifierOrCriteria); + } else if (offeredItem.itemType < 4) { + offeredItem.initialBalance = await tokenByType[ + offeredItem.itemType + ].balanceOf(offeredItem.account); + } + + if (offeredItem.itemType === 2) { + // ERC721 + offeredItem.ownsItemBefore = + (await tokenByType[offeredItem.itemType].ownerOf( + offeredItem.identifierOrCriteria + )) === offeredItem.account; + } + } + + for (const receivedItem of allReceivedItems as any[]) { + if (receivedItem.itemType > 3) { + console.error( + "CRITERIA-BASED BALANCE RECEIVED CHECKS NOT IMPLEMENTED YET" + ); + process.exit(1); + } + + if (receivedItem.itemType === 0) { + // ETH + receivedItem.initialBalance = await provider.getBalance( + receivedItem.recipient + ); + } else if (receivedItem.itemType === 3) { + // ERC1155 + receivedItem.initialBalance = await tokenByType[ + receivedItem.itemType + ].balanceOf(receivedItem.recipient, receivedItem.identifierOrCriteria); + } else { + receivedItem.initialBalance = await tokenByType[ + receivedItem.itemType + ].balanceOf(receivedItem.recipient); + } + + if (receivedItem.itemType === 2) { + // ERC721 + receivedItem.ownsItemBefore = + (await tokenByType[receivedItem.itemType].ownerOf( + receivedItem.identifierOrCriteria + )) === receivedItem.recipient; + } + } + + const receipt = await fn(); + + const from = receipt.from; + const gasUsed = receipt.gasUsed; + + for (const offeredItem of allOfferedItems as any[]) { + if (offeredItem.account === from && offeredItem.itemType === 0) { + offeredItem.initialBalance = offeredItem.initialBalance.sub(gasUsed); + } + } + + for (const receivedItem of allReceivedItems as any[]) { + if (receivedItem.recipient === from && receivedItem.itemType === 0) { + receivedItem.initialBalance = receivedItem.initialBalance.sub(gasUsed); + } + } + + for (const offeredItem of allOfferedItems as any[]) { + if (offeredItem.itemType > 3) { + console.error("CRITERIA-BASED BALANCE OFFERED CHECKS NOT MET"); + process.exit(1); + } + + if (offeredItem.itemType === 0) { + // ETH + offeredItem.finalBalance = await provider.getBalance( + offeredItem.account + ); + } else if (offeredItem.itemType === 3) { + // ERC1155 + offeredItem.finalBalance = await tokenByType[ + offeredItem.itemType + ].balanceOf(offeredItem.account, offeredItem.identifierOrCriteria); + } else if (offeredItem.itemType < 3) { + // TODO: criteria-based + offeredItem.finalBalance = await tokenByType[ + offeredItem.itemType + ].balanceOf(offeredItem.account); + } + + if (offeredItem.itemType === 2) { + // ERC721 + offeredItem.ownsItemAfter = + (await tokenByType[offeredItem.itemType].ownerOf( + offeredItem.identifierOrCriteria + )) === offeredItem.account; + } + } + + for (const receivedItem of allReceivedItems as any[]) { + if (receivedItem.itemType > 3) { + console.error("CRITERIA-BASED BALANCE RECEIVED CHECKS NOT MET"); + process.exit(1); + } + + if (receivedItem.itemType === 0) { + // ETH + receivedItem.finalBalance = await provider.getBalance( + receivedItem.recipient + ); + } else if (receivedItem.itemType === 3) { + // ERC1155 + receivedItem.finalBalance = await tokenByType[ + receivedItem.itemType + ].balanceOf(receivedItem.recipient, receivedItem.identifierOrCriteria); + } else { + receivedItem.finalBalance = await tokenByType[ + receivedItem.itemType + ].balanceOf(receivedItem.recipient); + } + + if (receivedItem.itemType === 2) { + // ERC721 + receivedItem.ownsItemAfter = + (await tokenByType[receivedItem.itemType].ownerOf( + receivedItem.identifierOrCriteria + )) === receivedItem.recipient; + } + } + + const { timestamp } = await provider.getBlock(receipt.blockHash); + + for (const offeredItem of allOfferedItems as any[]) { + const duration = toBN(offeredItem.endTime).sub(offeredItem.startTime); + const elapsed = toBN(timestamp).sub(offeredItem.startTime); + const remaining = duration.sub(elapsed); + + if (offeredItem.itemType < 4) { + // TODO: criteria-based + if (!additionalPayouts) { + expect( + offeredItem.initialBalance.sub(offeredItem.finalBalance).toString() + ).to.equal( + toBN(offeredItem.startAmount) + .mul(remaining) + .add(toBN(offeredItem.endAmount).mul(elapsed)) + .div(duration) + .mul(offeredItem.numerator) + .div(offeredItem.denominator) + .mul(multiplier) + .toString() + ); + } else { + expect( + offeredItem.initialBalance.sub(offeredItem.finalBalance).toString() + ).to.equal(additionalPayouts.add(offeredItem.endAmount).toString()); + } + } + + if (offeredItem.itemType === 2) { + // ERC721 + expect(offeredItem.ownsItemBefore).to.equal(true); + expect(offeredItem.ownsItemAfter).to.equal(false); + } + } + + for (const receivedItem of allReceivedItems as any[]) { + const duration = toBN(receivedItem.endTime).sub(receivedItem.startTime); + const elapsed = toBN(timestamp).sub(receivedItem.startTime); + const remaining = duration.sub(elapsed); + + expect( + receivedItem.finalBalance.sub(receivedItem.initialBalance).toString() + ).to.equal( + toBN(receivedItem.startAmount) + .mul(remaining) + .add(toBN(receivedItem.endAmount).mul(elapsed)) + .add(duration.sub(1)) + .div(duration) + .mul(receivedItem.numerator) + .div(receivedItem.denominator) + .mul(multiplier) + .toString() + ); + + if (receivedItem.itemType === 2) { + // ERC721 + expect(receivedItem.ownsItemBefore).to.equal(false); + expect(receivedItem.ownsItemAfter).to.equal(true); + } + } + + return receipt; + }; + + const checkTransferEvent = async ( + tx: any, + item: any, + { offerer, conduitKey, target }: any + ) => { + const { + itemType, + token, + identifier: id1, + identifierOrCriteria: id2, + amount, + recipient, + } = item; + const identifier = id1 || id2; + const sender = getTransferSender(offerer, conduitKey); + if ([1, 2, 5].includes(itemType)) { + const contract = new Contract( + token, + (itemType === 1 ? testERC20 : testERC721).interface, + provider + ); + await expect(tx) + .to.emit(contract, "Transfer") + .withArgs(offerer, recipient, itemType === 1 ? amount : identifier); + } else if ([3, 4].includes(itemType)) { + const contract = new Contract(token, testERC1155.interface, provider); + const operator = sender !== offerer ? sender : target; + await expect(tx) + .to.emit(contract, "TransferSingle") + .withArgs(operator, offerer, recipient, identifier, amount); + } + }; + + const checkExpectedEvents = async ( + tx: Promise, + receipt: ContractReceipt, + orderGroups: Array<{ + order: AdvancedOrder; + orderHash: string; + fulfiller?: string; + fulfillerConduitKey?: string; + recipient?: string; + }>, + standardExecutions: any[] = [], + criteriaResolvers: any[] = [], + shouldSkipAmountComparison = false, + multiplier = 1 + ) => { + const { timestamp } = await provider.getBlock(receipt.blockHash); + + if (standardExecutions && standardExecutions.length) { + for (const standardExecution of standardExecutions) { + const { item, offerer, conduitKey } = standardExecution; + await checkTransferEvent(tx, item, { + offerer, + conduitKey, + target: receipt.to, + }); + } + + // TODO: sum up executions and compare to orders to ensure that all the + // items (or partially-filled items) are accounted for + } + + if (criteriaResolvers && criteriaResolvers.length) { + for (const { orderIndex, side, index, identifier } of criteriaResolvers) { + const itemType = + orderGroups[orderIndex].order.parameters[ + side === 0 ? "offer" : "consideration" + ][index].itemType; + if (itemType < 4) { + console.error("APPLYING CRITERIA TO NON-CRITERIA-BASED ITEM"); + process.exit(1); + } + + orderGroups[orderIndex].order.parameters[ + side === 0 ? "offer" : "consideration" + ][index].itemType = itemType - 2; + orderGroups[orderIndex].order.parameters[ + side === 0 ? "offer" : "consideration" + ][index].identifierOrCriteria = identifier; + } + } + + for (let { + order, + orderHash, + fulfiller, + fulfillerConduitKey, + recipient, + } of orderGroups) { + if (!recipient) { + recipient = fulfiller; + } + const duration = toBN(order.parameters.endTime).sub( + order.parameters.startTime as any + ); + const elapsed = toBN(timestamp).sub(order.parameters.startTime as any); + const remaining = duration.sub(elapsed); + + const marketplaceContractEvents = (receipt.events as any[]) + .filter((x) => x.address === marketplaceContract.address) + .map((x) => ({ + eventName: x.event, + eventSignature: x.eventSignature, + orderHash: x.args.orderHash, + offerer: x.args.offerer, + zone: x.args.zone, + recipient: x.args.recipient, + offer: x.args.offer.map((y: any) => ({ + itemType: y.itemType, + token: y.token, + identifier: y.identifier, + amount: y.amount, + })), + consideration: x.args.consideration.map((y: any) => ({ + itemType: y.itemType, + token: y.token, + identifier: y.identifier, + amount: y.amount, + recipient: y.recipient, + })), + })) + .filter((x) => x.orderHash === orderHash); + + expect(marketplaceContractEvents.length).to.equal(1); + + const event = marketplaceContractEvents[0]; + + expect(event.eventName).to.equal("OrderFulfilled"); + expect(event.eventSignature).to.equal( + "OrderFulfilled(" + + "bytes32,address,address,address,(" + + "uint8,address,uint256,uint256)[],(" + + "uint8,address,uint256,uint256,address)[])" + ); + expect(event.orderHash).to.equal(orderHash); + expect(event.offerer).to.equal(order.parameters.offerer); + expect(event.zone).to.equal(order.parameters.zone); + expect(event.recipient).to.equal(recipient); + + const { offerer, conduitKey, consideration, offer } = order.parameters; + const compareEventItems = async ( + item: any, + orderItem: any, + isConsiderationItem: boolean + ) => { + expect(item.itemType).to.equal( + orderItem.itemType > 3 ? orderItem.itemType - 2 : orderItem.itemType + ); + expect(item.token).to.equal(orderItem.token); + expect(item.token).to.equal(tokenByType[item.itemType].address); + if (orderItem.itemType < 4) { + // no criteria-based + expect(item.identifier).to.equal(orderItem.identifierOrCriteria); + } else { + console.error("CRITERIA-BASED EVENT VALIDATION NOT MET"); + process.exit(1); + } + + if (order.parameters.orderType === 0) { + // FULL_OPEN (no partial fills) + if ( + orderItem.startAmount.toString() === orderItem.endAmount.toString() + ) { + expect(item.amount.toString()).to.equal( + orderItem.endAmount.toString() + ); + } else { + expect(item.amount.toString()).to.equal( + toBN(orderItem.startAmount) + .mul(remaining) + .add(toBN(orderItem.endAmount).mul(elapsed)) + .add(isConsiderationItem ? duration.sub(1) : 0) + .div(duration) + .toString() + ); + } + } else { + if ( + orderItem.startAmount.toString() === orderItem.endAmount.toString() + ) { + expect(item.amount.toString()).to.equal( + orderItem.endAmount + .mul(order.numerator) + .div(order.denominator) + .toString() + ); + } else { + console.error("SLIDING AMOUNT NOT IMPLEMENTED YET"); + process.exit(1); + } + } + }; + + if (!standardExecutions || !standardExecutions.length) { + for (const item of consideration) { + const { startAmount, endAmount } = item; + let amount; + if (order.parameters.orderType === 0) { + amount = startAmount.eq(endAmount) + ? endAmount + : startAmount + .mul(remaining) + .add(endAmount.mul(elapsed)) + .add(duration.sub(1)) + .div(duration); + } else { + amount = endAmount.mul(order.numerator).div(order.denominator); + } + amount = amount.mul(multiplier); + + await checkTransferEvent( + tx, + { ...item, amount }, + { + offerer: receipt.from, + conduitKey: fulfillerConduitKey, + target: receipt.to, + } + ); + } + + for (const item of offer) { + const { startAmount, endAmount } = item; + let amount; + if (order.parameters.orderType === 0) { + amount = startAmount.eq(endAmount) + ? endAmount + : startAmount + .mul(remaining) + .add(endAmount.mul(elapsed)) + .div(duration); + } else { + amount = endAmount.mul(order.numerator).div(order.denominator); + } + amount = amount.mul(multiplier); + + await checkTransferEvent( + tx, + { ...item, amount, recipient }, + { + offerer, + conduitKey, + target: receipt.to, + } + ); + } + } + + expect(event.offer.length).to.equal(order.parameters.offer.length); + for (const [index, offer] of Object.entries(event.offer) as any[]) { + const offerItem = order.parameters.offer[index]; + await compareEventItems(offer, offerItem, false); + + const tokenEvents = receipt.events?.filter( + (x) => x.address === offerItem.token + ); + + if (offer.itemType === 1) { + // ERC20 + // search for transfer + const transferLogs = (tokenEvents || []) + .map((x) => testERC20.interface.parseLog(x)) + .filter( + (x) => + x.signature === "Transfer(address,address,uint256)" && + x.args.from === event.offerer && + (recipient !== constants.AddressZero + ? x.args.to === recipient + : true) + ); + + expect(transferLogs.length).to.be.above(0); + for (const transferLog of transferLogs) { + // TODO: check each transferred amount + } + } else if (offer.itemType === 2) { + // ERC721 + // search for transfer + const transferLogs = (tokenEvents || []) + .map((x) => testERC721.interface.parseLog(x)) + .filter( + (x) => + x.signature === "Transfer(address,address,uint256)" && + x.args.from === event.offerer && + (recipient !== constants.AddressZero + ? x.args.to === recipient + : true) + ); + + expect(transferLogs.length).to.equal(1); + const transferLog = transferLogs[0]; + expect(transferLog.args.id.toString()).to.equal( + offer.identifier.toString() + ); + } else if (offer.itemType === 3) { + // search for transfer + const transferLogs = (tokenEvents || []) + .map((x) => testERC1155.interface.parseLog(x)) + .filter( + (x) => + (x.signature === + "TransferSingle(address,address,address,uint256,uint256)" && + x.args.from === event.offerer && + (fulfiller !== constants.AddressZero + ? x.args.to === fulfiller + : true)) || + (x.signature === + "TransferBatch(address,address,address,uint256[],uint256[])" && + x.args.from === event.offerer && + (fulfiller !== constants.AddressZero + ? x.args.to === fulfiller + : true)) + ); + + expect(transferLogs.length > 0).to.be.true; + + let found = false; + for (const transferLog of transferLogs) { + if ( + transferLog.signature === + "TransferSingle(address,address,address,uint256,uint256)" && + transferLog.args.id.toString() === offer.identifier.toString() && + (shouldSkipAmountComparison || + transferLog.args.amount.toString() === + offer.amount.mul(multiplier).toString()) + ) { + found = true; + break; + } + } + + expect(found).to.be.true; + } + } + + expect(event.consideration.length).to.equal( + order.parameters.consideration.length + ); + for (const [index, consideration] of Object.entries( + event.consideration + ) as any[]) { + const considerationItem = order.parameters.consideration[index]; + await compareEventItems(consideration, considerationItem, true); + expect(consideration.recipient).to.equal(considerationItem.recipient); + + const tokenEvents = receipt.events?.filter( + (x) => x.address === considerationItem.token + ); + + if (consideration.itemType === 1) { + // ERC20 + // search for transfer + const transferLogs = (tokenEvents || []) + .map((x) => testERC20.interface.parseLog(x)) + .filter( + (x) => + x.signature === "Transfer(address,address,uint256)" && + x.args.to === consideration.recipient + ); + + expect(transferLogs.length).to.be.above(0); + for (const transferLog of transferLogs) { + // TODO: check each transferred amount + } + } else if (consideration.itemType === 2) { + // ERC721 + // search for transfer + + const transferLogs = (tokenEvents || []) + .map((x) => testERC721.interface.parseLog(x)) + .filter( + (x) => + x.signature === "Transfer(address,address,uint256)" && + x.args.to === consideration.recipient + ); + + expect(transferLogs.length).to.equal(1); + const transferLog = transferLogs[0]; + expect(transferLog.args.id.toString()).to.equal( + consideration.identifier.toString() + ); + } else if (consideration.itemType === 3) { + // search for transfer + const transferLogs = (tokenEvents || []) + .map((x) => testERC1155.interface.parseLog(x)) + .filter( + (x) => + (x.signature === + "TransferSingle(address,address,address,uint256,uint256)" && + x.args.to === consideration.recipient) || + (x.signature === + "TransferBatch(address,address,address,uint256[],uint256[])" && + x.args.to === consideration.recipient) + ); + + expect(transferLogs.length > 0).to.be.true; + + let found = false; + for (const transferLog of transferLogs) { + if ( + transferLog.signature === + "TransferSingle(address,address,address,uint256,uint256)" && + transferLog.args.id.toString() === + consideration.identifier.toString() && + (shouldSkipAmountComparison || + transferLog.args.amount.toString() === + consideration.amount.mul(multiplier).toString()) + ) { + found = true; + break; + } + } + + expect(found).to.be.true; + } + } + } + }; + + return { + EIP1271WalletFactory, + reenterer, + chainId, + conduitController, + conduitImplementation, + conduitKeyOne, + conduitOne, + getTransferSender, + deployNewConduit, + testERC20, + mintAndApproveERC20, + getTestItem20, + testERC721, + set721ApprovalForAll, + mint721, + mint721s, + mintAndApprove721, + getTestItem721, + getTestItem721WithCriteria, + testERC1155, + set1155ApprovalForAll, + mint1155, + mintAndApprove1155, + getTestItem1155WithCriteria, + getTestItem1155, + testERC1155Two, + tokenByType, + createTransferWithApproval, + marketplaceContract, + directMarketplaceContract, + stubZone, + domainData, + signOrder, + createOrder, + createMirrorBuyNowOrder, + createMirrorAcceptOfferOrder, + withBalanceChecks, + checkTransferEvent, + checkExpectedEvents, + }; +}; + +export type SeaportFixtures = Awaited>; diff --git a/test/utils/fixtures/marketplace.ts b/test/utils/fixtures/marketplace.ts new file mode 100644 index 000000000..fa1527b73 --- /dev/null +++ b/test/utils/fixtures/marketplace.ts @@ -0,0 +1,476 @@ +import { expect } from "chai"; +import { constants, Wallet } from "ethers"; +import { keccak256, recoverAddress } from "ethers/lib/utils"; +import hre, { ethers } from "hardhat"; +import { + ConduitInterface, + ConduitControllerInterface, + ImmutableCreate2FactoryInterface, + ConsiderationInterface, + TestZone, +} from "../../../typechain-types"; +import { deployContract } from "../contracts"; +import { + calculateOrderHash, + convertSignatureToEIP2098, + randomHex, + toBN, +} from "../encoding"; +import { + AdvancedOrder, + ConsiderationItem, + CriteriaResolver, + OfferItem, + OrderComponents, +} from "../types"; + +const { orderType } = require("../../../eip-712-types/order"); +const deployConstants = require("../../../constants/constants"); + +const VERSION = !process.env.REFERENCE ? "1.1" : "rc.1.1"; + +export const marketplaceFixture = async ( + create2Factory: ImmutableCreate2FactoryInterface, + conduitController: ConduitControllerInterface, + conduitOne: ConduitInterface, + chainId: number, + owner: Wallet +) => { + // Deploy marketplace contract through efficient create2 factory + const marketplaceContractFactory = await ethers.getContractFactory( + process.env.REFERENCE ? "ReferenceConsideration" : "Seaport" + ); + + const directMarketplaceContract = await deployContract( + process.env.REFERENCE ? "ReferenceConsideration" : "Consideration", + owner as any, + conduitController.address + ); + + const marketplaceContractAddress = await create2Factory.findCreate2Address( + deployConstants.MARKETPLACE_CONTRACT_CREATION_SALT, + marketplaceContractFactory.bytecode + + conduitController.address.slice(2).padStart(64, "0") + ); + + let { gasLimit } = await ethers.provider.getBlock("latest"); + + if ((hre as any).__SOLIDITY_COVERAGE_RUNNING) { + gasLimit = ethers.BigNumber.from(300_000_000); + } + + await create2Factory.safeCreate2( + deployConstants.MARKETPLACE_CONTRACT_CREATION_SALT, + marketplaceContractFactory.bytecode + + conduitController.address.slice(2).padStart(64, "0"), + { + gasLimit, + } + ); + + const marketplaceContract = (await ethers.getContractAt( + process.env.REFERENCE ? "ReferenceConsideration" : "Seaport", + marketplaceContractAddress, + owner + )) as ConsiderationInterface; + + await conduitController + .connect(owner) + .updateChannel(conduitOne.address, marketplaceContract.address, true); + + const stubZone: TestZone = await deployContract("TestZone", owner as any); + + // Required for EIP712 signing + const domainData = { + name: process.env.REFERENCE ? "Consideration" : "Seaport", + version: VERSION, + chainId: chainId, + verifyingContract: marketplaceContract.address, + }; + + const getAndVerifyOrderHash = async (orderComponents: OrderComponents) => { + const orderHash = await marketplaceContract.getOrderHash( + orderComponents as any + ); + const derivedOrderHash = calculateOrderHash(orderComponents); + expect(orderHash).to.equal(derivedOrderHash); + return orderHash; + }; + + // Returns signature + const signOrder = async ( + orderComponents: OrderComponents, + signer: Wallet + ) => { + const signature = await signer._signTypedData( + domainData, + orderType, + orderComponents + ); + + const orderHash = await getAndVerifyOrderHash(orderComponents); + + const { domainSeparator } = await marketplaceContract.information(); + const digest = keccak256( + `0x1901${domainSeparator.slice(2)}${orderHash.slice(2)}` + ); + const recoveredAddress = recoverAddress(digest, signature); + + expect(recoveredAddress).to.equal(signer.address); + + return signature; + }; + + const createOrder = async ( + offerer: Wallet, + zone: Wallet | undefined | string = undefined, + offer: OfferItem[], + consideration: ConsiderationItem[], + orderType: number, + criteriaResolvers?: CriteriaResolver[], + timeFlag?: string | null, + signer?: Wallet, + zoneHash = constants.HashZero, + conduitKey = constants.HashZero, + extraCheap = false + ) => { + const counter = await marketplaceContract.getCounter(offerer.address); + + const salt = !extraCheap ? randomHex() : constants.HashZero; + const startTime = + timeFlag !== "NOT_STARTED" ? 0 : toBN("0xee00000000000000000000000000"); + const endTime = + timeFlag !== "EXPIRED" ? toBN("0xff00000000000000000000000000") : 1; + + const orderParameters = { + offerer: offerer.address, + zone: !extraCheap + ? (zone as Wallet).address || (zone as string) + : constants.AddressZero, + offer, + consideration, + totalOriginalConsiderationItems: consideration.length, + orderType, + zoneHash, + salt, + conduitKey, + startTime, + endTime, + }; + + const orderComponents = { + ...orderParameters, + counter, + }; + + const orderHash = await getAndVerifyOrderHash(orderComponents); + + const { isValidated, isCancelled, totalFilled, totalSize } = + await marketplaceContract.getOrderStatus(orderHash); + + expect(isCancelled).to.equal(false); + + const orderStatus = { + isValidated, + isCancelled, + totalFilled, + totalSize, + }; + + const flatSig = await signOrder(orderComponents, signer || offerer); + + const order = { + parameters: orderParameters, + signature: !extraCheap ? flatSig : convertSignatureToEIP2098(flatSig), + numerator: 1, // only used for advanced orders + denominator: 1, // only used for advanced orders + extraData: "0x", // only used for advanced orders + }; + + // How much ether (at most) needs to be supplied when fulfilling the order + const value = offer + .map((x) => + x.itemType === 0 + ? x.endAmount.gt(x.startAmount) + ? x.endAmount + : x.startAmount + : toBN(0) + ) + .reduce((a, b) => a.add(b), toBN(0)) + .add( + consideration + .map((x) => + x.itemType === 0 + ? x.endAmount.gt(x.startAmount) + ? x.endAmount + : x.startAmount + : toBN(0) + ) + .reduce((a, b) => a.add(b), toBN(0)) + ); + + return { + order, + orderHash, + value, + orderStatus, + orderComponents, + }; + }; + + const createMirrorBuyNowOrder = async ( + offerer: Wallet, + zone: Wallet, + order: AdvancedOrder, + conduitKey = constants.HashZero + ) => { + const counter = await marketplaceContract.getCounter(offerer.address); + const salt = randomHex(); + const startTime = order.parameters.startTime; + const endTime = order.parameters.endTime; + + const compressedOfferItems = []; + for (const { + itemType, + token, + identifierOrCriteria, + startAmount, + endAmount, + } of order.parameters.offer) { + if ( + !compressedOfferItems + .map((x) => `${x.itemType}+${x.token}+${x.identifierOrCriteria}`) + .includes(`${itemType}+${token}+${identifierOrCriteria}`) + ) { + compressedOfferItems.push({ + itemType, + token, + identifierOrCriteria, + startAmount: startAmount.eq(endAmount) + ? startAmount + : startAmount.sub(1), + endAmount: startAmount.eq(endAmount) ? endAmount : endAmount.sub(1), + }); + } else { + const index = compressedOfferItems + .map((x) => `${x.itemType}+${x.token}+${x.identifierOrCriteria}`) + .indexOf(`${itemType}+${token}+${identifierOrCriteria}`); + + compressedOfferItems[index].startAmount = compressedOfferItems[ + index + ].startAmount.add( + startAmount.eq(endAmount) ? startAmount : startAmount.sub(1) + ); + compressedOfferItems[index].endAmount = compressedOfferItems[ + index + ].endAmount.add( + startAmount.eq(endAmount) ? endAmount : endAmount.sub(1) + ); + } + } + + const compressedConsiderationItems = []; + for (const { + itemType, + token, + identifierOrCriteria, + startAmount, + endAmount, + recipient, + } of order.parameters.consideration) { + if ( + !compressedConsiderationItems + .map((x) => `${x.itemType}+${x.token}+${x.identifierOrCriteria}`) + .includes(`${itemType}+${token}+${identifierOrCriteria}`) + ) { + compressedConsiderationItems.push({ + itemType, + token, + identifierOrCriteria, + startAmount: startAmount.eq(endAmount) + ? startAmount + : startAmount.add(1), + endAmount: startAmount.eq(endAmount) ? endAmount : endAmount.add(1), + recipient, + }); + } else { + const index = compressedConsiderationItems + .map((x) => `${x.itemType}+${x.token}+${x.identifierOrCriteria}`) + .indexOf(`${itemType}+${token}+${identifierOrCriteria}`); + + compressedConsiderationItems[index].startAmount = + compressedConsiderationItems[index].startAmount.add( + startAmount.eq(endAmount) ? startAmount : startAmount.add(1) + ); + compressedConsiderationItems[index].endAmount = + compressedConsiderationItems[index].endAmount.add( + startAmount.eq(endAmount) ? endAmount : endAmount.add(1) + ); + } + } + + const orderParameters = { + offerer: offerer.address, + zone: zone.address, + offer: compressedConsiderationItems.map((x) => ({ ...x })), + consideration: compressedOfferItems.map((x) => ({ + ...x, + recipient: offerer.address, + })), + totalOriginalConsiderationItems: compressedOfferItems.length, + orderType: order.parameters.orderType, // FULL_OPEN + zoneHash: "0x".padEnd(66, "0"), + salt, + conduitKey, + startTime, + endTime, + }; + + const orderComponents = { + ...orderParameters, + counter, + }; + + const flatSig = await signOrder(orderComponents, offerer); + + const mirrorOrderHash = await getAndVerifyOrderHash(orderComponents); + + const mirrorOrder = { + parameters: orderParameters, + signature: flatSig, + numerator: order.numerator, // only used for advanced orders + denominator: order.denominator, // only used for advanced orders + extraData: "0x", // only used for advanced orders + }; + + // How much ether (at most) needs to be supplied when fulfilling the order + const mirrorValue = orderParameters.consideration + .map((x) => + x.itemType === 0 + ? x.endAmount.gt(x.startAmount) + ? x.endAmount + : x.startAmount + : toBN(0) + ) + .reduce((a, b) => a.add(b), toBN(0)); + + return { + mirrorOrder, + mirrorOrderHash, + mirrorValue, + }; + }; + + const createMirrorAcceptOfferOrder = async ( + offerer: Wallet, + zone: Wallet, + order: AdvancedOrder, + criteriaResolvers: CriteriaResolver[] = [], + conduitKey = constants.HashZero + ) => { + const counter = await marketplaceContract.getCounter(offerer.address); + const salt = randomHex(); + const startTime = order.parameters.startTime; + const endTime = order.parameters.endTime; + + const orderParameters = { + offerer: offerer.address, + zone: zone.address, + offer: order.parameters.consideration + .filter((x) => x.itemType !== 1) + .map((x) => ({ + itemType: x.itemType < 4 ? x.itemType : x.itemType - 2, + token: x.token, + identifierOrCriteria: + x.itemType < 4 + ? x.identifierOrCriteria + : criteriaResolvers[0].identifier, + startAmount: x.startAmount, + endAmount: x.endAmount, + })), + consideration: order.parameters.offer.map((x) => ({ + itemType: x.itemType < 4 ? x.itemType : x.itemType - 2, + token: x.token, + identifierOrCriteria: + x.itemType < 4 + ? x.identifierOrCriteria + : criteriaResolvers[0].identifier, + recipient: offerer.address, + startAmount: toBN(x.endAmount).sub( + order.parameters.consideration + .filter( + (i) => + i.itemType < 2 && + i.itemType === x.itemType && + i.token === x.token + ) + .map((i) => i.endAmount) + .reduce((a, b) => a.add(b), toBN(0)) + ), + endAmount: toBN(x.endAmount).sub( + order.parameters.consideration + .filter( + (i) => + i.itemType < 2 && + i.itemType === x.itemType && + i.token === x.token + ) + .map((i) => i.endAmount) + .reduce((a, b) => a.add(b), toBN(0)) + ), + })), + totalOriginalConsiderationItems: order.parameters.offer.length, + orderType: 0, // FULL_OPEN + zoneHash: constants.HashZero, + salt, + conduitKey, + startTime, + endTime, + }; + + const orderComponents = { + ...orderParameters, + counter, + }; + + const flatSig = await signOrder(orderComponents as any, offerer); + + const mirrorOrderHash = await getAndVerifyOrderHash(orderComponents as any); + + const mirrorOrder = { + parameters: orderParameters, + signature: flatSig, + numerator: 1, // only used for advanced orders + denominator: 1, // only used for advanced orders + extraData: "0x", // only used for advanced orders + }; + + // How much ether (at most) needs to be supplied when fulfilling the order + const mirrorValue = orderParameters.consideration + .map((x) => + x.itemType === 0 + ? x.endAmount.gt(x.startAmount) + ? x.endAmount + : x.startAmount + : toBN(0) + ) + .reduce((a, b) => a.add(b), toBN(0)); + + return { + mirrorOrder, + mirrorOrderHash, + mirrorValue, + }; + }; + + return { + marketplaceContract, + directMarketplaceContract, + stubZone, + domainData, + signOrder, + createOrder, + createMirrorBuyNowOrder, + createMirrorAcceptOfferOrder, + }; +}; diff --git a/test/utils/fixtures/tokens.ts b/test/utils/fixtures/tokens.ts new file mode 100644 index 000000000..9fddd4372 --- /dev/null +++ b/test/utils/fixtures/tokens.ts @@ -0,0 +1,301 @@ +/* eslint-disable camelcase */ +import { JsonRpcSigner } from "@ethersproject/providers"; +import { expect } from "chai"; +import { BigNumber, constants, Wallet } from "ethers"; +import { ethers } from "hardhat"; +import { TestERC1155, TestERC20, TestERC721 } from "../../../typechain-types"; +import { deployContract } from "../contracts"; +import { + randomBN, + toBN, + BigNumberish, + getOfferOrConsiderationItem, + random128, +} from "../encoding"; +import { whileImpersonating } from "../impersonate"; + +export const fixtureERC20 = async (signer: JsonRpcSigner) => { + const testERC20: TestERC20 = await deployContract("TestERC20", signer); + + const mintAndApproveERC20 = async ( + signer: Wallet, + spender: string, + tokenAmount: BigNumberish + ) => { + const amount = toBN(tokenAmount); + // Offerer mints ERC20 + await testERC20.mint(signer.address, amount); + + // Offerer approves marketplace contract to tokens + await expect(testERC20.connect(signer).approve(spender, amount)) + .to.emit(testERC20, "Approval") + .withArgs(signer.address, spender, tokenAmount); + }; + + const getTestItem20 = ( + startAmount: BigNumberish = 50, + endAmount: BigNumberish = 50, + recipient?: string, + token = testERC20.address + ) => + getOfferOrConsiderationItem(1, token, 0, startAmount, endAmount, recipient); + + return { + testERC20, + mintAndApproveERC20, + getTestItem20, + }; +}; + +export const fixtureERC721 = async (signer: JsonRpcSigner) => { + const testERC721: TestERC721 = await deployContract("TestERC721", signer); + + const set721ApprovalForAll = ( + signer: Wallet, + spender: string, + approved = true, + contract = testERC721 + ) => { + return expect(contract.connect(signer).setApprovalForAll(spender, approved)) + .to.emit(contract, "ApprovalForAll") + .withArgs(signer.address, spender, approved); + }; + + const mint721 = async (signer: Wallet, id?: BigNumberish) => { + const nftId = id ? toBN(id) : randomBN(); + await testERC721.mint(signer.address, nftId); + return nftId; + }; + + const mint721s = async (signer: Wallet, count: number) => { + const arr = []; + for (let i = 0; i < count; i++) arr.push(await mint721(signer)); + return arr; + }; + + const mintAndApprove721 = async ( + signer: Wallet, + spender: string, + id?: BigNumberish + ) => { + await set721ApprovalForAll(signer, spender, true); + return mint721(signer, id); + }; + + const getTestItem721 = ( + identifierOrCriteria: BigNumberish, + startAmount: BigNumberish = 1, + endAmount: BigNumberish = 1, + recipient?: string, + token = testERC721.address + ) => + getOfferOrConsiderationItem( + 2, + token, + identifierOrCriteria, + startAmount, + endAmount, + recipient + ); + + const getTestItem721WithCriteria = ( + identifierOrCriteria: BigNumberish, + startAmount: BigNumberish = 1, + endAmount: BigNumberish = 1, + recipient?: string + ) => + getOfferOrConsiderationItem( + 4, + testERC721.address, + identifierOrCriteria, + startAmount, + endAmount, + recipient + ); + + return { + testERC721, + set721ApprovalForAll, + mint721, + mint721s, + mintAndApprove721, + getTestItem721, + getTestItem721WithCriteria, + }; +}; + +export const fixtureERC1155 = async (signer: JsonRpcSigner) => { + const testERC1155: TestERC1155 = await deployContract("TestERC1155", signer); + + const set1155ApprovalForAll = ( + signer: Wallet, + spender: string, + approved = true, + token = testERC1155 + ) => { + return expect(token.connect(signer).setApprovalForAll(spender, approved)) + .to.emit(token, "ApprovalForAll") + .withArgs(signer.address, spender, approved); + }; + + const mint1155 = async ( + signer: Wallet, + multiplier = 1, + token = testERC1155, + id?: BigNumberish, + amt?: BigNumberish + ) => { + const nftId = id ? toBN(id) : randomBN(); + const amount = amt ? toBN(amt) : toBN(randomBN(4)); + await token.mint(signer.address, nftId, amount.mul(multiplier)); + return { nftId, amount }; + }; + + const mintAndApprove1155 = async ( + signer: Wallet, + spender: string, + multiplier = 1, + id?: BigNumberish, + amt?: BigNumberish + ) => { + const { nftId, amount } = await mint1155( + signer, + multiplier, + testERC1155, + id, + amt + ); + await set1155ApprovalForAll(signer, spender, true); + return { nftId, amount }; + }; + + const getTestItem1155WithCriteria = ( + identifierOrCriteria: BigNumberish, + startAmount: BigNumberish = 1, + endAmount: BigNumberish = 1, + recipient?: string + ) => + getOfferOrConsiderationItem( + 5, + testERC1155.address, + identifierOrCriteria, + startAmount, + endAmount, + recipient + ); + + const getTestItem1155 = ( + identifierOrCriteria: BigNumberish, + startAmount: BigNumberish, + endAmount: BigNumberish, + token = testERC1155.address, + recipient?: string + ) => + getOfferOrConsiderationItem( + 3, + token, + identifierOrCriteria, + startAmount, + endAmount, + recipient + ); + + return { + testERC1155, + set1155ApprovalForAll, + mint1155, + mintAndApprove1155, + getTestItem1155WithCriteria, + getTestItem1155, + }; +}; + +const minRandom = (min: number) => randomBN(10).add(min); + +export const tokensFixture = async (signer: JsonRpcSigner) => { + const erc20 = await fixtureERC20(signer); + const erc721 = await fixtureERC721(signer); + const erc1155 = await fixtureERC1155(signer); + const { testERC1155: testERC1155Two } = await fixtureERC1155(signer); + const tokenByType = [ + { + address: constants.AddressZero, + } as any, // ETH + erc20.testERC20, + erc721.testERC721, + erc1155.testERC1155, + ]; + const createTransferWithApproval = async ( + contract: TestERC20 | TestERC1155 | TestERC721, + receiver: Wallet, + itemType: 0 | 1 | 2 | 3 | 4 | 5, + approvalAddress: string, + from: string, + to: string + ) => { + let identifier: BigNumber = toBN(0); + let amount: BigNumber = toBN(0); + const token = contract.address; + + switch (itemType) { + case 0: + break; + case 1: // ERC20 + amount = minRandom(100); + await (contract as TestERC20).mint(receiver.address, amount); + + // Receiver approves contract to transfer tokens + await whileImpersonating( + receiver.address, + ethers.provider, + async () => { + await expect( + (contract as TestERC20) + .connect(receiver) + .approve(approvalAddress, amount) + ) + .to.emit(contract, "Approval") + .withArgs(receiver.address, approvalAddress, amount); + } + ); + break; + case 2: // ERC721 + case 4: // ERC721_WITH_CRITERIA + amount = toBN(1); + identifier = randomBN(); + await (contract as TestERC721).mint(receiver.address, identifier); + + // Receiver approves contract to transfer tokens + await erc721.set721ApprovalForAll( + receiver, + approvalAddress, + true, + contract as TestERC721 + ); + break; + case 3: // ERC1155 + case 5: // ERC1155_WITH_CRITERIA + identifier = random128(); + amount = minRandom(1); + await contract.mint(receiver.address, identifier, amount); + + // Receiver approves contract to transfer tokens + await erc1155.set1155ApprovalForAll( + receiver, + approvalAddress, + true, + contract as TestERC1155 + ); + break; + } + return { itemType, token, from, to, identifier, amount }; + }; + return { + ...erc20, + ...erc721, + ...erc1155, + testERC1155Two, + tokenByType, + createTransferWithApproval, + }; +}; diff --git a/test/utils/impersonate.ts b/test/utils/impersonate.ts index 970087996..bd1ccb283 100644 --- a/test/utils/impersonate.ts +++ b/test/utils/impersonate.ts @@ -1,5 +1,7 @@ import { JsonRpcProvider } from "@ethersproject/providers"; import { parseEther } from "@ethersproject/units"; +import { ethers } from "hardhat"; +import { randomHex } from "./encoding"; const TEN_THOUSAND_ETH = parseEther("10000").toHexString().replace("0x0", "0x"); @@ -15,6 +17,12 @@ export const faucet = async (address: string, provider: JsonRpcProvider) => { await provider.send("hardhat_setBalance", [address, TEN_THOUSAND_ETH]); }; +export const getWalletWithEther = async () => { + const wallet = new ethers.Wallet(randomHex(32), ethers.provider); + await faucet(wallet.address, ethers.provider); + return wallet; +}; + export const stopImpersonation = async ( address: string, provider: JsonRpcProvider diff --git a/test/utils/seeded-rng.js b/test/utils/seeded-rng.js index 5d8994ff9..927790cfc 100644 --- a/test/utils/seeded-rng.js +++ b/test/utils/seeded-rng.js @@ -27,7 +27,7 @@ for JavaScript. It is driven by 1536 bits of entropy, stored in an array of 48, 32-bit JavaScript variables. Since many applications of this generator, including ours with the "Off The Grid" Latin Square generator, may require - the deteriministic re-generation of a sequence of PRNs, this PRNG's initial + the deterministic re-generation of a sequence of PRNs, this PRNG's initial entropic state can be read and written as a static whole, and incrementally evolved by pouring new source entropy into the generator's internal state. ---------------------------------------------------------------------------- diff --git a/test/utils/types.ts b/test/utils/types.ts index e9276b1ca..898286ae5 100644 --- a/test/utils/types.ts +++ b/test/utils/types.ts @@ -7,6 +7,19 @@ export type AdditionalRecipient = { recipient: string; }; +export type FulfillmentComponent = { + orderIndex: number; + itemIndex: number; +}; + +export type CriteriaResolver = { + orderIndex: number; + side: 0 | 1; + index: number; + identifier: BigNumber; + criteriaProof: string[]; +}; + export type BasicOrderParameters = { considerationToken: string; considerationIdentifier: BigNumber; @@ -17,16 +30,17 @@ export type BasicOrderParameters = { offerIdentifier: BigNumber; offerAmount: BigNumber; basicOrderType: number; - startTime: BigNumber; - endTime: BigNumber; + startTime: string | BigNumber | number; + endTime: string | BigNumber | number; zoneHash: string; - salt: BigNumber; + salt: string; offererConduitKey: string; fulfillerConduitKey: string; totalOriginalAdditionalRecipients: BigNumber; additionalRecipients: AdditionalRecipient[]; signature: string; }; + export type OfferItem = { itemType: number; token: string; @@ -43,34 +57,36 @@ export type ConsiderationItem = { recipient: string; }; -export type OrderComponents = { - offerer: string; - zone: string; - offer: OfferItem[]; - consideration: ConsiderationItem[]; - orderType: number; - startTime: BigNumber; - endTime: BigNumber; - zoneHash: string; - salt: BigNumber; - nonce: BigNumber; -}; - export type OrderParameters = { offerer: string; zone: string; offer: OfferItem[]; consideration: ConsiderationItem[]; orderType: number; - startTime: BigNumber; - endTime: BigNumber; + startTime: string | BigNumber | number; + endTime: string | BigNumber | number; zoneHash: string; - salt: BigNumber; + salt: string; conduitKey: string; - totalOriginalConsiderationItems: BigNumber; + totalOriginalConsiderationItems: string | BigNumber | number; +}; + +export type OrderComponents = Omit< + OrderParameters, + "totalOriginalConsiderationItems" +> & { + counter: BigNumber; }; export type Order = { parameters: OrderParameters; signature: string; }; + +export type AdvancedOrder = { + parameters: OrderParameters; + numerator: string | BigNumber | number; + denominator: string | BigNumber | number; + signature: string; + extraData: string; +}; diff --git a/yarn.lock b/yarn.lock index 1a359d41f..c8722159b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2336,6 +2336,13 @@ bufferutil@^4.0.1: dependencies: node-gyp-build "^4.3.0" +builtins@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9" + integrity sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ== + dependencies: + semver "^7.0.0" + bytes@3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" @@ -3497,10 +3504,10 @@ eslint-config-prettier@^8.3.0: resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz" integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== -eslint-config-standard@^16.0.3: - version "16.0.3" - resolved "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz" - integrity sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg== +eslint-config-standard@^17.0.0: + version "17.0.0" + resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-17.0.0.tgz#fd5b6cf1dcf6ba8d29f200c461de2e19069888cf" + integrity sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg== eslint-import-resolver-node@^0.3.6: version "0.3.6" @@ -3518,10 +3525,10 @@ eslint-module-utils@^2.7.3: debug "^3.2.7" find-up "^2.1.0" -eslint-plugin-es@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz" - integrity sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ== +eslint-plugin-es@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz#f0822f0c18a535a97c3e714e89f88586a7641ec9" + integrity sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ== dependencies: eslint-utils "^2.0.0" regexpp "^3.0.0" @@ -3545,17 +3552,19 @@ eslint-plugin-import@^2.25.4: resolve "^1.22.0" tsconfig-paths "^3.14.1" -eslint-plugin-node@^11.1.0: - version "11.1.0" - resolved "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz" - integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g== +eslint-plugin-n@^15.2.0: + version "15.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-15.2.1.tgz#e62cf800da076ac5a970eb7554efa2136ebfa194" + integrity sha512-uMG50pvKqXK9ab163bSI5OpyZR0F5yIB0pEC4ciGpBLrXVjVDOlx5oTq8GQULWzbelJt7wL5Rw4T+FfAff5Cxg== dependencies: - eslint-plugin-es "^3.0.0" - eslint-utils "^2.0.0" + builtins "^5.0.1" + eslint-plugin-es "^4.1.0" + eslint-utils "^3.0.0" ignore "^5.1.1" - minimatch "^3.0.4" + is-core-module "^2.9.0" + minimatch "^3.1.2" resolve "^1.10.1" - semver "^6.1.0" + semver "^7.3.7" eslint-plugin-prettier@^4.0.0: version "4.0.0" @@ -5613,7 +5622,7 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-core-module@^2.8.1: +is-core-module@^2.8.1, is-core-module@^2.9.0: version "2.9.0" resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz" integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== @@ -8351,6 +8360,11 @@ scryptsy@^1.2.1: dependencies: pbkdf2 "^3.0.3" +scuffed-abi@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/scuffed-abi/-/scuffed-abi-1.0.4.tgz#bc88129877856de5026d8afaea49de9a1972b6cf" + integrity sha512-1NN2L1j+TMF6+/J2jHcAnhPH8Lwaqu5dlgknZPqejEVFQ8+cvcnXYNbaHtGEXTjSNrQLBGePXicD4oFGqecOnQ== + secp256k1@^4.0.1: version "4.0.3" resolved "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz" @@ -8380,12 +8394,12 @@ semaphore@>=1.0.1, semaphore@^1.0.3, semaphore@^1.1.0: resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@^6.1.0, semver@^6.3.0: +semver@^6.3.0: version "6.3.0" resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.4, semver@^7.3.5: +semver@^7.0.0, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: version "7.3.7" resolved "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz" integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==