diff --git a/src/BaseMarketConfig.sol b/src/BaseMarketConfig.sol index ecd6ba5..a6e59fb 100644 --- a/src/BaseMarketConfig.sol +++ b/src/BaseMarketConfig.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.7; -import { SetupCall, TestOrderPayload, TestOrderContext, TestCallParameters, TestItem20, TestItem721, TestItem1155 } from "./Types.sol"; +import "./Types.sol"; abstract contract BaseMarketConfig { /** @@ -101,7 +101,6 @@ abstract contract BaseMarketConfig { uint256 /* ethAmount */ ) external - view virtual returns ( TestOrderPayload memory /* execution */ @@ -194,7 +193,6 @@ abstract contract BaseMarketConfig { TestItem20 calldata /* erc20 */ ) external - view virtual returns ( TestOrderPayload memory /* execution */ @@ -278,7 +276,6 @@ abstract contract BaseMarketConfig { TestItem1155 calldata /* nft */ ) external - view virtual returns ( TestOrderPayload memory /* execution */ @@ -354,7 +351,6 @@ abstract contract BaseMarketConfig { uint256 /* feeEthAmount */ ) external - view virtual returns ( TestOrderPayload memory /* execution */ @@ -386,7 +382,6 @@ abstract contract BaseMarketConfig { uint256 /* feeEthAmount2 */ ) external - view virtual returns ( TestOrderPayload memory /* execution */ @@ -418,6 +413,67 @@ abstract contract BaseMarketConfig { _notImplemented(); } + /** + * @dev Get call parameters to execute an order selling many 721 tokens for Ether. + * If `context.listOnChain` is true and marketplace does not support on-chain + * listing, this function must revert with NotImplemented. + * Each token is priced individually as royalties can exist per individual NFT + * and context is lost when pricing the entire bundle. + * @param context Order context, including the buyer and seller and whether the + * order should be listed on chain. + * @param nfts Array of Address and ID for ERC721 tokens to be sold. + * @param ethAmounts Array of Ether amounts to be received for each NFT. + */ + function getPayload_BuyOfferedManyERC721WithEtherItemsPricedIndividually( + TestOrderContext calldata context, + TestItem721[] calldata nfts, + uint256[] calldata ethAmounts + ) external virtual returns (TestOrderPayload memory execution) { + _notImplemented(); + } + + /** + * @dev Get call parameters to execute an order selling many 721 tokens for Ether with one fee recipient. + * If `context.listOnChain` is true and marketplace does not support on-chain + * listing, this function must revert with NotImplemented. + * Each token is priced individually as royalties can exist per individual NFT + * and context is lost when pricing the entire bundle. + * @param args Struct containing the data needed to create the payload: + * args.context Order context, including the buyer and seller and whether the + * order should be listed on chain. + * args.nfts Array of Address and ID for ERC721 tokens to be sold. + * args.ethAmounts Array of Ether amounts to be received for each NFT. + * args.feeRecipient Address to send fee to. + * args.feeEthAmounts Amount of Ether to send for fee. + */ + function getPayload_BuyOfferedManyERC721WithEtherItemsPricedIndividuallyOneFeeRecipient( + TestBundleOrderWithSingleFeeReceiver memory args + ) external virtual returns (TestOrderPayload memory execution) { + _notImplemented(); + } + + /** + * @dev Get call parameters to execute an order selling many 721 tokens for Ether with one fee recipient. + * If `context.listOnChain` is true and marketplace does not support on-chain + * listing, this function must revert with NotImplemented. + * Each token is priced individually as royalties can exist per individual NFT + * and context is lost when pricing the entire bundle. + * @param args Struct containing the data needed to create the payload: + * args.context Order context, including the buyer and seller and whether the + * order should be listed on chain. + * args.nfts Array of Address and ID for ERC721 tokens to be sold. + * args.ethAmounts Array of Ether amounts to be received for each NFT. + * args.feeRecipient1 Address to send fee 1 to. + * args.feeEthAmount1 Amount of Ether to send for fee 1. + * args.feeRecipient2 Address to send fee 2 to. + * args.feeEthAmount2 Amount of Ether to send for fee 2. + */ + function getPayload_BuyOfferedManyERC721WithEtherItemsPricedIndividuallyTwoFeeRecipients( + TestBundleOrderWithTwoFeeReceivers memory args + ) external virtual returns (TestOrderPayload memory execution) { + _notImplemented(); + } + /** * @dev Get call parameters to execute an order "sweeping the floor" buy filling 10 distinct * ERC-721->ETH orders at once. Same seller on each order. If the market does not support the @@ -432,7 +488,6 @@ abstract contract BaseMarketConfig { uint256[] calldata /* ethAmounts */ ) external - view virtual returns ( TestOrderPayload memory /* execution */ @@ -451,18 +506,11 @@ abstract contract BaseMarketConfig { * @ param erc20Amounts Array of Erc20 amounts to be received for the NFTs in each order */ function getPayload_BuyOfferedManyERC721WithErc20DistinctOrders( - TestOrderContext[] calldata, /* contexts */ - address, /* erc20Address */ - TestItem721[] calldata, /* nfts */ - uint256[] calldata /* erc20Amounts */ - ) - external - view - virtual - returns ( - TestOrderPayload memory /* execution */ - ) - { + TestOrderContext[] calldata contexts, + address erc20Address, + TestItem721[] calldata nfts, + uint256[] calldata erc20Amounts + ) external virtual returns (TestOrderPayload memory execution) { _notImplemented(); } @@ -476,18 +524,11 @@ abstract contract BaseMarketConfig { * @ param erc20Amounts Array of WETH amounts to be received for the NFTs in each order */ function getPayload_BuyOfferedManyERC721WithWETHDistinctOrders( - TestOrderContext[] calldata, /* contexts */ - address, /* erc20Address */ - TestItem721[] calldata, /* nfts */ - uint256[] calldata /* erc20Amounts */ - ) - external - view - virtual - returns ( - TestOrderPayload memory /* execution */ - ) - { + TestOrderContext[] calldata contexts, + address erc20Address, + TestItem721[] calldata nfts, + uint256[] calldata erc20Amounts + ) external virtual returns (TestOrderPayload memory execution) { _notImplemented(); } diff --git a/src/Types.sol b/src/Types.sol index 0ae61b3..3926cb6 100644 --- a/src/Types.sol +++ b/src/Types.sol @@ -41,3 +41,21 @@ struct TestOrderPayload { // Call needed to actually execute the order TestCallParameters executeOrder; } + +struct TestBundleOrderWithSingleFeeReceiver { + TestOrderContext context; + TestItem721[] nfts; + uint256[] itemPrices; + address feeRecipient; + uint256 feeRate; +} + +struct TestBundleOrderWithTwoFeeReceivers { + TestOrderContext context; + TestItem721[] nfts; + uint256[] itemPrices; + address feeRecipient1; + uint256 feeRate1; + address feeRecipient2; + uint256 feeRate2; +} diff --git a/src/marketplaces/lb-payment-processor/PaymentProcessorConfig.sol b/src/marketplaces/lb-payment-processor/PaymentProcessorConfig.sol new file mode 100644 index 0000000..b1c3ff3 --- /dev/null +++ b/src/marketplaces/lb-payment-processor/PaymentProcessorConfig.sol @@ -0,0 +1,1924 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.7; + +import { BaseMarketConfig } from "../../BaseMarketConfig.sol"; +import "../../Types.sol"; +import { IPaymentProcessor } from "./interfaces/IPaymentProcessor.sol"; +import "./interfaces/PaymentProcessorDataTypes.sol"; +import "forge-std/Test.sol"; +import { ECDSA } from "./lib/ECDSA.sol"; +import { TestERC721 } from "test/tokens/TestERC721.sol"; + +contract PaymentProcessorConfig is BaseMarketConfig, Test { + /// @notice keccack256("OfferApproval(uint8 protocol,address marketplace,uint256 marketplaceFeeNumerator,address delegatedPurchaser,address buyer,address tokenAddress,uint256 tokenId,uint256 amount,uint256 price,uint256 expiration,uint256 nonce,uint256 masterNonce,address coin)") + bytes32 public constant OFFER_APPROVAL_HASH = + 0x2008a1ab898fdaa2d8f178bc39e807035d2d6e330dac5e42e913ca727ab56038; + + /// @notice keccack256("CollectionOfferApproval(uint8 protocol,bool collectionLevelOffer,address marketplace,uint256 marketplaceFeeNumerator,address delegatedPurchaser,address buyer,address tokenAddress,uint256 amount,uint256 price,uint256 expiration,uint256 nonce,uint256 masterNonce,address coin)") + bytes32 public constant COLLECTION_OFFER_APPROVAL_HASH = + 0x0bc3075778b80a2341ce445063e81924b88d61eb5f21c815e8f9cc824af096d0; + + /// @notice keccack256("BundledOfferApproval(uint8 protocol,address marketplace,uint256 marketplaceFeeNumerator,address delegatedPurchaser,address buyer,address tokenAddress,uint256 price,uint256 expiration,uint256 nonce,uint256 masterNonce,address coin,uint256[] tokenIds,uint256[] amounts,uint256[] itemSalePrices)") + bytes32 public constant BUNDLED_OFFER_APPROVAL_HASH = + 0x126520d0bca0cfa7e5852d004cc4335723ce67c638cbd55cd530fe992a089e7b; + + /// @notice keccack256("SaleApproval(uint8 protocol,bool sellerAcceptedOffer,address marketplace,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,address privateBuyer,address seller,address tokenAddress,uint256 tokenId,uint256 amount,uint256 minPrice,uint256 expiration,uint256 nonce,uint256 masterNonce,address coin)") + bytes32 public constant SALE_APPROVAL_HASH = + 0xd3f4273db8ff5262b6bc5f6ee07d139463b4f826cce90c05165f63062f3686dc; + + /// @notice keccack256("BundledSaleApproval(uint8 protocol,address marketplace,uint256 marketplaceFeeNumerator,address privateBuyer,address seller,address tokenAddress,uint256 expiration,uint256 nonce,uint256 masterNonce,address coin,uint256[] tokenIds,uint256[] amounts,uint256[] maxRoyaltyFeeNumerators,uint256[] itemPrices)") + bytes32 public constant BUNDLED_SALE_APPROVAL_HASH = + 0x80244acca7a02d7199149a3038653fc8cb10ca984341ec429a626fab631e1662; + + uint256 internal securityPolicyId; + + IPaymentProcessor paymentProcessor = + IPaymentProcessor(address(0x009a1dC629242961C9E4f089b437aFD394474cc0)); + mapping(address => uint256) internal _nonces; + + function name() external pure override returns (string memory) { + return "Payment Processor"; + } + + function market() public view override returns (address) { + return address(paymentProcessor); + } + + function beforeAllPrepareMarketplace(address, address) external override { + buyerNftApprovalTarget = sellerNftApprovalTarget = buyerErc20ApprovalTarget = sellerErc20ApprovalTarget = address( + paymentProcessor + ); + + securityPolicyId = paymentProcessor.createSecurityPolicy( + false, + true, + false, + false, + false, + false, + false, + 2300, + "TEST POLICY" + ); + } + + function getPayload_BuyOfferedERC721WithEther( + TestOrderContext calldata context, + TestItem721 calldata nft, + uint256 ethAmount + ) external override returns (TestOrderPayload memory execution) { + if (context.listOnChain) { + _notImplemented(); + } + + address alice = context.offerer; + address bob = context.fulfiller; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(bob)); + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: false, + collectionLevelOffer: false, + protocol: TokenProtocols.ERC721, + paymentCoin: address(0), + tokenAddress: nft.token, + seller: alice, + privateBuyer: address(0), + buyer: bob, + delegatedPurchaser: address(0), + marketplace: address(0), + marketplaceFeeNumerator: 0, + maxRoyaltyFeeNumerator: 0, + listingNonce: _getNextNonce(alice), + offerNonce: _getNextNonce(bob), + listingMinPrice: ethAmount, + offerPrice: ethAmount, + listingExpiration: type(uint256).max, + offerExpiration: type(uint256).max, + tokenId: nft.identifier, + amount: 1 + }); + + SignatureECDSA memory signedListing = _getSignedListing( + alice, + saleDetails + ); + SignatureECDSA memory signedOffer = _getSignedOffer(bob, saleDetails); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.buySingleListing.selector, + saleDetails, + signedListing, + signedOffer + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + ethAmount, + payload + ); + } + + function getPayload_BuyOfferedERC1155WithEther( + TestOrderContext calldata context, + TestItem1155 calldata nft, + uint256 ethAmount + ) external override returns (TestOrderPayload memory execution) { + if (context.listOnChain) { + _notImplemented(); + } + + address alice = context.offerer; + address bob = context.fulfiller; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(bob)); + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: false, + collectionLevelOffer: false, + protocol: TokenProtocols.ERC1155, + paymentCoin: address(0), + tokenAddress: nft.token, + seller: alice, + privateBuyer: address(0), + buyer: bob, + delegatedPurchaser: address(0), + marketplace: address(0), + marketplaceFeeNumerator: 0, + maxRoyaltyFeeNumerator: 0, + listingNonce: _getNextNonce(alice), + offerNonce: _getNextNonce(bob), + listingMinPrice: ethAmount, + offerPrice: ethAmount, + listingExpiration: type(uint256).max, + offerExpiration: type(uint256).max, + tokenId: nft.identifier, + amount: nft.amount + }); + + SignatureECDSA memory signedListing = _getSignedListing( + alice, + saleDetails + ); + SignatureECDSA memory signedOffer = _getSignedOffer(bob, saleDetails); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.buySingleListing.selector, + saleDetails, + signedListing, + signedOffer + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + ethAmount, + payload + ); + } + + function getPayload_BuyOfferedERC721WithERC20( + TestOrderContext calldata context, + TestItem721 calldata nft, + TestItem20 calldata erc20 + ) external override returns (TestOrderPayload memory execution) { + if (context.listOnChain) { + _notImplemented(); + } + + vm.prank(nft.token); + paymentProcessor.setCollectionSecurityPolicy( + nft.token, + securityPolicyId + ); + + if ( + !paymentProcessor.isPaymentMethodApproved( + securityPolicyId, + erc20.token + ) + ) { + paymentProcessor.whitelistPaymentMethod( + securityPolicyId, + erc20.token + ); + } + + address alice = context.offerer; + address bob = context.fulfiller; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(bob)); + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: false, + collectionLevelOffer: false, + protocol: TokenProtocols.ERC721, + paymentCoin: erc20.token, + tokenAddress: nft.token, + seller: alice, + privateBuyer: address(0), + buyer: bob, + delegatedPurchaser: address(0), + marketplace: address(0), + marketplaceFeeNumerator: 0, + maxRoyaltyFeeNumerator: 0, + listingNonce: _getNextNonce(alice), + offerNonce: _getNextNonce(bob), + listingMinPrice: erc20.amount, + offerPrice: erc20.amount, + listingExpiration: type(uint256).max, + offerExpiration: type(uint256).max, + tokenId: nft.identifier, + amount: 1 + }); + + SignatureECDSA memory signedListing = _getSignedListing( + alice, + saleDetails + ); + SignatureECDSA memory signedOffer = _getSignedOffer(bob, saleDetails); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.buySingleListing.selector, + saleDetails, + signedListing, + signedOffer + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + 0, + payload + ); + } + + function getPayload_BuyOfferedERC721WithWETH( + TestOrderContext calldata context, + TestItem721 memory nft, + TestItem20 memory erc20 + ) external override returns (TestOrderPayload memory execution) { + if (context.listOnChain) { + _notImplemented(); + } + + vm.prank(nft.token); + paymentProcessor.setCollectionSecurityPolicy( + nft.token, + securityPolicyId + ); + + if ( + !paymentProcessor.isPaymentMethodApproved( + securityPolicyId, + erc20.token + ) + ) { + paymentProcessor.whitelistPaymentMethod( + securityPolicyId, + erc20.token + ); + } + + address alice = context.offerer; + address bob = context.fulfiller; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(bob)); + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: false, + collectionLevelOffer: false, + protocol: TokenProtocols.ERC721, + paymentCoin: erc20.token, + tokenAddress: nft.token, + seller: alice, + privateBuyer: address(0), + buyer: bob, + delegatedPurchaser: address(0), + marketplace: address(0), + marketplaceFeeNumerator: 0, + maxRoyaltyFeeNumerator: 0, + listingNonce: _getNextNonce(alice), + offerNonce: _getNextNonce(bob), + listingMinPrice: erc20.amount, + offerPrice: erc20.amount, + listingExpiration: type(uint256).max, + offerExpiration: type(uint256).max, + tokenId: nft.identifier, + amount: 1 + }); + + SignatureECDSA memory signedListing = _getSignedListing( + alice, + saleDetails + ); + SignatureECDSA memory signedOffer = _getSignedOffer(bob, saleDetails); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.buySingleListing.selector, + saleDetails, + signedListing, + signedOffer + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + 0, + payload + ); + } + + function getPayload_BuyOfferedERC1155WithERC20( + TestOrderContext calldata context, + TestItem1155 calldata nft, + TestItem20 memory erc20 + ) external override returns (TestOrderPayload memory execution) { + if (context.listOnChain) { + _notImplemented(); + } + + vm.prank(nft.token); + paymentProcessor.setCollectionSecurityPolicy( + nft.token, + securityPolicyId + ); + + if ( + !paymentProcessor.isPaymentMethodApproved( + securityPolicyId, + erc20.token + ) + ) { + paymentProcessor.whitelistPaymentMethod( + securityPolicyId, + erc20.token + ); + } + + address alice = context.offerer; + address bob = context.fulfiller; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(bob)); + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: false, + collectionLevelOffer: false, + protocol: TokenProtocols.ERC1155, + paymentCoin: erc20.token, + tokenAddress: nft.token, + seller: alice, + privateBuyer: address(0), + buyer: bob, + delegatedPurchaser: address(0), + marketplace: address(0), + marketplaceFeeNumerator: 0, + maxRoyaltyFeeNumerator: 0, + listingNonce: _getNextNonce(alice), + offerNonce: _getNextNonce(bob), + listingMinPrice: erc20.amount, + offerPrice: erc20.amount, + listingExpiration: type(uint256).max, + offerExpiration: type(uint256).max, + tokenId: nft.identifier, + amount: nft.amount + }); + + SignatureECDSA memory signedListing = _getSignedListing( + alice, + saleDetails + ); + SignatureECDSA memory signedOffer = _getSignedOffer(bob, saleDetails); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.buySingleListing.selector, + saleDetails, + signedListing, + signedOffer + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + 0, + payload + ); + } + + function getPayload_BuyOfferedERC20WithERC721( + TestOrderContext calldata context, + TestItem20 memory erc20, + TestItem721 memory nft + ) external override returns (TestOrderPayload memory execution) { + if (context.listOnChain) { + _notImplemented(); + } + + vm.prank(nft.token); + paymentProcessor.setCollectionSecurityPolicy( + nft.token, + securityPolicyId + ); + + if ( + !paymentProcessor.isPaymentMethodApproved( + securityPolicyId, + erc20.token + ) + ) { + paymentProcessor.whitelistPaymentMethod( + securityPolicyId, + erc20.token + ); + } + + address alice = context.offerer; + address bob = context.fulfiller; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(bob)); + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: true, + collectionLevelOffer: false, + protocol: TokenProtocols.ERC721, + paymentCoin: erc20.token, + tokenAddress: nft.token, + seller: bob, + privateBuyer: address(0), + buyer: alice, + delegatedPurchaser: address(0), + marketplace: address(0), + marketplaceFeeNumerator: 0, + maxRoyaltyFeeNumerator: 0, + listingNonce: _getNextNonce(bob), + offerNonce: _getNextNonce(alice), + listingMinPrice: erc20.amount, + offerPrice: erc20.amount, + listingExpiration: type(uint256).max, + offerExpiration: type(uint256).max, + tokenId: nft.identifier, + amount: 1 + }); + + SignatureECDSA memory signedListing = _getSignedListing( + bob, + saleDetails + ); + SignatureECDSA memory signedOffer = _getSignedOffer(alice, saleDetails); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.buySingleListing.selector, + saleDetails, + signedListing, + signedOffer + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + 0, + payload + ); + } + + function getPayload_BuyOfferedWETHWithERC721( + TestOrderContext calldata context, + TestItem20 memory erc20, + TestItem721 memory nft + ) external override returns (TestOrderPayload memory execution) { + if (context.listOnChain) { + _notImplemented(); + } + + vm.prank(nft.token); + paymentProcessor.setCollectionSecurityPolicy( + nft.token, + securityPolicyId + ); + + if ( + !paymentProcessor.isPaymentMethodApproved( + securityPolicyId, + erc20.token + ) + ) { + paymentProcessor.whitelistPaymentMethod( + securityPolicyId, + erc20.token + ); + } + + address alice = context.offerer; + address bob = context.fulfiller; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(bob)); + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: true, + collectionLevelOffer: false, + protocol: TokenProtocols.ERC721, + paymentCoin: erc20.token, + tokenAddress: nft.token, + seller: bob, + privateBuyer: address(0), + buyer: alice, + delegatedPurchaser: address(0), + marketplace: address(0), + marketplaceFeeNumerator: 0, + maxRoyaltyFeeNumerator: 0, + listingNonce: _getNextNonce(bob), + offerNonce: _getNextNonce(alice), + listingMinPrice: erc20.amount, + offerPrice: erc20.amount, + listingExpiration: type(uint256).max, + offerExpiration: type(uint256).max, + tokenId: nft.identifier, + amount: 1 + }); + + SignatureECDSA memory signedListing = _getSignedListing( + bob, + saleDetails + ); + SignatureECDSA memory signedOffer = _getSignedOffer(alice, saleDetails); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.buySingleListing.selector, + saleDetails, + signedListing, + signedOffer + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + 0, + payload + ); + } + + function getPayload_BuyOfferedERC20WithERC1155( + TestOrderContext calldata context, + TestItem20 memory erc20, + TestItem1155 memory nft + ) external override returns (TestOrderPayload memory execution) { + if (context.listOnChain) { + _notImplemented(); + } + + vm.prank(nft.token); + paymentProcessor.setCollectionSecurityPolicy( + nft.token, + securityPolicyId + ); + + if ( + !paymentProcessor.isPaymentMethodApproved( + securityPolicyId, + erc20.token + ) + ) { + paymentProcessor.whitelistPaymentMethod( + securityPolicyId, + erc20.token + ); + } + + address alice = context.offerer; + address bob = context.fulfiller; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(bob)); + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: true, + collectionLevelOffer: false, + protocol: TokenProtocols.ERC1155, + paymentCoin: erc20.token, + tokenAddress: nft.token, + seller: bob, + privateBuyer: address(0), + buyer: alice, + delegatedPurchaser: address(0), + marketplace: address(0), + marketplaceFeeNumerator: 0, + maxRoyaltyFeeNumerator: 0, + listingNonce: _getNextNonce(bob), + offerNonce: _getNextNonce(alice), + listingMinPrice: erc20.amount, + offerPrice: erc20.amount, + listingExpiration: type(uint256).max, + offerExpiration: type(uint256).max, + tokenId: nft.identifier, + amount: nft.amount + }); + + SignatureECDSA memory signedListing = _getSignedListing( + bob, + saleDetails + ); + SignatureECDSA memory signedOffer = _getSignedOffer(alice, saleDetails); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.buySingleListing.selector, + saleDetails, + signedListing, + signedOffer + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + 0, + payload + ); + } + + function getPayload_BuyOfferedERC721WithEtherOneFeeRecipient( + TestOrderContext calldata context, + TestItem721 memory nft, + uint256 priceEthAmount, + address feeRecipient, + uint256 feeEthAmount + ) external override returns (TestOrderPayload memory execution) { + if (context.listOnChain) { + _notImplemented(); + } + + address alice = context.offerer; + address bob = context.fulfiller; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(feeRecipient, _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(feeRecipient, _getNextNonce(bob)); + + uint256 ethAmount = priceEthAmount + feeEthAmount; + uint256 feeRate = (feeEthAmount * 10000) / priceEthAmount; + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: false, + collectionLevelOffer: false, + protocol: TokenProtocols.ERC721, + paymentCoin: address(0), + tokenAddress: nft.token, + seller: alice, + privateBuyer: address(0), + buyer: bob, + delegatedPurchaser: address(0), + marketplace: feeRecipient, + marketplaceFeeNumerator: feeRate, + maxRoyaltyFeeNumerator: 0, + listingNonce: _getNextNonce(alice), + offerNonce: _getNextNonce(bob), + listingMinPrice: ethAmount, + offerPrice: ethAmount, + listingExpiration: type(uint256).max, + offerExpiration: type(uint256).max, + tokenId: nft.identifier, + amount: 1 + }); + + SignatureECDSA memory signedListing = _getSignedListing( + alice, + saleDetails + ); + SignatureECDSA memory signedOffer = _getSignedOffer(bob, saleDetails); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.buySingleListing.selector, + saleDetails, + signedListing, + signedOffer + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + ethAmount, + payload + ); + } + + function getPayload_BuyOfferedERC721WithEtherTwoFeeRecipient( + TestOrderContext calldata context, + TestItem721 memory nft, + uint256 priceEthAmount, + address feeRecipient1, + uint256 feeEthAmount1, + address feeRecipient2, + uint256 feeEthAmount2 + ) external override returns (TestOrderPayload memory execution) { + if (context.listOnChain) { + _notImplemented(); + } + + address alice = context.offerer; + address bob = context.fulfiller; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(feeRecipient1, _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(feeRecipient1, _getNextNonce(bob)); + + uint256 ethAmount = priceEthAmount + feeEthAmount1 + feeEthAmount2; + uint256 feeRate1 = (feeEthAmount1 * 10000) / priceEthAmount; + uint256 feeRate2 = (feeEthAmount2 * 10000) / priceEthAmount; + + TestERC721(nft.token).setTokenRoyalty( + nft.identifier, + feeRecipient2, + uint96(feeRate2) + ); + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: false, + collectionLevelOffer: false, + protocol: TokenProtocols.ERC721, + paymentCoin: address(0), + tokenAddress: nft.token, + seller: alice, + privateBuyer: address(0), + buyer: bob, + delegatedPurchaser: address(0), + marketplace: feeRecipient1, + marketplaceFeeNumerator: feeRate1, + maxRoyaltyFeeNumerator: feeRate2, + listingNonce: _getNextNonce(alice), + offerNonce: _getNextNonce(bob), + listingMinPrice: ethAmount, + offerPrice: ethAmount, + listingExpiration: type(uint256).max, + offerExpiration: type(uint256).max, + tokenId: nft.identifier, + amount: 1 + }); + + SignatureECDSA memory signedListing = _getSignedListing( + alice, + saleDetails + ); + SignatureECDSA memory signedOffer = _getSignedOffer(bob, saleDetails); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.buySingleListing.selector, + saleDetails, + signedListing, + signedOffer + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + ethAmount, + payload + ); + } + + function getPayload_BuyOfferedManyERC721WithEther( + TestOrderContext calldata context, + TestItem721[] calldata nfts, + uint256 ethAmount + ) external override returns (TestOrderPayload memory execution) { + if (context.listOnChain) { + _notImplemented(); + } + address alice = context.offerer; + address bob = context.fulfiller; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(bob)); + + MatchedOrderBundleBase + memory bundledOfferDetails = MatchedOrderBundleBase({ + protocol: TokenProtocols.ERC721, + paymentCoin: address(0), + tokenAddress: address(nfts[0].token), + privateBuyer: address(0), + buyer: bob, + delegatedPurchaser: address(0), + marketplace: address(0), + marketplaceFeeNumerator: 0, + offerNonce: _getNextNonce(bob), + offerPrice: ethAmount, + offerExpiration: type(uint256).max + }); + + MatchedOrderBundleExtended + memory bundleOfferDetailsExtended = MatchedOrderBundleExtended({ + bundleBase: bundledOfferDetails, + seller: alice, + listingNonce: _getNextNonce(alice), + listingExpiration: type(uint256).max + }); + + uint256 numItemsInBundle = nfts.length; + + BundledItem[] memory bundledOfferItems = new BundledItem[]( + numItemsInBundle + ); + + Accumulator memory accumulator = Accumulator({ + tokenIds: new uint256[](numItemsInBundle), + amounts: new uint256[](numItemsInBundle), + salePrices: new uint256[](numItemsInBundle), + maxRoyaltyFeeNumerators: new uint256[](numItemsInBundle), + sellers: new address[](numItemsInBundle), + sumListingPrices: 0 + }); + + for (uint256 i = 0; i < numItemsInBundle; ++i) { + bundledOfferItems[i].tokenId = nfts[i].identifier; + bundledOfferItems[i].amount = 1; + bundledOfferItems[i].maxRoyaltyFeeNumerator = 0; + bundledOfferItems[i].itemPrice = ethAmount / numItemsInBundle; + bundledOfferItems[i].listingNonce = 0; + bundledOfferItems[i].listingExpiration = 0; + bundledOfferItems[i].seller = alice; + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: false, + collectionLevelOffer: false, + protocol: bundledOfferDetails.protocol, + paymentCoin: bundledOfferDetails.paymentCoin, + tokenAddress: bundledOfferDetails.tokenAddress, + seller: bundledOfferItems[i].seller, + privateBuyer: address(0), + buyer: bundledOfferDetails.buyer, + delegatedPurchaser: bundledOfferDetails.delegatedPurchaser, + marketplace: bundledOfferDetails.marketplace, + marketplaceFeeNumerator: bundledOfferDetails + .marketplaceFeeNumerator, + maxRoyaltyFeeNumerator: bundledOfferItems[i] + .maxRoyaltyFeeNumerator, + listingNonce: bundledOfferItems[i].listingNonce, + offerNonce: bundledOfferDetails.offerNonce, + listingMinPrice: bundledOfferItems[i].itemPrice, + offerPrice: bundledOfferItems[i].itemPrice, + listingExpiration: bundledOfferItems[i].listingExpiration, + offerExpiration: bundledOfferDetails.offerExpiration, + tokenId: bundledOfferItems[i].tokenId, + amount: bundledOfferItems[i].amount + }); + + accumulator.tokenIds[i] = saleDetails.tokenId; + accumulator.amounts[i] = saleDetails.amount; + accumulator.salePrices[i] = saleDetails.listingMinPrice; + accumulator.maxRoyaltyFeeNumerators[i] = saleDetails + .maxRoyaltyFeeNumerator; + accumulator.sellers[i] = alice; + accumulator.sumListingPrices += saleDetails.listingMinPrice; + + // TestERC721(nfts[i].token).setTokenRoyalty(saleDetails.tokenId, feeReceiver, 0); + } + + AccumulatorHashes memory accumulatorHashes = AccumulatorHashes({ + tokenIdsKeccakHash: keccak256( + abi.encodePacked(accumulator.tokenIds) + ), + amountsKeccakHash: keccak256(abi.encodePacked(accumulator.amounts)), + maxRoyaltyFeeNumeratorsKeccakHash: keccak256( + abi.encodePacked(accumulator.maxRoyaltyFeeNumerators) + ), + itemPricesKeccakHash: keccak256( + abi.encodePacked(accumulator.salePrices) + ) + }); + + SignatureECDSA + memory signedBundledOffer = _getSignedOfferForBundledItems( + bob, + bundledOfferDetails, + bundledOfferItems + ); + SignatureECDSA memory signedBundledListing = _getSignedBundledListing( + alice, + accumulatorHashes, + bundleOfferDetailsExtended + ); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.buyBundledListing.selector, + signedBundledListing, + signedBundledOffer, + bundleOfferDetailsExtended, + bundledOfferItems + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + ethAmount, + payload + ); + } + + function getPayload_BuyOfferedManyERC721WithEtherItemsPricedIndividually( + TestOrderContext calldata context, + TestItem721[] calldata nfts, + uint256[] calldata ethAmounts + ) external override returns (TestOrderPayload memory execution) { + if (context.listOnChain) { + _notImplemented(); + } + address alice = context.offerer; + address bob = context.fulfiller; + uint256 numItemsInBundle = nfts.length; + uint256 totalEthAmount = 0; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(bob)); + + for (uint256 i = 0; i < numItemsInBundle; ++i) { + totalEthAmount += ethAmounts[i]; + } + + MatchedOrderBundleBase + memory bundledOfferDetails = MatchedOrderBundleBase({ + protocol: TokenProtocols.ERC721, + paymentCoin: address(0), + tokenAddress: address(nfts[0].token), + privateBuyer: address(0), + buyer: bob, + delegatedPurchaser: address(0), + marketplace: address(0), + marketplaceFeeNumerator: 0, + offerNonce: _getNextNonce(bob), + offerPrice: totalEthAmount, + offerExpiration: type(uint256).max + }); + + MatchedOrderBundleExtended + memory bundleOfferDetailsExtended = MatchedOrderBundleExtended({ + bundleBase: bundledOfferDetails, + seller: alice, + listingNonce: _getNextNonce(alice), + listingExpiration: type(uint256).max + }); + + BundledItem[] memory bundledOfferItems = new BundledItem[]( + numItemsInBundle + ); + + Accumulator memory accumulator = Accumulator({ + tokenIds: new uint256[](numItemsInBundle), + amounts: new uint256[](numItemsInBundle), + salePrices: new uint256[](numItemsInBundle), + maxRoyaltyFeeNumerators: new uint256[](numItemsInBundle), + sellers: new address[](numItemsInBundle), + sumListingPrices: 0 + }); + + for (uint256 i = 0; i < numItemsInBundle; ++i) { + bundledOfferItems[i].tokenId = nfts[i].identifier; + bundledOfferItems[i].amount = 1; + bundledOfferItems[i].maxRoyaltyFeeNumerator = 0; + bundledOfferItems[i].itemPrice = ethAmounts[i]; + bundledOfferItems[i].listingNonce = 0; + bundledOfferItems[i].listingExpiration = 0; + bundledOfferItems[i].seller = alice; + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: false, + collectionLevelOffer: false, + protocol: bundledOfferDetails.protocol, + paymentCoin: bundledOfferDetails.paymentCoin, + tokenAddress: bundledOfferDetails.tokenAddress, + seller: bundledOfferItems[i].seller, + privateBuyer: address(0), + buyer: bundledOfferDetails.buyer, + delegatedPurchaser: bundledOfferDetails.delegatedPurchaser, + marketplace: bundledOfferDetails.marketplace, + marketplaceFeeNumerator: bundledOfferDetails + .marketplaceFeeNumerator, + maxRoyaltyFeeNumerator: bundledOfferItems[i] + .maxRoyaltyFeeNumerator, + listingNonce: bundledOfferItems[i].listingNonce, + offerNonce: bundledOfferDetails.offerNonce, + listingMinPrice: bundledOfferItems[i].itemPrice, + offerPrice: bundledOfferItems[i].itemPrice, + listingExpiration: bundledOfferItems[i].listingExpiration, + offerExpiration: bundledOfferDetails.offerExpiration, + tokenId: bundledOfferItems[i].tokenId, + amount: bundledOfferItems[i].amount + }); + + accumulator.tokenIds[i] = saleDetails.tokenId; + accumulator.amounts[i] = saleDetails.amount; + accumulator.salePrices[i] = saleDetails.listingMinPrice; + accumulator.maxRoyaltyFeeNumerators[i] = saleDetails + .maxRoyaltyFeeNumerator; + accumulator.sellers[i] = alice; + accumulator.sumListingPrices += saleDetails.listingMinPrice; + + // TestERC721(nfts[i].token).setTokenRoyalty(saleDetails.tokenId, feeReceiver, 0); + } + + AccumulatorHashes memory accumulatorHashes = AccumulatorHashes({ + tokenIdsKeccakHash: keccak256( + abi.encodePacked(accumulator.tokenIds) + ), + amountsKeccakHash: keccak256(abi.encodePacked(accumulator.amounts)), + maxRoyaltyFeeNumeratorsKeccakHash: keccak256( + abi.encodePacked(accumulator.maxRoyaltyFeeNumerators) + ), + itemPricesKeccakHash: keccak256( + abi.encodePacked(accumulator.salePrices) + ) + }); + + SignatureECDSA + memory signedBundledOffer = _getSignedOfferForBundledItems( + bob, + bundledOfferDetails, + bundledOfferItems + ); + SignatureECDSA memory signedBundledListing = _getSignedBundledListing( + alice, + accumulatorHashes, + bundleOfferDetailsExtended + ); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.buyBundledListing.selector, + signedBundledListing, + signedBundledOffer, + bundleOfferDetailsExtended, + bundledOfferItems + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + totalEthAmount, + payload + ); + } + + function getPayload_BuyOfferedManyERC721WithEtherItemsPricedIndividuallyOneFeeRecipient( + TestBundleOrderWithSingleFeeReceiver memory args + ) external override returns (TestOrderPayload memory execution) { + if (args.context.listOnChain) { + _notImplemented(); + } + uint256 totalEthAmount = 0; + uint256 numItemsInBundle = args.nfts.length; + address alice = args.context.offerer; + address bob = args.context.fulfiller; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(bob)); + + for (uint256 i = 0; i < numItemsInBundle; ++i) { + totalEthAmount += args.itemPrices[i]; + } + + MatchedOrderBundleBase + memory bundledOfferDetails = MatchedOrderBundleBase({ + protocol: TokenProtocols.ERC721, + paymentCoin: address(0), + tokenAddress: address(args.nfts[0].token), + privateBuyer: address(0), + buyer: bob, + delegatedPurchaser: address(0), + marketplace: args.feeRecipient, + marketplaceFeeNumerator: args.feeRate, + offerNonce: _getNextNonce(bob), + offerPrice: totalEthAmount, + offerExpiration: type(uint256).max + }); + + MatchedOrderBundleExtended + memory bundleOfferDetailsExtended = MatchedOrderBundleExtended({ + bundleBase: bundledOfferDetails, + seller: alice, + listingNonce: _getNextNonce(alice), + listingExpiration: type(uint256).max + }); + + BundledItem[] memory bundledOfferItems = new BundledItem[]( + numItemsInBundle + ); + + Accumulator memory accumulator = Accumulator({ + tokenIds: new uint256[](numItemsInBundle), + amounts: new uint256[](numItemsInBundle), + salePrices: new uint256[](numItemsInBundle), + maxRoyaltyFeeNumerators: new uint256[](numItemsInBundle), + sellers: new address[](numItemsInBundle), + sumListingPrices: 0 + }); + + for (uint256 i = 0; i < numItemsInBundle; ++i) { + bundledOfferItems[i].tokenId = args.nfts[i].identifier; + bundledOfferItems[i].amount = 1; + bundledOfferItems[i].maxRoyaltyFeeNumerator = 0; + bundledOfferItems[i].itemPrice = args.itemPrices[i]; + bundledOfferItems[i].listingNonce = 0; + bundledOfferItems[i].listingExpiration = 0; + bundledOfferItems[i].seller = alice; + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: false, + collectionLevelOffer: false, + protocol: bundledOfferDetails.protocol, + paymentCoin: bundledOfferDetails.paymentCoin, + tokenAddress: bundledOfferDetails.tokenAddress, + seller: bundledOfferItems[i].seller, + privateBuyer: address(0), + buyer: bundledOfferDetails.buyer, + delegatedPurchaser: bundledOfferDetails.delegatedPurchaser, + marketplace: bundledOfferDetails.marketplace, + marketplaceFeeNumerator: bundledOfferDetails + .marketplaceFeeNumerator, + maxRoyaltyFeeNumerator: bundledOfferItems[i] + .maxRoyaltyFeeNumerator, + listingNonce: bundledOfferItems[i].listingNonce, + offerNonce: bundledOfferDetails.offerNonce, + listingMinPrice: bundledOfferItems[i].itemPrice, + offerPrice: bundledOfferItems[i].itemPrice, + listingExpiration: bundledOfferItems[i].listingExpiration, + offerExpiration: bundledOfferDetails.offerExpiration, + tokenId: bundledOfferItems[i].tokenId, + amount: bundledOfferItems[i].amount + }); + + accumulator.tokenIds[i] = saleDetails.tokenId; + accumulator.amounts[i] = saleDetails.amount; + accumulator.salePrices[i] = saleDetails.listingMinPrice; + accumulator.maxRoyaltyFeeNumerators[i] = saleDetails + .maxRoyaltyFeeNumerator; + accumulator.sellers[i] = alice; + accumulator.sumListingPrices += saleDetails.listingMinPrice; + + // TestERC721(args.nfts[i].token).setTokenRoyalty(saleDetails.tokenId, args.feeRecipient, uint96(args.feeEthAmounts[i] * 10000 / args.ethAmounts[i])); + } + + AccumulatorHashes memory accumulatorHashes = AccumulatorHashes({ + tokenIdsKeccakHash: keccak256( + abi.encodePacked(accumulator.tokenIds) + ), + amountsKeccakHash: keccak256(abi.encodePacked(accumulator.amounts)), + maxRoyaltyFeeNumeratorsKeccakHash: keccak256( + abi.encodePacked(accumulator.maxRoyaltyFeeNumerators) + ), + itemPricesKeccakHash: keccak256( + abi.encodePacked(accumulator.salePrices) + ) + }); + + SignatureECDSA memory signedBundledListing = _getSignedBundledListing( + alice, + accumulatorHashes, + bundleOfferDetailsExtended + ); + SignatureECDSA + memory signedBundledOffer = _getSignedOfferForBundledItems( + bob, + bundledOfferDetails, + bundledOfferItems + ); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.buyBundledListing.selector, + signedBundledListing, + signedBundledOffer, + bundleOfferDetailsExtended, + bundledOfferItems + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + totalEthAmount, + payload + ); + } + + function getPayload_BuyOfferedManyERC721WithEtherItemsPricedIndividuallyTwoFeeRecipients( + TestBundleOrderWithTwoFeeReceivers memory args + ) external override returns (TestOrderPayload memory execution) { + if (args.context.listOnChain) { + _notImplemented(); + } + uint256 totalEthAmount = 0; + uint256 numItemsInBundle = args.nfts.length; + address alice = args.context.offerer; + address bob = args.context.fulfiller; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(bob)); + + for (uint256 i = 0; i < numItemsInBundle; ++i) { + totalEthAmount += args.itemPrices[i]; + } + + MatchedOrderBundleBase + memory bundledOfferDetails = MatchedOrderBundleBase({ + protocol: TokenProtocols.ERC721, + paymentCoin: address(0), + tokenAddress: address(args.nfts[0].token), + privateBuyer: address(0), + buyer: bob, + delegatedPurchaser: address(0), + marketplace: args.feeRecipient1, + marketplaceFeeNumerator: args.feeRate1, + offerNonce: _getNextNonce(bob), + offerPrice: totalEthAmount, + offerExpiration: type(uint256).max + }); + + MatchedOrderBundleExtended + memory bundleOfferDetailsExtended = MatchedOrderBundleExtended({ + bundleBase: bundledOfferDetails, + seller: alice, + listingNonce: _getNextNonce(alice), + listingExpiration: type(uint256).max + }); + + BundledItem[] memory bundledOfferItems = new BundledItem[]( + numItemsInBundle + ); + + Accumulator memory accumulator = Accumulator({ + tokenIds: new uint256[](numItemsInBundle), + amounts: new uint256[](numItemsInBundle), + salePrices: new uint256[](numItemsInBundle), + maxRoyaltyFeeNumerators: new uint256[](numItemsInBundle), + sellers: new address[](numItemsInBundle), + sumListingPrices: 0 + }); + + for (uint256 i = 0; i < numItemsInBundle; ++i) { + bundledOfferItems[i].tokenId = args.nfts[i].identifier; + bundledOfferItems[i].amount = 1; + bundledOfferItems[i].maxRoyaltyFeeNumerator = args.feeRate2; + bundledOfferItems[i].itemPrice = args.itemPrices[i]; + bundledOfferItems[i].listingNonce = 0; + bundledOfferItems[i].listingExpiration = 0; + bundledOfferItems[i].seller = alice; + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: false, + collectionLevelOffer: false, + protocol: bundledOfferDetails.protocol, + paymentCoin: bundledOfferDetails.paymentCoin, + tokenAddress: bundledOfferDetails.tokenAddress, + seller: bundledOfferItems[i].seller, + privateBuyer: address(0), + buyer: bundledOfferDetails.buyer, + delegatedPurchaser: bundledOfferDetails.delegatedPurchaser, + marketplace: bundledOfferDetails.marketplace, + marketplaceFeeNumerator: bundledOfferDetails + .marketplaceFeeNumerator, + maxRoyaltyFeeNumerator: bundledOfferItems[i] + .maxRoyaltyFeeNumerator, + listingNonce: bundledOfferItems[i].listingNonce, + offerNonce: bundledOfferDetails.offerNonce, + listingMinPrice: bundledOfferItems[i].itemPrice, + offerPrice: bundledOfferItems[i].itemPrice, + listingExpiration: bundledOfferItems[i].listingExpiration, + offerExpiration: bundledOfferDetails.offerExpiration, + tokenId: bundledOfferItems[i].tokenId, + amount: bundledOfferItems[i].amount + }); + + accumulator.tokenIds[i] = saleDetails.tokenId; + accumulator.amounts[i] = saleDetails.amount; + accumulator.salePrices[i] = saleDetails.listingMinPrice; + accumulator.maxRoyaltyFeeNumerators[i] = saleDetails + .maxRoyaltyFeeNumerator; + accumulator.sellers[i] = alice; + accumulator.sumListingPrices += saleDetails.listingMinPrice; + + TestERC721(args.nfts[i].token).setTokenRoyalty( + saleDetails.tokenId, + args.feeRecipient2, + uint96(args.feeRate2) + ); + } + + AccumulatorHashes memory accumulatorHashes = AccumulatorHashes({ + tokenIdsKeccakHash: keccak256( + abi.encodePacked(accumulator.tokenIds) + ), + amountsKeccakHash: keccak256(abi.encodePacked(accumulator.amounts)), + maxRoyaltyFeeNumeratorsKeccakHash: keccak256( + abi.encodePacked(accumulator.maxRoyaltyFeeNumerators) + ), + itemPricesKeccakHash: keccak256( + abi.encodePacked(accumulator.salePrices) + ) + }); + + SignatureECDSA memory signedBundledListing = _getSignedBundledListing( + alice, + accumulatorHashes, + bundleOfferDetailsExtended + ); + SignatureECDSA + memory signedBundledOffer = _getSignedOfferForBundledItems( + bob, + bundledOfferDetails, + bundledOfferItems + ); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.buyBundledListing.selector, + signedBundledListing, + signedBundledOffer, + bundleOfferDetailsExtended, + bundledOfferItems + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + totalEthAmount, + payload + ); + } + + function getPayload_BuyOfferedManyERC721WithEtherDistinctOrders( + TestOrderContext[] calldata contexts, + TestItem721[] calldata nfts, + uint256[] calldata ethAmounts + ) external override returns (TestOrderPayload memory execution) { + for (uint256 i = 0; i < contexts.length; ++i) { + if (contexts[i].listOnChain) { + _notImplemented(); + } + } + + uint256 totalEthAmount = 0; + uint256 numItemsInBundle = nfts.length; + address alice = contexts[0].offerer; + address bob = contexts[0].fulfiller; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(bob)); + + for (uint256 i = 0; i < numItemsInBundle; ++i) { + totalEthAmount += ethAmounts[i]; + } + + MatchedOrderBundleBase + memory bundledOfferDetails = MatchedOrderBundleBase({ + protocol: TokenProtocols.ERC721, + paymentCoin: address(0), + tokenAddress: nfts[0].token, + privateBuyer: address(0), + buyer: bob, + delegatedPurchaser: address(0), + marketplace: address(0), + marketplaceFeeNumerator: 0, + offerNonce: _getNextNonce(bob), + offerPrice: totalEthAmount, + offerExpiration: type(uint256).max + }); + + BundledItem[] memory bundledOfferItems = new BundledItem[]( + numItemsInBundle + ); + SignatureECDSA[] memory signedListings = new SignatureECDSA[]( + numItemsInBundle + ); + + for (uint256 i = 0; i < numItemsInBundle; ++i) { + bundledOfferItems[i].seller = alice; + bundledOfferItems[i].tokenId = nfts[i].identifier; + bundledOfferItems[i].amount = 1; + bundledOfferItems[i].maxRoyaltyFeeNumerator = 0; + bundledOfferItems[i].listingNonce = _getNextNonce(alice); + bundledOfferItems[i].itemPrice = ethAmounts[i]; + bundledOfferItems[i].listingExpiration = type(uint256).max; + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: false, + collectionLevelOffer: false, + protocol: bundledOfferDetails.protocol, + paymentCoin: bundledOfferDetails.paymentCoin, + tokenAddress: bundledOfferDetails.tokenAddress, + seller: bundledOfferItems[i].seller, + privateBuyer: address(0), + buyer: bundledOfferDetails.buyer, + delegatedPurchaser: bundledOfferDetails.delegatedPurchaser, + marketplace: bundledOfferDetails.marketplace, + marketplaceFeeNumerator: bundledOfferDetails + .marketplaceFeeNumerator, + maxRoyaltyFeeNumerator: bundledOfferItems[i] + .maxRoyaltyFeeNumerator, + listingNonce: bundledOfferItems[i].listingNonce, + offerNonce: bundledOfferDetails.offerNonce, + listingMinPrice: bundledOfferItems[i].itemPrice, + offerPrice: bundledOfferItems[i].itemPrice, + listingExpiration: bundledOfferItems[i].listingExpiration, + offerExpiration: bundledOfferDetails.offerExpiration, + tokenId: bundledOfferItems[i].tokenId, + amount: bundledOfferItems[i].amount + }); + + signedListings[i] = _getSignedListing(alice, saleDetails); + } + + SignatureECDSA memory signedOffer = _getSignedOfferForBundledItems( + bob, + bundledOfferDetails, + bundledOfferItems + ); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.sweepCollection.selector, + signedOffer, + bundledOfferDetails, + bundledOfferItems, + signedListings + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + totalEthAmount, + payload + ); + } + + function getPayload_BuyOfferedManyERC721WithErc20DistinctOrders( + TestOrderContext[] calldata contexts, + address erc20Address, + TestItem721[] calldata nfts, + uint256[] calldata erc20Amounts + ) external override returns (TestOrderPayload memory execution) { + for (uint256 i = 0; i < contexts.length; ++i) { + if (contexts[i].listOnChain) { + _notImplemented(); + } + } + + vm.prank(nfts[0].token); + paymentProcessor.setCollectionSecurityPolicy( + nfts[0].token, + securityPolicyId + ); + + if ( + !paymentProcessor.isPaymentMethodApproved( + securityPolicyId, + erc20Address + ) + ) { + paymentProcessor.whitelistPaymentMethod( + securityPolicyId, + erc20Address + ); + } + + uint256 totalErc20Amount = 0; + uint256 numItemsInBundle = nfts.length; + address alice = contexts[0].offerer; + address bob = contexts[0].fulfiller; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(bob)); + + for (uint256 i = 0; i < numItemsInBundle; ++i) { + totalErc20Amount += erc20Amounts[i]; + } + + MatchedOrderBundleBase + memory bundledOfferDetails = MatchedOrderBundleBase({ + protocol: TokenProtocols.ERC721, + paymentCoin: erc20Address, + tokenAddress: nfts[0].token, + privateBuyer: address(0), + buyer: bob, + delegatedPurchaser: address(0), + marketplace: address(0), + marketplaceFeeNumerator: 0, + offerNonce: _getNextNonce(bob), + offerPrice: totalErc20Amount, + offerExpiration: type(uint256).max + }); + + BundledItem[] memory bundledOfferItems = new BundledItem[]( + numItemsInBundle + ); + SignatureECDSA[] memory signedListings = new SignatureECDSA[]( + numItemsInBundle + ); + + for (uint256 i = 0; i < numItemsInBundle; ++i) { + bundledOfferItems[i].seller = alice; + bundledOfferItems[i].tokenId = nfts[i].identifier; + bundledOfferItems[i].amount = 1; + bundledOfferItems[i].maxRoyaltyFeeNumerator = 0; + bundledOfferItems[i].listingNonce = _getNextNonce(alice); + bundledOfferItems[i].itemPrice = erc20Amounts[i]; + bundledOfferItems[i].listingExpiration = type(uint256).max; + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: false, + collectionLevelOffer: false, + protocol: bundledOfferDetails.protocol, + paymentCoin: bundledOfferDetails.paymentCoin, + tokenAddress: bundledOfferDetails.tokenAddress, + seller: bundledOfferItems[i].seller, + privateBuyer: address(0), + buyer: bundledOfferDetails.buyer, + delegatedPurchaser: bundledOfferDetails.delegatedPurchaser, + marketplace: bundledOfferDetails.marketplace, + marketplaceFeeNumerator: bundledOfferDetails + .marketplaceFeeNumerator, + maxRoyaltyFeeNumerator: bundledOfferItems[i] + .maxRoyaltyFeeNumerator, + listingNonce: bundledOfferItems[i].listingNonce, + offerNonce: bundledOfferDetails.offerNonce, + listingMinPrice: bundledOfferItems[i].itemPrice, + offerPrice: bundledOfferItems[i].itemPrice, + listingExpiration: bundledOfferItems[i].listingExpiration, + offerExpiration: bundledOfferDetails.offerExpiration, + tokenId: bundledOfferItems[i].tokenId, + amount: bundledOfferItems[i].amount + }); + + signedListings[i] = _getSignedListing(alice, saleDetails); + } + + SignatureECDSA memory signedOffer = _getSignedOfferForBundledItems( + bob, + bundledOfferDetails, + bundledOfferItems + ); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.sweepCollection.selector, + signedOffer, + bundledOfferDetails, + bundledOfferItems, + signedListings + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + 0, + payload + ); + } + + function getPayload_BuyOfferedManyERC721WithWETHDistinctOrders( + TestOrderContext[] calldata contexts, + address erc20Address, + TestItem721[] calldata nfts, + uint256[] calldata erc20Amounts + ) external override returns (TestOrderPayload memory execution) { + for (uint256 i = 0; i < contexts.length; ++i) { + if (contexts[i].listOnChain) { + _notImplemented(); + } + } + + vm.prank(nfts[0].token); + paymentProcessor.setCollectionSecurityPolicy( + nfts[0].token, + securityPolicyId + ); + + if ( + !paymentProcessor.isPaymentMethodApproved( + securityPolicyId, + erc20Address + ) + ) { + paymentProcessor.whitelistPaymentMethod( + securityPolicyId, + erc20Address + ); + } + + uint256 totalErc20Amount = 0; + uint256 numItemsInBundle = nfts.length; + address alice = contexts[0].offerer; + address bob = contexts[0].fulfiller; + + vm.prank(alice); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(alice)); + + vm.prank(bob); + paymentProcessor.revokeSingleNonce(address(0), _getNextNonce(bob)); + + for (uint256 i = 0; i < numItemsInBundle; ++i) { + totalErc20Amount += erc20Amounts[i]; + } + + MatchedOrderBundleBase + memory bundledOfferDetails = MatchedOrderBundleBase({ + protocol: TokenProtocols.ERC721, + paymentCoin: erc20Address, + tokenAddress: nfts[0].token, + privateBuyer: address(0), + buyer: bob, + delegatedPurchaser: address(0), + marketplace: address(0), + marketplaceFeeNumerator: 0, + offerNonce: _getNextNonce(bob), + offerPrice: totalErc20Amount, + offerExpiration: type(uint256).max + }); + + BundledItem[] memory bundledOfferItems = new BundledItem[]( + numItemsInBundle + ); + SignatureECDSA[] memory signedListings = new SignatureECDSA[]( + numItemsInBundle + ); + + for (uint256 i = 0; i < numItemsInBundle; ++i) { + bundledOfferItems[i].seller = alice; + bundledOfferItems[i].tokenId = nfts[i].identifier; + bundledOfferItems[i].amount = 1; + bundledOfferItems[i].maxRoyaltyFeeNumerator = 0; + bundledOfferItems[i].listingNonce = _getNextNonce(alice); + bundledOfferItems[i].itemPrice = erc20Amounts[i]; + bundledOfferItems[i].listingExpiration = type(uint256).max; + + MatchedOrder memory saleDetails = MatchedOrder({ + sellerAcceptedOffer: false, + collectionLevelOffer: false, + protocol: bundledOfferDetails.protocol, + paymentCoin: bundledOfferDetails.paymentCoin, + tokenAddress: bundledOfferDetails.tokenAddress, + seller: bundledOfferItems[i].seller, + privateBuyer: address(0), + buyer: bundledOfferDetails.buyer, + delegatedPurchaser: bundledOfferDetails.delegatedPurchaser, + marketplace: bundledOfferDetails.marketplace, + marketplaceFeeNumerator: bundledOfferDetails + .marketplaceFeeNumerator, + maxRoyaltyFeeNumerator: bundledOfferItems[i] + .maxRoyaltyFeeNumerator, + listingNonce: bundledOfferItems[i].listingNonce, + offerNonce: bundledOfferDetails.offerNonce, + listingMinPrice: bundledOfferItems[i].itemPrice, + offerPrice: bundledOfferItems[i].itemPrice, + listingExpiration: bundledOfferItems[i].listingExpiration, + offerExpiration: bundledOfferDetails.offerExpiration, + tokenId: bundledOfferItems[i].tokenId, + amount: bundledOfferItems[i].amount + }); + + signedListings[i] = _getSignedListing(alice, saleDetails); + } + + SignatureECDSA memory signedOffer = _getSignedOfferForBundledItems( + bob, + bundledOfferDetails, + bundledOfferItems + ); + + bytes memory payload = abi.encodeWithSelector( + IPaymentProcessor.sweepCollection.selector, + signedOffer, + bundledOfferDetails, + bundledOfferItems, + signedListings + ); + + execution.executeOrder = TestCallParameters( + address(paymentProcessor), + 0, + payload + ); + } + + function _getNextNonce(address account) internal returns (uint256) { + uint256 nextUnusedNonce = _nonces[account]; + ++_nonces[account]; + return nextUnusedNonce; + } + + function _getSignedListing( + address sellerAddress_, + MatchedOrder memory saleDetails + ) internal view returns (SignatureECDSA memory) { + bytes32 listingDigest = ECDSA.toTypedDataHash( + paymentProcessor.getDomainSeparator(), + keccak256( + bytes.concat( + abi.encode( + SALE_APPROVAL_HASH, + uint8(saleDetails.protocol), + saleDetails.sellerAcceptedOffer, + saleDetails.marketplace, + saleDetails.marketplaceFeeNumerator, + saleDetails.maxRoyaltyFeeNumerator, + saleDetails.privateBuyer, + saleDetails.seller, + saleDetails.tokenAddress, + saleDetails.tokenId + ), + abi.encode( + saleDetails.amount, + saleDetails.listingMinPrice, + saleDetails.listingExpiration, + saleDetails.listingNonce, + paymentProcessor.masterNonces(saleDetails.seller), + saleDetails.paymentCoin + ) + ) + ) + ); + + (uint8 listingV, bytes32 listingR, bytes32 listingS) = _sign( + sellerAddress_, + listingDigest + ); + SignatureECDSA memory signedListing = SignatureECDSA({ + v: listingV, + r: listingR, + s: listingS + }); + + return signedListing; + } + + function _getSignedOffer( + address buyerAddress_, + MatchedOrder memory saleDetails + ) internal view returns (SignatureECDSA memory) { + bytes32 offerDigest = ECDSA.toTypedDataHash( + paymentProcessor.getDomainSeparator(), + keccak256( + bytes.concat( + abi.encode( + OFFER_APPROVAL_HASH, + uint8(saleDetails.protocol), + saleDetails.marketplace, + saleDetails.marketplaceFeeNumerator, + saleDetails.delegatedPurchaser, + saleDetails.buyer, + saleDetails.tokenAddress, + saleDetails.tokenId, + saleDetails.amount, + saleDetails.offerPrice + ), + abi.encode( + saleDetails.offerExpiration, + saleDetails.offerNonce, + paymentProcessor.masterNonces(saleDetails.buyer), + saleDetails.paymentCoin + ) + ) + ) + ); + + (uint8 offerV, bytes32 offerR, bytes32 offerS) = _sign( + buyerAddress_, + offerDigest + ); + SignatureECDSA memory signedOffer = SignatureECDSA({ + v: offerV, + r: offerR, + s: offerS + }); + + return signedOffer; + } + + function _getSignedOfferForBundledItems( + address buyerAddress_, + MatchedOrderBundleBase memory bundledOfferDetails, + BundledItem[] memory bundledOfferItems + ) internal view returns (SignatureECDSA memory) { + uint256[] memory tokenIds = new uint256[](bundledOfferItems.length); + uint256[] memory amounts = new uint256[](bundledOfferItems.length); + uint256[] memory itemPrices = new uint256[](bundledOfferItems.length); + for (uint256 i = 0; i < bundledOfferItems.length; ++i) { + tokenIds[i] = bundledOfferItems[i].tokenId; + amounts[i] = bundledOfferItems[i].amount; + itemPrices[i] = bundledOfferItems[i].itemPrice; + } + + bytes32 offerDigest = ECDSA.toTypedDataHash( + paymentProcessor.getDomainSeparator(), + keccak256( + bytes.concat( + abi.encode( + BUNDLED_OFFER_APPROVAL_HASH, + uint8(bundledOfferDetails.protocol), + bundledOfferDetails.marketplace, + bundledOfferDetails.marketplaceFeeNumerator, + bundledOfferDetails.delegatedPurchaser, + bundledOfferDetails.buyer, + bundledOfferDetails.tokenAddress, + bundledOfferDetails.offerPrice + ), + abi.encode( + bundledOfferDetails.offerExpiration, + bundledOfferDetails.offerNonce, + paymentProcessor.masterNonces( + bundledOfferDetails.buyer + ), + bundledOfferDetails.paymentCoin, + keccak256(abi.encodePacked(tokenIds)), + keccak256(abi.encodePacked(amounts)), + keccak256(abi.encodePacked(itemPrices)) + ) + ) + ) + ); + + (uint8 offerV, bytes32 offerR, bytes32 offerS) = _sign( + buyerAddress_, + offerDigest + ); + SignatureECDSA memory signedOffer = SignatureECDSA({ + v: offerV, + r: offerR, + s: offerS + }); + + return signedOffer; + } + + function _getSignedBundledListing( + address sellerAddress_, + AccumulatorHashes memory accumulatorHashes, + MatchedOrderBundleExtended memory bundleDetails + ) internal view returns (SignatureECDSA memory) { + bytes32 listingDigest = ECDSA.toTypedDataHash( + paymentProcessor.getDomainSeparator(), + keccak256( + bytes.concat( + abi.encode( + BUNDLED_SALE_APPROVAL_HASH, + uint8(bundleDetails.bundleBase.protocol), + bundleDetails.bundleBase.marketplace, + bundleDetails.bundleBase.marketplaceFeeNumerator, + bundleDetails.bundleBase.privateBuyer, + bundleDetails.seller, + bundleDetails.bundleBase.tokenAddress + ), + abi.encode( + bundleDetails.listingExpiration, + bundleDetails.listingNonce, + paymentProcessor.masterNonces(bundleDetails.seller), + bundleDetails.bundleBase.paymentCoin, + accumulatorHashes.tokenIdsKeccakHash, + accumulatorHashes.amountsKeccakHash, + accumulatorHashes.maxRoyaltyFeeNumeratorsKeccakHash, + accumulatorHashes.itemPricesKeccakHash + ) + ) + ) + ); + + (uint8 listingV, bytes32 listingR, bytes32 listingS) = _sign( + sellerAddress_, + listingDigest + ); + SignatureECDSA memory signedListing = SignatureECDSA({ + v: listingV, + r: listingR, + s: listingS + }); + + return signedListing; + } +} diff --git a/src/marketplaces/lb-payment-processor/interfaces/IPaymentProcessor.sol b/src/marketplaces/lb-payment-processor/interfaces/IPaymentProcessor.sol new file mode 100644 index 0000000..783f053 --- /dev/null +++ b/src/marketplaces/lb-payment-processor/interfaces/IPaymentProcessor.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.7; + +import "./PaymentProcessorDataTypes.sol"; + +/** + * @title IPaymentProcessor + * @author Limit Break, Inc. + * @notice Interface definition for payment processor contracts. + */ +interface IPaymentProcessor { + /// @notice Emitted when a bundle of ERC-721 tokens is successfully purchased using `buyBundledListing` + event BuyBundledListingERC721( + address indexed marketplace, + address indexed tokenAddress, + address indexed paymentCoin, + address buyer, + address seller, + bool[] unsuccessfulFills, + uint256[] tokenIds, + uint256[] salePrices + ); + + /// @notice Emitted when a bundle of ERC-1155 tokens is successfully purchased using `buyBundledListing` + event BuyBundledListingERC1155( + address indexed marketplace, + address indexed tokenAddress, + address indexed paymentCoin, + address buyer, + address seller, + bool[] unsuccessfulFills, + uint256[] tokenIds, + uint256[] amounts, + uint256[] salePrices + ); + + /// @notice Emitted for each token successfully purchased using either `buySingleLising` or `buyBatchOfListings` + event BuySingleListing( + address indexed marketplace, + address indexed tokenAddress, + address indexed paymentCoin, + address buyer, + address seller, + uint256 tokenId, + uint256 amount, + uint256 salePrice + ); + + /// @notice Emitted when a security policy is either created or modified + event CreatedOrUpdatedSecurityPolicy( + uint256 indexed securityPolicyId, + bool enforceExchangeWhitelist, + bool enforcePaymentMethodWhitelist, + bool enforcePricingConstraints, + bool disablePrivateListings, + bool disableDelegatedPurchases, + bool disableEIP1271Signatures, + bool disableExchangeWhitelistEOABypass, + uint32 pushPaymentGasLimit, + string policyName + ); + + /// @notice Emitted when an address is added to the exchange whitelist for a security policy + event ExchangeAddedToWhitelist( + uint256 indexed securityPolicyId, + address indexed exchange + ); + + /// @notice Emitted when an address is removed from the exchange whitelist for a security policy + event ExchangeRemovedFromWhitelist( + uint256 indexed securityPolicyId, + address indexed exchange + ); + + /// @notice Emitted when a user revokes all of their existing listings or offers that share the master nonce. + event MasterNonceInvalidated( + uint256 indexed nonce, + address indexed account + ); + + /// @notice Emitted when a user revokes a single listing or offer nonce for a specific marketplace. + event NonceInvalidated( + uint256 indexed nonce, + address indexed account, + address indexed marketplace, + bool wasCancellation + ); + + /// @notice Emitted when a coin is added to the approved coins mapping for a security policy + event PaymentMethodAddedToWhitelist( + uint256 indexed securityPolicyId, + address indexed coin + ); + + /// @notice Emitted when a coin is removed from the approved coins mapping for a security policy + event PaymentMethodRemovedFromWhitelist( + uint256 indexed securityPolicyId, + address indexed coin + ); + + /// @notice Emitted when the ownership of a security policy is transferred to a new account + event SecurityPolicyOwnershipTransferred( + address indexed previousOwner, + address indexed newOwner + ); + + /// @notice Emitted when a collection of ERC-721 tokens is successfully swept using `sweepCollection` + event SweepCollectionERC721( + address indexed marketplace, + address indexed tokenAddress, + address indexed paymentCoin, + address buyer, + bool[] unsuccessfulFills, + address[] sellers, + uint256[] tokenIds, + uint256[] salePrices + ); + + /// @notice Emitted when a collection of ERC-1155 tokens is successfully swept using `sweepCollection` + event SweepCollectionERC1155( + address indexed marketplace, + address indexed tokenAddress, + address indexed paymentCoin, + address buyer, + bool[] unsuccessfulFills, + address[] sellers, + uint256[] tokenIds, + uint256[] amounts, + uint256[] salePrices + ); + + /// @notice Emitted whenever the designated security policy id changes for a collection. + event UpdatedCollectionSecurityPolicy( + address indexed tokenAddress, + uint256 indexed securityPolicyId + ); + + /// @notice Emitted whenever the supported ERC-20 payment is set for price-constrained collections. + event UpdatedCollectionPaymentCoin( + address indexed tokenAddress, + address indexed paymentCoin + ); + + /// @notice Emitted whenever pricing bounds change at a collection level for price-constrained collections. + event UpdatedCollectionLevelPricingBoundaries( + address indexed tokenAddress, + uint256 floorPrice, + uint256 ceilingPrice + ); + + /// @notice Emitted whenever pricing bounds change at a token level for price-constrained collections. + event UpdatedTokenLevelPricingBoundaries( + address indexed tokenAddress, + uint256 indexed tokenId, + uint256 floorPrice, + uint256 ceilingPrice + ); + + function createSecurityPolicy( + bool enforceExchangeWhitelist, + bool enforcePaymentMethodWhitelist, + bool enforcePricingConstraints, + bool disablePrivateListings, + bool disableDelegatedPurchases, + bool disableEIP1271Signatures, + bool disableExchangeWhitelistEOABypass, + uint32 pushPaymentGasLimit, + string calldata registryName + ) external returns (uint256); + + function updateSecurityPolicy( + uint256 securityPolicyId, + bool enforceExchangeWhitelist, + bool enforcePaymentMethodWhitelist, + bool enforcePricingConstraints, + bool disablePrivateListings, + bool disableDelegatedPurchases, + bool disableEIP1271Signatures, + bool disableExchangeWhitelistEOABypass, + uint32 pushPaymentGasLimit, + string calldata registryName + ) external; + + function transferSecurityPolicyOwnership( + uint256 securityPolicyId, + address newOwner + ) external; + + function renounceSecurityPolicyOwnership(uint256 securityPolicyId) external; + + function setCollectionSecurityPolicy( + address tokenAddress, + uint256 securityPolicyId + ) external; + + function setCollectionPaymentCoin(address tokenAddress, address coin) + external; + + function setCollectionPricingBounds( + address tokenAddress, + PricingBounds calldata pricingBounds + ) external; + + function setTokenPricingBounds( + address tokenAddress, + uint256[] calldata tokenIds, + PricingBounds[] calldata pricingBounds + ) external; + + function whitelistExchange(uint256 securityPolicyId, address account) + external; + + function unwhitelistExchange(uint256 securityPolicyId, address account) + external; + + function whitelistPaymentMethod(uint256 securityPolicyId, address coin) + external; + + function unwhitelistPaymentMethod(uint256 securityPolicyId, address coin) + external; + + function revokeMasterNonce() external; + + function revokeSingleNonce(address marketplace, uint256 nonce) external; + + function buySingleListing( + MatchedOrder memory saleDetails, + SignatureECDSA memory signedListing, + SignatureECDSA memory signedOffer + ) external payable; + + function buyBatchOfListings( + MatchedOrder[] calldata saleDetailsArray, + SignatureECDSA[] calldata signedListings, + SignatureECDSA[] calldata signedOffers + ) external payable; + + function buyBundledListing( + SignatureECDSA memory signedListing, + SignatureECDSA memory signedOffer, + MatchedOrderBundleExtended memory bundleDetails, + BundledItem[] calldata bundleItems + ) external payable; + + function sweepCollection( + SignatureECDSA memory signedOffer, + MatchedOrderBundleBase memory bundleDetails, + BundledItem[] calldata bundleItems, + SignatureECDSA[] calldata signedListings + ) external payable; + + function getDomainSeparator() external view returns (bytes32); + + function getSecurityPolicy(uint256 securityPolicyId) + external + view + returns (SecurityPolicy memory); + + function isWhitelisted(uint256 securityPolicyId, address account) + external + view + returns (bool); + + function isPaymentMethodApproved(uint256 securityPolicyId, address coin) + external + view + returns (bool); + + function getTokenSecurityPolicyId(address collectionAddress) + external + view + returns (uint256); + + function isCollectionPricingImmutable(address tokenAddress) + external + view + returns (bool); + + function isTokenPricingImmutable(address tokenAddress, uint256 tokenId) + external + view + returns (bool); + + function getFloorPrice(address tokenAddress, uint256 tokenId) + external + view + returns (uint256); + + function getCeilingPrice(address tokenAddress, uint256 tokenId) + external + view + returns (uint256); + + function masterNonces(address) external view returns (uint256); +} diff --git a/src/marketplaces/lb-payment-processor/interfaces/PaymentProcessorDataTypes.sol b/src/marketplaces/lb-payment-processor/interfaces/PaymentProcessorDataTypes.sol new file mode 100644 index 0000000..68e3352 --- /dev/null +++ b/src/marketplaces/lb-payment-processor/interfaces/PaymentProcessorDataTypes.sol @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.7; + +import "test/tokens/interfaces/IERC20.sol"; + +enum TokenProtocols { + ERC721, + ERC1155 +} + +/** + * @dev The `v`, `r`, and `s` components of an ECDSA signature. For more information + * [refer to this article](https://medium.com/mycrypto/the-magic-of-digital-signatures-on-ethereum-98fe184dc9c7). + */ +struct SignatureECDSA { + uint8 v; + bytes32 r; + bytes32 s; +} + +/** + * @dev This struct is used as input to `buySingleListing` and `buyBatchOfListings` calls after an exchange matches + * @dev a buyer and seller. + * + * @dev **sellerAcceptedOffer**: Denotes that the transaction was initiated by the seller account by accepting an offer. + * @dev When true, ETH/native payments are not accepted, and only ERC-20 payment methods can be used. + * @dev **collectionLevelOffer**: Denotes that the offer that was accepted was at the collection level. When `true`, + * @dev the Buyer should be prompted to sign the the collection offer approval stucture. When false, the Buyer should + * @dev prompted to sign the offer approval structure. + * @dev **protocol**: 0 for ERC-721 or 1 for ERC-1155. See `TokenProtocols`. + * @dev **paymentCoin**: `address(0)` denotes native currency sale. Otherwise ERC-20 payment coin address. + * @dev **tokenAddress**: The smart contract address of the ERC-721 or ERC-1155 token being sold. + * @dev **seller**: The seller/current owner of the token. + * @dev **privateBuyer**: `address(0)` denotes a listing available to any buyer. Otherwise, this denotes the privately + * @dev designated buyer. + * @dev **buyer**: The buyer/new owner of the token. + * @dev **delegatedPurchaser**: Allows a buyer to delegate an address to buy a token on their behalf. This would allow + * @dev a warm burner wallet to purchase tokens and allow them to be received in a cold wallet, for example. + * @dev **marketplace**: The address designated to receive marketplace fees, if applicable. + * @dev **marketplaceFeeNumerator**: Marketplace fee percentage. Denominator is 10,000. + * @dev 0.5% fee numerator is 50, 1% fee numerator is 100, 10% fee numerator is 1,000 and so on. + * @dev **maxRoyaltyFeeNumerator**: Maximum approved royalty fee percentage. Denominator is 10,000. + * @dev 0.5% fee numerator is 50, 1% fee numerator is 100, 10% fee numerator is 1,000 and so on. + * @dev Marketplaces are responsible to query EIP-2981 royalty info from the NFT contract when presenting this + * @dev for signature. + * @dev **listingNonce**: The nonce the seller signed in the listing. + * @dev **offerNonce**: The nonce the buyer signed in the offer. + * @dev **listingMinPrice**: The minimum price the seller signed off on, in wei. Buyer can buy above, + * @dev but not below the seller-approved minimum price. + * @dev **offerPrice**: The sale price of the matched order, in wei. Buyer signs off on the final offer price. + * @dev **listingExpiration**: The timestamp at which the listing expires. + * @dev **offerExpiration**: The timestamp at which the offer expires. + * @dev **tokenId**: The id of the token being sold. For ERC-721 tokens, this is the specific NFT token id. + * @dev For ERC-1155 tokens, this denotes the token type id. + * @dev **amount**: The number of tokens being sold. For ERC-721 tokens, this must always be `1`. + * @dev For ERC-1155 tokens where balances are transferred, this must be greater than or equal to `1`. + */ +struct MatchedOrder { + bool sellerAcceptedOffer; + bool collectionLevelOffer; + TokenProtocols protocol; + address paymentCoin; + address tokenAddress; + address seller; + address privateBuyer; + address buyer; + address delegatedPurchaser; + address marketplace; + uint256 marketplaceFeeNumerator; + uint256 maxRoyaltyFeeNumerator; + uint256 listingNonce; + uint256 offerNonce; + uint256 listingMinPrice; + uint256 offerPrice; + uint256 listingExpiration; + uint256 offerExpiration; + uint256 tokenId; + uint256 amount; +} + +/** + * @dev This struct is used as input to `buyBundledListing` calls after an exchange matches a buyer and seller. + * @dev Wraps `MatchedOrderBundleBase` and adds seller, listing nonce and listing expiration. + * + * @dev **bundleBase**: Includes all fields from `MatchedOrderBundleBase`. + * @dev **seller**: The seller/current owner of all the tokens in a bundled listing. + * @dev **listingNonce**: The nonce the seller signed in the listing. Only one nonce is required approving the sale + * @dev of multiple tokens from one collection. + * @dev **listingExpiration**: The timestamp at which the listing expires. + */ +struct MatchedOrderBundleExtended { + MatchedOrderBundleBase bundleBase; + address seller; + uint256 listingNonce; + uint256 listingExpiration; +} + +/** + * @dev This struct is used as input to `sweepCollection` calls after an exchange matches multiple individual listings + * @dev with a single buyer. + * + * @dev **protocol**: 0 for ERC-721 or 1 for ERC-1155. See `TokenProtocols`. + * @dev **paymentCoin**: `address(0)` denotes native currency sale. Otherwise ERC-20 payment coin address. + * @dev **tokenAddress**: The smart contract address of the ERC-721 or ERC-1155 token being sold. + * @dev **privateBuyer**: `address(0)` denotes a listing available to any buyer. Otherwise, this denotes the privately + * @dev designated buyer. + * @dev **buyer**: The buyer/new owner of the token. + * @dev **delegatedPurchaser**: Allows a buyer to delegate an address to buy a token on their behalf. This would allow + * @dev a warm burner wallet to purchase tokens and allow them to be received in a cold wallet, for example. + * @dev **marketplace**: The address designated to receive marketplace fees, if applicable. + * @dev **marketplaceFeeNumerator**: Marketplace fee percentage. Denominator is 10,000. + * @dev 0.5% fee numerator is 50, 1% fee numerator is 100, 10% fee numerator is 1,000 and so on. + * @dev **offerNonce**: The nonce the buyer signed in the offer. Only one nonce is required approving the purchase + * @dev of multiple tokens from one collection. + * @dev **offerPrice**: The sale price of the entire order, in wei. Buyer signs off on the final offer price. + * @dev **offerExpiration**: The timestamp at which the offer expires. + */ +struct MatchedOrderBundleBase { + TokenProtocols protocol; + address paymentCoin; + address tokenAddress; + address privateBuyer; + address buyer; + address delegatedPurchaser; + address marketplace; + uint256 marketplaceFeeNumerator; + uint256 offerNonce; + uint256 offerPrice; + uint256 offerExpiration; +} + +/** + * @dev This struct is used as input to `sweepCollection` and `buyBundledListing` calls. + * @dev These fields are required per individual item listed. + * + * @dev **tokenId**: The id of the token being sold. For ERC-721 tokens, this is the specific NFT token id. + * @dev For ERC-1155 tokens, this denotes the token type id. + * @dev **amount**: The number of tokens being sold. For ERC-721 tokens, this must always be `1`. + * @dev For ERC-1155 tokens where balances are transferred, this must be greater than or equal to `1`. + * @dev **maxRoyaltyFeeNumerator**: Maximum approved royalty fee percentage. Denominator is 10,000. + * @dev 0.5% fee numerator is 50, 1% fee numerator is 100, 10% fee numerator is 1,000 and so on. + * @dev Marketplaces are responsible to query EIP-2981 royalty info from the NFT contract when presenting this + * @dev for signature. + * @dev **itemPrice**: The exact price the seller signed off on for an individual item, in wei. + * @dev Purchase price for the item must be exactly the listing item price. + * @dev **listingNonce**: The nonce the seller signed in the listing for an individual item. This should be set + * @dev for collection sweep transactions, but it should be zero for bundled listings, as the listing nonce is global + * @dev in that case. + * @dev **listingExpiration**: The timestamp at which an individual listing expires. This should be set + * @dev for collection sweep transactions, but it should be zero for bundled listings, as the listing nonce is global + * @dev in that case. + * @dev **seller**: The seller/current owner of the token. This should be set + * @dev for collection sweep transactions, but it should be zero for bundled listings, as the listing nonce is global + * @dev in that case. + */ +struct BundledItem { + uint256 tokenId; + uint256 amount; + uint256 maxRoyaltyFeeNumerator; + uint256 itemPrice; + uint256 listingNonce; + uint256 listingExpiration; + address seller; +} + +/** + * @dev This struct is used to define the marketplace behavior and constraints, giving creators flexibility to define + * marketplace behavior(s). + * + * @dev **enforceExchangeWhitelist**: Requires `buy` calls from smart contracts to be whitelisted. + * @dev **enforcePaymentMethodWhitelist**: Requires ERC-20 payment coins for `buy` calls to be whitelisted as an + * @dev approved payment method. + * @dev **enforcePricingConstraints**: Allows the creator to specify exactly one approved payment method, a minimum + * @dev floor price and a maximum ceiling price. When true, this value supercedes `enforcePaymentMethodWhitelist`. + * @dev **disablePrivateListings**: Disables private sales. + * @dev **disableDelegatedPurchases**: Disables purchases by delegated accounts on behalf of buyers. + * @dev **disableEIP1271Signatures**: Disables sales and purchases using multi-sig wallets that implement EIP-1271. + * @dev Enforces that buyers and sellers are EOAs. + * @dev **disableExchangeWhitelistEOABypass**: Has no effect when `enforceExchangeWhitelist` is false. + * @dev When exchange whitelist is enforced, this disables calls from EOAs, effectively requiring purchases to be + * @dev composed by whitelisted 3rd party exchange contracts. + * @dev **pushPaymentGasLimit**: This is the amount of gas to forward when pushing native payments. + * @dev At the time this contract was written, 2300 gas is the recommended amount, but should costs of EVM opcodes + * @dev change in the future, this field can be used to increase or decrease the amount of forwarded gas. Care should + * @dev be taken to ensure not enough gas is forwarded to result in possible re-entrancy. + * @dev **policyOwner**: The account that has access to modify a security policy or update the exchange whitelist + * @dev or approved payment list for the security policy. + */ +struct SecurityPolicy { + bool enforceExchangeWhitelist; + bool enforcePaymentMethodWhitelist; + bool enforcePricingConstraints; + bool disablePrivateListings; + bool disableDelegatedPurchases; + bool disableEIP1271Signatures; + bool disableExchangeWhitelistEOABypass; + uint32 pushPaymentGasLimit; + address policyOwner; +} + +/** + * @dev This struct is used to define pricing constraints for a collection or individual token. + * + * @dev **isEnabled**: When true, this indicates that pricing constraints are set for the collection or token. + * @dev **isImmutable**: When true, this indicates that pricing constraints are immutable and cannot be changed. + * @dev **floorPrice**: The minimum price for a token or collection. This is only enforced when + * @dev `enforcePricingConstraints` is `true`. + * @dev **ceilingPrice**: The maximum price for a token or collection. This is only enforced when + * @dev `enforcePricingConstraints` is `true`. + */ +struct PricingBounds { + bool isEnabled; + bool isImmutable; + uint256 floorPrice; + uint256 ceilingPrice; +} + +/** + * @dev Internal contract use only - this is not a public-facing struct + */ +struct SplitProceeds { + address royaltyRecipient; + uint256 royaltyProceeds; + uint256 marketplaceProceeds; + uint256 sellerProceeds; +} + +/** + * @dev Internal contract use only - this is not a public-facing struct + */ +struct Accumulator { + uint256[] tokenIds; + uint256[] amounts; + uint256[] salePrices; + uint256[] maxRoyaltyFeeNumerators; + address[] sellers; + uint256 sumListingPrices; +} + +/** + * @dev Internal contract use only - this is not a public-facing struct + */ +struct AccumulatorHashes { + bytes32 tokenIdsKeccakHash; + bytes32 amountsKeccakHash; + bytes32 maxRoyaltyFeeNumeratorsKeccakHash; + bytes32 itemPricesKeccakHash; +} + +/** + * @dev Internal contract use only - this is not a public-facing struct + */ +struct PayoutsAccumulator { + address lastSeller; + address lastMarketplace; + address lastRoyaltyRecipient; + uint256 accumulatedSellerProceeds; + uint256 accumulatedMarketplaceProceeds; + uint256 accumulatedRoyaltyProceeds; +} + +/** + * @dev Internal contract use only - this is not a public-facing struct + */ +struct ComputeAndDistributeProceedsArgs { + uint256 pushPaymentGasLimit; + address purchaser; + IERC20 paymentCoin; + function(address, address, IERC20, uint256, uint256) funcPayout; + function(address, address, address, uint256, uint256) + returns (bool) funcDispenseToken; +} diff --git a/src/marketplaces/lb-payment-processor/lib/ECDSA.sol b/src/marketplaces/lb-payment-processor/lib/ECDSA.sol new file mode 100644 index 0000000..a8e5f41 --- /dev/null +++ b/src/marketplaces/lb-payment-processor/lib/ECDSA.sol @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.3) (utils/cryptography/ECDSA.sol) + +pragma solidity ^0.8.0; + +import "./Strings.sol"; + +/** + * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. + * + * These functions can be used to verify that a message was signed by the holder + * of the private keys of a given address. + */ +library ECDSA { + enum RecoverError { + NoError, + InvalidSignature, + InvalidSignatureLength, + InvalidSignatureS, + InvalidSignatureV + } + + function _throwError(RecoverError error) private pure { + if (error == RecoverError.NoError) { + return; // no error: do nothing + } else if (error == RecoverError.InvalidSignature) { + revert("ECDSA: invalid signature"); + } else if (error == RecoverError.InvalidSignatureLength) { + revert("ECDSA: invalid signature length"); + } else if (error == RecoverError.InvalidSignatureS) { + revert("ECDSA: invalid signature 's' value"); + } else if (error == RecoverError.InvalidSignatureV) { + revert("ECDSA: invalid signature 'v' value"); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature` or error string. This address can then be used for verification purposes. + * + * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {toEthSignedMessageHash} on it. + * + * Documentation for signature generation: + * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] + * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] + * + * _Available since v4.3._ + */ + function tryRecover(bytes32 hash, bytes memory signature) + internal + pure + returns (address, RecoverError) + { + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + /// @solidity memory-safe-assembly + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + return tryRecover(hash, v, r, s); + } else { + return (address(0), RecoverError.InvalidSignatureLength); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature`. This address can then be used for verification purposes. + * + * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {toEthSignedMessageHash} on it. + */ + function recover(bytes32 hash, bytes memory signature) + internal + pure + returns (address) + { + (address recovered, RecoverError error) = tryRecover(hash, signature); + _throwError(error); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately. + * + * See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures] + * + * _Available since v4.3._ + */ + function tryRecover( + bytes32 hash, + bytes32 r, + bytes32 vs + ) internal pure returns (address, RecoverError) { + bytes32 s = vs & + bytes32( + 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + ); + uint8 v = uint8((uint256(vs) >> 255) + 27); + return tryRecover(hash, v, r, s); + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately. + * + * _Available since v4.2._ + */ + function recover( + bytes32 hash, + bytes32 r, + bytes32 vs + ) internal pure returns (address) { + (address recovered, RecoverError error) = tryRecover(hash, r, vs); + _throwError(error); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `v`, + * `r` and `s` signature fields separately. + * + * _Available since v4.3._ + */ + function tryRecover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address, RecoverError) { + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if ( + uint256(s) > + 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0 + ) { + return (address(0), RecoverError.InvalidSignatureS); + } + if (v != 27 && v != 28) { + return (address(0), RecoverError.InvalidSignatureV); + } + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(hash, v, r, s); + if (signer == address(0)) { + return (address(0), RecoverError.InvalidSignature); + } + + return (signer, RecoverError.NoError); + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function recover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address) { + (address recovered, RecoverError error) = tryRecover(hash, v, r, s); + _throwError(error); + return recovered; + } + + /** + * @dev Returns an Ethereum Signed Message, created from a `hash`. This + * produces hash corresponding to the one signed with the + * https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] + * JSON-RPC method as part of EIP-191. + * + * See {recover}. + */ + function toEthSignedMessageHash(bytes32 hash) + internal + pure + returns (bytes32) + { + // 32 is the length in bytes of hash, + // enforced by the type signature above + return + keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", hash) + ); + } + + /** + * @dev Returns an Ethereum Signed Message, created from `s`. This + * produces hash corresponding to the one signed with the + * https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] + * JSON-RPC method as part of EIP-191. + * + * See {recover}. + */ + function toEthSignedMessageHash(bytes memory s) + internal + pure + returns (bytes32) + { + return + keccak256( + abi.encodePacked( + "\x19Ethereum Signed Message:\n", + Strings.toString(s.length), + s + ) + ); + } + + /** + * @dev Returns an Ethereum Signed Typed Data, created from a + * `domainSeparator` and a `structHash`. This produces hash corresponding + * to the one signed with the + * https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] + * JSON-RPC method as part of EIP-712. + * + * See {recover}. + */ + function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) + internal + pure + returns (bytes32) + { + return + keccak256( + abi.encodePacked("\x19\x01", domainSeparator, structHash) + ); + } +} diff --git a/src/marketplaces/lb-payment-processor/lib/Strings.sol b/src/marketplaces/lb-payment-processor/lib/Strings.sol new file mode 100644 index 0000000..8b03110 --- /dev/null +++ b/src/marketplaces/lb-payment-processor/lib/Strings.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (utils/Strings.sol) + +pragma solidity ^0.8.0; + +/** + * @dev String operations. + */ +library Strings { + bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef"; + uint8 private constant _ADDRESS_LENGTH = 20; + + /** + * @dev Converts a `uint256` to its ASCII `string` decimal representation. + */ + function toString(uint256 value) internal pure returns (string memory) { + // Inspired by OraclizeAPI's implementation - MIT licence + // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol + + if (value == 0) { + return "0"; + } + uint256 temp = value; + uint256 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); + value /= 10; + } + return string(buffer); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + */ + function toHexString(uint256 value) internal pure returns (string memory) { + if (value == 0) { + return "0x00"; + } + uint256 temp = value; + uint256 length = 0; + while (temp != 0) { + length++; + temp >>= 8; + } + return toHexString(value, length); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + */ + function toHexString(uint256 value, uint256 length) + internal + pure + returns (string memory) + { + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = _HEX_SYMBOLS[value & 0xf]; + value >>= 4; + } + require(value == 0, "Strings: hex length insufficient"); + return string(buffer); + } + + /** + * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation. + */ + function toHexString(address addr) internal pure returns (string memory) { + return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH); + } +} diff --git a/src/marketplaces/seaport-1.5/SeaportOnePointFiveConfig.sol b/src/marketplaces/seaport-1.5/SeaportOnePointFiveConfig.sol index 4a3ff91..e36dde4 100644 --- a/src/marketplaces/seaport-1.5/SeaportOnePointFiveConfig.sol +++ b/src/marketplaces/seaport-1.5/SeaportOnePointFiveConfig.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.7; import { BaseMarketConfig } from "../../BaseMarketConfig.sol"; -import { TestCallParameters, TestOrderContext, TestOrderPayload, TestItem721, TestItem1155, TestItem20 } from "../../Types.sol"; +import "../../Types.sol"; import "./lib/ConsiderationStructs.sol"; import "./lib/ConsiderationTypeHashes.sol"; import { ConsiderationInterface as ISeaport } from "./interfaces/ConsiderationInterface.sol"; @@ -964,6 +964,204 @@ contract SeaportOnePointFiveConfig is ); } + function getPayload_BuyOfferedManyERC721WithEtherItemsPricedIndividually( + TestOrderContext calldata context, + TestItem721[] calldata nfts, + uint256[] calldata ethAmounts + ) external view override returns (TestOrderPayload memory execution) { + uint256 sumPayments = 0; + address alice = context.offerer; + uint256 numItemsInBundle = nfts.length; + + OfferItem[] memory offerItems = new OfferItem[](numItemsInBundle); + ConsiderationItem[] memory considerationItems = new ConsiderationItem[]( + numItemsInBundle + ); + + for (uint256 i = 0; i < numItemsInBundle; i++) { + offerItems[i] = OfferItem( + ItemType.ERC721, + nfts[i].token, + nfts[i].identifier, + 1, + 1 + ); + + considerationItems[i] = ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + ethAmounts[i], + ethAmounts[i], + payable(alice) + ); + + sumPayments += ethAmounts[i]; + } + + Order memory order = buildOrder(alice, offerItems, considerationItems); + + if (context.listOnChain) { + order.signature = ""; + + Order[] memory orders = new Order[](1); + orders[0] = order; + execution.submitOrder = TestCallParameters( + address(seaport), + 0, + abi.encodeWithSelector(ISeaport.validate.selector, orders) + ); + } + + execution.executeOrder = TestCallParameters( + address(seaport), + sumPayments, + abi.encodeWithSelector(ISeaport.fulfillOrder.selector, order, 0) + ); + } + + function getPayload_BuyOfferedManyERC721WithEtherItemsPricedIndividuallyOneFeeRecipient( + TestBundleOrderWithSingleFeeReceiver memory args + ) external view override returns (TestOrderPayload memory execution) { + uint256 sumPayments = 0; + address alice = args.context.offerer; + uint256 numItemsInBundle = args.nfts.length; + + OfferItem[] memory offerItems = new OfferItem[](numItemsInBundle); + ConsiderationItem[] memory considerationItems = new ConsiderationItem[]( + numItemsInBundle * 2 + ); + + for (uint256 i = 0; i < numItemsInBundle; i++) { + uint256 itemPrice = args.itemPrices[i]; + uint256 itemFee = (itemPrice * args.feeRate) / 10000; + uint256 sellerProceeds = itemPrice - itemFee; + offerItems[i] = OfferItem( + ItemType.ERC721, + args.nfts[i].token, + args.nfts[i].identifier, + 1, + 1 + ); + + considerationItems[2 * i] = ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + sellerProceeds, + sellerProceeds, + payable(alice) + ); + + considerationItems[2 * i + 1] = ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + itemFee, + itemFee, + payable(args.feeRecipient) + ); + + sumPayments += args.itemPrices[i]; + } + + Order memory order = buildOrder(alice, offerItems, considerationItems); + + if (args.context.listOnChain) { + order.signature = ""; + + Order[] memory orders = new Order[](1); + orders[0] = order; + execution.submitOrder = TestCallParameters( + address(seaport), + 0, + abi.encodeWithSelector(ISeaport.validate.selector, orders) + ); + } + + execution.executeOrder = TestCallParameters( + address(seaport), + sumPayments, + abi.encodeWithSelector(ISeaport.fulfillOrder.selector, order, 0) + ); + } + + function getPayload_BuyOfferedManyERC721WithEtherItemsPricedIndividuallyTwoFeeRecipients( + TestBundleOrderWithTwoFeeReceivers memory args + ) external view override returns (TestOrderPayload memory execution) { + uint256 sumPayments = 0; + address alice = args.context.offerer; + uint256 numItemsInBundle = args.nfts.length; + + OfferItem[] memory offerItems = new OfferItem[](numItemsInBundle); + ConsiderationItem[] memory considerationItems = new ConsiderationItem[]( + numItemsInBundle * 3 + ); + + for (uint256 i = 0; i < numItemsInBundle; i++) { + uint256 itemPrice = args.itemPrices[i]; + uint256 itemFee1 = (itemPrice * args.feeRate1) / 10000; + uint256 itemFee2 = (itemPrice * args.feeRate2) / 10000; + uint256 sellerProceeds = itemPrice - itemFee1 - itemFee2; + offerItems[i] = OfferItem( + ItemType.ERC721, + args.nfts[i].token, + args.nfts[i].identifier, + 1, + 1 + ); + + considerationItems[3 * i] = ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + sellerProceeds, + sellerProceeds, + payable(alice) + ); + + considerationItems[3 * i + 1] = ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + itemFee1, + itemFee1, + payable(args.feeRecipient1) + ); + + considerationItems[3 * i + 2] = ConsiderationItem( + ItemType.NATIVE, + address(0), + 0, + itemFee2, + itemFee2, + payable(args.feeRecipient2) + ); + + sumPayments += args.itemPrices[i]; + } + + Order memory order = buildOrder(alice, offerItems, considerationItems); + + if (args.context.listOnChain) { + order.signature = ""; + + Order[] memory orders = new Order[](1); + orders[0] = order; + execution.submitOrder = TestCallParameters( + address(seaport), + 0, + abi.encodeWithSelector(ISeaport.validate.selector, orders) + ); + } + + execution.executeOrder = TestCallParameters( + address(seaport), + sumPayments, + abi.encodeWithSelector(ISeaport.fulfillOrder.selector, order, 0) + ); + } + function getPayload_BuyOfferedManyERC721WithEtherDistinctOrders( TestOrderContext[] calldata contexts, TestItem721[] calldata nfts, diff --git a/test/GenericMarketplaceTest.t.sol b/test/GenericMarketplaceTest.t.sol index 4d07e36..c87faeb 100644 --- a/test/GenericMarketplaceTest.t.sol +++ b/test/GenericMarketplaceTest.t.sol @@ -6,6 +6,7 @@ import { BlurConfig } from "../src/marketplaces/blur/BlurConfig.sol"; import { BlurV2Config } from "../src/marketplaces/blur-2.0/BlurV2Config.sol"; import { FoundationConfig } from "../src/marketplaces/foundation/FoundationConfig.sol"; import { LooksRareConfig } from "../src/marketplaces/looksRare/LooksRareConfig.sol"; +import { PaymentProcessorConfig } from "../src/marketplaces/lb-payment-processor/PaymentProcessorConfig.sol"; import { SeaportOnePointFiveConfig } from "../src/marketplaces/seaport-1.5/SeaportOnePointFiveConfig.sol"; import { LooksRareV2Config } from "../src/marketplaces/looksRare-v2/LooksRareV2Config.sol"; import { SeaportOnePointOneConfig } from "../src/marketplaces/seaport-1.1/SeaportOnePointOneConfig.sol"; @@ -14,7 +15,7 @@ import { WyvernConfig } from "../src/marketplaces/wyvern/WyvernConfig.sol"; import { X2Y2Config } from "../src/marketplaces/X2Y2/X2Y2Config.sol"; import { ZeroExConfig } from "../src/marketplaces/zeroEx/ZeroExConfig.sol"; -import { SetupCall, TestOrderPayload, TestOrderContext, TestCallParameters, TestItem20, TestItem721, TestItem1155 } from "../src/Types.sol"; +import "../src/Types.sol"; import "./tokens/TestERC20.sol"; import "./tokens/TestERC721.sol"; @@ -26,6 +27,7 @@ contract GenericMarketplaceTest is BaseOrderTest { BaseMarketConfig blurV2Config; BaseMarketConfig foundationConfig; BaseMarketConfig looksRareConfig; + BaseMarketConfig paymentProcessorConfig; BaseMarketConfig looksRareV2Config; BaseMarketConfig seaportOnePointOneConfig; BaseMarketConfig seaportOnePointFiveConfig; @@ -39,6 +41,7 @@ contract GenericMarketplaceTest is BaseOrderTest { blurV2Config = BaseMarketConfig(new BlurV2Config()); foundationConfig = BaseMarketConfig(new FoundationConfig()); looksRareConfig = BaseMarketConfig(new LooksRareConfig()); + paymentProcessorConfig = BaseMarketConfig(new PaymentProcessorConfig()); looksRareV2Config = BaseMarketConfig(new LooksRareV2Config()); seaportOnePointOneConfig = BaseMarketConfig( new SeaportOnePointOneConfig() @@ -96,47 +99,61 @@ contract GenericMarketplaceTest is BaseOrderTest { // benchmarkMarket(wyvernConfig); // } + function testPaymentProcessor() external { + benchmarkMarket(paymentProcessorConfig); + } + function benchmarkMarket(BaseMarketConfig config) public { beforeAllPrepareMarketplaceTest(config); - benchmark_BuyOfferedERC721WithEther_ListOnChain(config); benchmark_BuyOfferedERC721WithEther(config); - benchmark_BuyOfferedERC1155WithEther_ListOnChain(config); benchmark_BuyOfferedERC1155WithEther(config); - benchmark_BuyOfferedERC721WithWETH_ListOnChain(config); benchmark_BuyOfferedERC721WithWETH(config); - benchmark_BuyOfferedERC721WithERC20_ListOnChain(config); benchmark_BuyOfferedERC721WithERC20(config); benchmark_BuyOfferedERC721WithBETH(config); benchmark_BuyOfferedERC1155WithERC20_ListOnChain(config); benchmark_BuyOfferedERC1155WithERC20(config); - benchmark_BuyOfferedERC20WithERC721_ListOnChain(config); benchmark_BuyOfferedERC20WithERC721(config); - benchmark_BuyOfferedWETHWithERC721_ListOnChain(config); benchmark_BuyOfferedWETHWithERC721(config); benchmark_BuyOfferedBETHWithERC721(config); benchmark_BuyOfferedERC20WithERC1155_ListOnChain(config); benchmark_BuyOfferedERC20WithERC1155(config); - benchmark_BuyOfferedERC721WithERC1155_ListOnChain(config); benchmark_BuyOfferedERC721WithERC1155(config); - benchmark_BuyOfferedERC1155WithERC721_ListOnChain(config); benchmark_BuyOfferedERC1155WithERC721(config); - benchmark_BuyOfferedERC721WithEtherFee_ListOnChain(config); benchmark_BuyOfferedERC721WithEtherFee(config); - benchmark_BuyOfferedERC721WithEtherFeeTwoRecipients_ListOnChain(config); benchmark_BuyOfferedERC721WithEtherFeeTwoRecipients(config); - benchmark_BuyTenOfferedERC721WithEther_ListOnChain(config); benchmark_BuyTenOfferedERC721WithEther(config); + benchmark_BuyTenOfferedERC721WithEtherDistinctOrders(config); + benchmark_BuyTenOfferedERC721WithErc20DistinctOrders(config); + benchmark_BuyTenOfferedERC721WithWETHDistinctOrders(config); + benchmark_MatchOrders_ABCA(config); + benchmark_BuyTenOfferedERC721WithEtherItemsPricedIndividually(config); + benchmark_BuyTenOfferedERC721WithEtherItemsPricedIndividuallyWithEtherFee( + config + ); + benchmark_BuyTenOfferedERC721WithEtherItemsPricedIndividuallyWithEtherFeeTwoRecipients( + config + ); + + benchmark_BuyOfferedERC721WithEther_ListOnChain(config); + benchmark_BuyOfferedERC1155WithEther_ListOnChain(config); + benchmark_BuyOfferedERC721WithWETH_ListOnChain(config); + benchmark_BuyOfferedERC721WithERC20_ListOnChain(config); + benchmark_BuyOfferedERC1155WithERC20_ListOnChain(config); + benchmark_BuyOfferedERC20WithERC721_ListOnChain(config); + benchmark_BuyOfferedWETHWithERC721_ListOnChain(config); + benchmark_BuyOfferedERC20WithERC1155_ListOnChain(config); + benchmark_BuyOfferedERC721WithERC1155_ListOnChain(config); + benchmark_BuyOfferedERC1155WithERC721_ListOnChain(config); + benchmark_BuyOfferedERC721WithEtherFee_ListOnChain(config); + benchmark_BuyOfferedERC721WithEtherFeeTwoRecipients_ListOnChain(config); + benchmark_BuyTenOfferedERC721WithEther_ListOnChain(config); benchmark_BuyTenOfferedERC721WithEtherDistinctOrders_ListOnChain( config ); - benchmark_BuyTenOfferedERC721WithEtherDistinctOrders(config); benchmark_BuyTenOfferedERC721WithErc20DistinctOrders_ListOnChain( config ); - benchmark_BuyTenOfferedERC721WithErc20DistinctOrders(config); benchmark_BuyTenOfferedERC721WithWETHDistinctOrders_ListOnChain(config); - benchmark_BuyTenOfferedERC721WithWETHDistinctOrders(config); - benchmark_MatchOrders_ABCA(config); } function beforeAllPrepareMarketplaceTest(BaseMarketConfig config) internal { @@ -1208,6 +1225,154 @@ contract GenericMarketplaceTest is BaseOrderTest { } } + function benchmark_BuyTenOfferedERC721WithEtherItemsPricedIndividually( + BaseMarketConfig config + ) internal prepareTest(config) { + string memory testLabel = "(ERC721x10 -> ETH (Priced Indvidually))"; + + TestItem721[] memory nfts = new TestItem721[](10); + uint256[] memory nftPrices = new uint256[](10); + + for (uint256 i = 0; i < 10; i++) { + test721_1.mint(alice, i + 1); + nfts[i] = TestItem721(address(test721_1), i + 1); + nftPrices[i] = 100 + (100 * i); + } + + try + config + .getPayload_BuyOfferedManyERC721WithEtherItemsPricedIndividually( + TestOrderContext(false, alice, bob), + nfts, + nftPrices + ) + returns (TestOrderPayload memory payload) { + for (uint256 i = 0; i < 10; i++) { + assertEq(test721_1.ownerOf(i + 1), alice); + } + + _benchmarkCallWithParams( + config.name(), + string(abi.encodePacked(testLabel, " Fulfill /w Sig")), + bob, + payload.executeOrder + ); + + for (uint256 i = 0; i < 10; i++) { + assertEq(test721_1.ownerOf(i + 1), bob); + } + } catch { + _logNotSupported(config.name(), testLabel); + } + } + + function benchmark_BuyTenOfferedERC721WithEtherItemsPricedIndividuallyWithEtherFee( + BaseMarketConfig config + ) internal prepareTest(config) { + string + memory testLabel = "(ERC721x10 -> ETH One-Fee-Recipient (Priced Indvidually))"; + + TestItem721[] memory nfts = new TestItem721[](10); + uint256[] memory nftPrices = new uint256[](10); + uint256 totalPrice = 0; + uint256 feeRate = 500; + + for (uint256 i = 0; i < 10; i++) { + test721_1.mint(alice, i + 1); + nfts[i] = TestItem721(address(test721_1), i + 1); + nftPrices[i] = 100 + (100 * i); + totalPrice += nftPrices[i]; + } + + try + config + .getPayload_BuyOfferedManyERC721WithEtherItemsPricedIndividuallyOneFeeRecipient( + TestBundleOrderWithSingleFeeReceiver({ + context: TestOrderContext(false, alice, bob), + nfts: nfts, + itemPrices: nftPrices, + feeRecipient: feeReciever1, + feeRate: feeRate + }) + ) + returns (TestOrderPayload memory payload) { + for (uint256 i = 0; i < 10; i++) { + assertEq(test721_1.ownerOf(i + 1), alice); + } + assertEq(feeReciever1.balance, 0); + + _benchmarkCallWithParams( + config.name(), + string(abi.encodePacked(testLabel, " Fulfill /w Sig")), + bob, + payload.executeOrder + ); + + for (uint256 i = 0; i < 10; i++) { + assertEq(test721_1.ownerOf(i + 1), bob); + } + assertEq(feeReciever1.balance, (totalPrice * feeRate) / 10000); + } catch { + _logNotSupported(config.name(), testLabel); + } + } + + function benchmark_BuyTenOfferedERC721WithEtherItemsPricedIndividuallyWithEtherFeeTwoRecipients( + BaseMarketConfig config + ) internal prepareTest(config) { + string + memory testLabel = "(ERC721x10 -> ETH Two-Fee-Recipients (Priced Indvidually))"; + + TestItem721[] memory nfts = new TestItem721[](10); + uint256[] memory nftPrices = new uint256[](10); + uint256 totalPrice = 0; + uint256 feeRate1 = 500; + uint256 feeRate2 = 1000; + + for (uint256 i = 0; i < 10; i++) { + test721_1.mint(alice, i + 1); + nfts[i] = TestItem721(address(test721_1), i + 1); + nftPrices[i] = 100 + (100 * i); + totalPrice += nftPrices[i]; + } + + try + config + .getPayload_BuyOfferedManyERC721WithEtherItemsPricedIndividuallyTwoFeeRecipients( + TestBundleOrderWithTwoFeeReceivers({ + context: TestOrderContext(false, alice, bob), + nfts: nfts, + itemPrices: nftPrices, + feeRecipient1: feeReciever1, + feeRate1: feeRate1, + feeRecipient2: feeReciever2, + feeRate2: feeRate2 + }) + ) + returns (TestOrderPayload memory payload) { + for (uint256 i = 0; i < 10; i++) { + assertEq(test721_1.ownerOf(i + 1), alice); + } + assertEq(feeReciever1.balance, 0); + assertEq(feeReciever2.balance, 0); + + _benchmarkCallWithParams( + config.name(), + string(abi.encodePacked(testLabel, " Fulfill /w Sig")), + bob, + payload.executeOrder + ); + + for (uint256 i = 0; i < 10; i++) { + assertEq(test721_1.ownerOf(i + 1), bob); + } + assertEq(feeReciever1.balance, (totalPrice * feeRate1) / 10000); + assertEq(feeReciever2.balance, (totalPrice * feeRate2) / 10000); + } catch { + _logNotSupported(config.name(), testLabel); + } + } + function benchmark_BuyTenOfferedERC721WithEtherDistinctOrders( BaseMarketConfig config ) internal prepareTest(config) { diff --git a/test/tokens/ERC2981.sol b/test/tokens/ERC2981.sol new file mode 100644 index 0000000..4dd151c --- /dev/null +++ b/test/tokens/ERC2981.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (token/common/ERC2981.sol) + +pragma solidity ^0.8.0; + +import "./interfaces/IERC2981.sol"; + +/** + * @dev Implementation of the NFT Royalty Standard, a standardized way to retrieve royalty payment information. + * + * Royalty information can be specified globally for all token ids via {_setDefaultRoyalty}, and/or individually for + * specific token ids via {_setTokenRoyalty}. The latter takes precedence over the first. + * + * Royalty is specified as a fraction of sale price. {_feeDenominator} is overridable but defaults to 10000, meaning the + * fee is specified in basis points by default. + * + * IMPORTANT: ERC-2981 only specifies a way to signal royalty information and does not enforce its payment. See + * https://eips.ethereum.org/EIPS/eip-2981#optional-royalty-payments[Rationale] in the EIP. Marketplaces are expected to + * voluntarily pay royalties together with sales, but note that this standard is not yet widely supported. + * + * _Available since v4.5._ + */ +abstract contract ERC2981 is IERC2981 { + struct RoyaltyInfo { + address receiver; + uint96 royaltyFraction; + } + + RoyaltyInfo private _defaultRoyaltyInfo; + mapping(uint256 => RoyaltyInfo) private _tokenRoyaltyInfo; + + /** + * @inheritdoc IERC2981 + */ + function royaltyInfo(uint256 _tokenId, uint256 _salePrice) + public + view + virtual + override + returns (address, uint256) + { + RoyaltyInfo memory royalty = _tokenRoyaltyInfo[_tokenId]; + + if (royalty.receiver == address(0)) { + royalty = _defaultRoyaltyInfo; + } + + uint256 royaltyAmount = (_salePrice * royalty.royaltyFraction) / + _feeDenominator(); + + return (royalty.receiver, royaltyAmount); + } + + /** + * @dev The denominator with which to interpret the fee set in {_setTokenRoyalty} and {_setDefaultRoyalty} as a + * fraction of the sale price. Defaults to 10000 so fees are expressed in basis points, but may be customized by an + * override. + */ + function _feeDenominator() internal pure virtual returns (uint96) { + return 10000; + } + + /** + * @dev Sets the royalty information that all ids in this contract will default to. + * + * Requirements: + * + * - `receiver` cannot be the zero address. + * - `feeNumerator` cannot be greater than the fee denominator. + */ + function _setDefaultRoyalty(address receiver, uint96 feeNumerator) + internal + virtual + { + //require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice"); + + _defaultRoyaltyInfo = RoyaltyInfo(receiver, feeNumerator); + } + + /** + * @dev Removes default royalty information. + */ + function _deleteDefaultRoyalty() internal virtual { + delete _defaultRoyaltyInfo; + } + + /** + * @dev Sets the royalty information for a specific token id, overriding the global default. + * + * Requirements: + * + * - `receiver` cannot be the zero address. + * - `feeNumerator` cannot be greater than the fee denominator. + */ + function _setTokenRoyalty( + uint256 tokenId, + address receiver, + uint96 feeNumerator + ) internal virtual { + //require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice"); + + _tokenRoyaltyInfo[tokenId] = RoyaltyInfo(receiver, feeNumerator); + } + + /** + * @dev Resets royalty information for the token id back to the global default. + */ + function _resetTokenRoyalty(uint256 tokenId) internal virtual { + delete _tokenRoyaltyInfo[tokenId]; + } +} diff --git a/test/tokens/TestERC721.sol b/test/tokens/TestERC721.sol index 55813d7..2a6adc0 100644 --- a/test/tokens/TestERC721.sol +++ b/test/tokens/TestERC721.sol @@ -2,8 +2,9 @@ pragma solidity 0.8.14; import "solmate/tokens/ERC721.sol"; +import "./ERC2981.sol"; -contract TestERC721 is ERC721("Test721", "TST721") { +contract TestERC721 is ERC721("Test721", "TST721"), ERC2981 { function mint(address to, uint256 tokenId) public returns (bool) { _mint(to, tokenId); return true; @@ -12,4 +13,18 @@ contract TestERC721 is ERC721("Test721", "TST721") { function tokenURI(uint256) public pure override returns (string memory) { return "tokenURI"; } + + function setDefaultRoyaltyInfo(address receiver, uint96 feeNumerator) + public + { + _setDefaultRoyalty(receiver, feeNumerator); + } + + function setTokenRoyalty( + uint256 tokenId, + address receiver, + uint96 feeNumerator + ) public { + _setTokenRoyalty(tokenId, receiver, feeNumerator); + } } diff --git a/test/tokens/interfaces/IERC20.sol b/test/tokens/interfaces/IERC20.sol new file mode 100644 index 0000000..ef473c6 --- /dev/null +++ b/test/tokens/interfaces/IERC20.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol) + +pragma solidity >=0.8.7; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) + external + view + returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); +} diff --git a/test/tokens/interfaces/IERC2981.sol b/test/tokens/interfaces/IERC2981.sol new file mode 100644 index 0000000..131d0c7 --- /dev/null +++ b/test/tokens/interfaces/IERC2981.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (interfaces/IERC2981.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface for the NFT Royalty Standard. + * + * A standardized way to retrieve royalty payment information for non-fungible tokens (NFTs) to enable universal + * support for royalty payments across all NFT marketplaces and ecosystem participants. + * + * _Available since v4.5._ + */ +interface IERC2981 { + /** + * @dev Returns how much royalty is owed and to whom, based on a sale price that may be denominated in any unit of + * exchange. The royalty amount is denominated and should be paid in that same unit of exchange. + */ + function royaltyInfo(uint256 tokenId, uint256 salePrice) + external + view + returns (address receiver, uint256 royaltyAmount); +}