diff --git a/packages/deploy/deploy/500_marketplace/500_deploy_royalties_registry.ts b/packages/deploy/deploy/500_marketplace/500_deploy_royalties_registry.ts index d628529c85..7c4805b3f5 100644 --- a/packages/deploy/deploy/500_marketplace/500_deploy_royalties_registry.ts +++ b/packages/deploy/deploy/500_marketplace/500_deploy_royalties_registry.ts @@ -10,7 +10,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { await deploy('RoyaltiesRegistry', { from: deployer, contract: - '@sandbox-smart-contracts/marketplace/contracts/RoyaltiesRegistry.sol:RoyaltiesRegistry', + '@sandbox-smart-contracts/marketplace@1.0.1/contracts/RoyaltiesRegistry.sol:RoyaltiesRegistry', proxy: { owner: upgradeAdmin, proxyContract: 'OpenZeppelinTransparentProxy', diff --git a/packages/deploy/deploy/500_marketplace/502_deploy_order_validator.ts b/packages/deploy/deploy/500_marketplace/502_deploy_order_validator.ts index e6236efe24..28e798a012 100644 --- a/packages/deploy/deploy/500_marketplace/502_deploy_order_validator.ts +++ b/packages/deploy/deploy/500_marketplace/502_deploy_order_validator.ts @@ -14,7 +14,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { await deploy('OrderValidator', { from: deployer, contract: - '@sandbox-smart-contracts/marketplace/contracts/OrderValidator.sol:OrderValidator', + '@sandbox-smart-contracts/marketplace@1.0.1/contracts/OrderValidator.sol:OrderValidator', proxy: { owner: upgradeAdmin, proxyContract: 'OpenZeppelinTransparentProxy', diff --git a/packages/deploy/deploy/500_marketplace/503_order_validator_setup.ts b/packages/deploy/deploy/500_marketplace/503_order_validator_setup.ts index f556110820..4131639c68 100644 --- a/packages/deploy/deploy/500_marketplace/503_order_validator_setup.ts +++ b/packages/deploy/deploy/500_marketplace/503_order_validator_setup.ts @@ -13,7 +13,12 @@ const func: DeployFunction = async function ( const TSB_ROLE = await read('OrderValidator', 'TSB_ROLE'); const PARTNER_ROLE = await read('OrderValidator', 'PARTNER_ROLE'); - const SandContract = await deployments.get('PolygonSand'); + let SandContract; + if (hre.network.name === 'polygon') { + SandContract = await deployments.get('PolygonSand'); + } else { + SandContract = await deployments.get('Sand'); + } const AssetContract = await deployments.get('Asset'); const addressesToGrant = []; @@ -110,5 +115,6 @@ func.tags = [ func.dependencies = [ 'OrderValidator_deploy', 'PolygonSand_deploy', + 'Sand_deploy', 'Asset_deploy', ]; diff --git a/packages/deploy/deploy/500_marketplace/504_deploy_exchange.ts b/packages/deploy/deploy/500_marketplace/504_deploy_exchange.ts index 99c661e949..6283a24857 100644 --- a/packages/deploy/deploy/500_marketplace/504_deploy_exchange.ts +++ b/packages/deploy/deploy/500_marketplace/504_deploy_exchange.ts @@ -18,7 +18,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { await deploy('Exchange', { from: deployer, - contract: `@sandbox-smart-contracts/marketplace/contracts/Exchange.sol:Exchange`, + contract: `@sandbox-smart-contracts/marketplace@1.0.1/contracts/Exchange.sol:Exchange`, proxy: { owner: upgradeAdmin, proxyContract: 'OpenZeppelinTransparentProxy', diff --git a/packages/deploy/deploy/500_marketplace/505_upgrade_royalties_registry.ts b/packages/deploy/deploy/500_marketplace/505_upgrade_royalties_registry.ts new file mode 100644 index 0000000000..6ead1f5d49 --- /dev/null +++ b/packages/deploy/deploy/500_marketplace/505_upgrade_royalties_registry.ts @@ -0,0 +1,36 @@ +import {DeployFunction} from 'hardhat-deploy/types'; +import {HardhatRuntimeEnvironment} from 'hardhat/types'; +import {DEPLOY_TAGS} from '../../hardhat.config'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const {deployments, getNamedAccounts} = hre; + const {deploy, catchUnknownSigner} = deployments; + const {deployer, upgradeAdmin} = await getNamedAccounts(); + await catchUnknownSigner( + deploy('RoyaltiesRegistry', { + from: deployer, + contract: + '@sandbox-smart-contracts/marketplace/contracts/RoyaltiesRegistry.sol:RoyaltiesRegistry', + proxy: { + owner: upgradeAdmin, + proxyContract: 'OpenZeppelinTransparentProxy', + upgradeIndex: 1, + }, + log: true, + }) + ); +}; + +export default func; +func.tags = [ + 'RoyaltiesRegistry', + 'RoyaltiesRegistry', + 'RoyaltiesRegistryV2_deploy', + DEPLOY_TAGS.L1, + DEPLOY_TAGS.L1_PROD, + DEPLOY_TAGS.L1_TEST, + DEPLOY_TAGS.L2, + DEPLOY_TAGS.L2_PROD, + DEPLOY_TAGS.L2_TEST, +]; +func.dependencies = ['RoyaltiesRegistry_deploy']; diff --git a/packages/deploy/deploy/500_marketplace/506_upgrade_order_validator.ts b/packages/deploy/deploy/500_marketplace/506_upgrade_order_validator.ts new file mode 100644 index 0000000000..2f96e749de --- /dev/null +++ b/packages/deploy/deploy/500_marketplace/506_upgrade_order_validator.ts @@ -0,0 +1,36 @@ +import {DeployFunction} from 'hardhat-deploy/types'; +import {HardhatRuntimeEnvironment} from 'hardhat/types'; +import {DEPLOY_TAGS} from '../../hardhat.config'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const {deployments, getNamedAccounts} = hre; + const {deploy, catchUnknownSigner} = deployments; + const {deployer, upgradeAdmin} = await getNamedAccounts(); + await catchUnknownSigner( + deploy('OrderValidator', { + from: deployer, + contract: + '@sandbox-smart-contracts/marketplace/contracts/OrderValidator.sol:OrderValidator', + proxy: { + owner: upgradeAdmin, + proxyContract: 'OpenZeppelinTransparentProxy', + upgradeIndex: 1, + }, + log: true, + }) + ); +}; + +export default func; +func.tags = [ + 'OrderValidator', + 'OrderValidator', + 'OrderValidatorV2_deploy', + DEPLOY_TAGS.L1, + DEPLOY_TAGS.L1_PROD, + DEPLOY_TAGS.L1_TEST, + DEPLOY_TAGS.L2, + DEPLOY_TAGS.L2_PROD, + DEPLOY_TAGS.L2_TEST, +]; +func.dependencies = ['OrderValidator_deploy']; diff --git a/packages/deploy/deploy/500_marketplace/507_upgrade_exchange.ts b/packages/deploy/deploy/500_marketplace/507_upgrade_exchange.ts new file mode 100644 index 0000000000..441aef723d --- /dev/null +++ b/packages/deploy/deploy/500_marketplace/507_upgrade_exchange.ts @@ -0,0 +1,35 @@ +import {DeployFunction} from 'hardhat-deploy/types'; +import {HardhatRuntimeEnvironment} from 'hardhat/types'; +import {DEPLOY_TAGS} from '../../hardhat.config'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const {deployments, getNamedAccounts} = hre; + const {deploy, catchUnknownSigner} = deployments; + const {deployer, upgradeAdmin} = await getNamedAccounts(); + await catchUnknownSigner( + deploy('Exchange', { + from: deployer, + contract: `@sandbox-smart-contracts/marketplace/contracts/Exchange.sol:Exchange`, + proxy: { + owner: upgradeAdmin, + proxyContract: 'OpenZeppelinTransparentProxy', + upgradeIndex: 1, + }, + log: true, + }) + ); +}; + +export default func; +func.tags = [ + 'Exchange', + 'Exchange', + 'ExchangeV2_deploy', + DEPLOY_TAGS.L1, + DEPLOY_TAGS.L1_PROD, + DEPLOY_TAGS.L1_TEST, + DEPLOY_TAGS.L2, + DEPLOY_TAGS.L2_PROD, + DEPLOY_TAGS.L2_TEST, +]; +func.dependencies = ['Exchange_deploy']; diff --git a/packages/deploy/deploy/500_marketplace/505_exchange_setup.ts b/packages/deploy/deploy/500_marketplace/508_exchange_setup.ts similarity index 65% rename from packages/deploy/deploy/500_marketplace/505_exchange_setup.ts rename to packages/deploy/deploy/500_marketplace/508_exchange_setup.ts index 83bc8be94c..f4b1f9f6c7 100644 --- a/packages/deploy/deploy/500_marketplace/505_exchange_setup.ts +++ b/packages/deploy/deploy/500_marketplace/508_exchange_setup.ts @@ -11,7 +11,16 @@ const func: DeployFunction = async function ( const ERC1776_OPERATOR_ROLE = await read('Exchange', 'ERC1776_OPERATOR_ROLE'); const EXCHANGE_ADMIN_ROLE = await read('Exchange', 'EXCHANGE_ADMIN_ROLE'); - const sandContract = await deployments.get('PolygonSand'); + + let sandContract; + let landContract; + if (hre.network.name === 'polygon') { + sandContract = await deployments.get('PolygonSand'); + landContract = await deployments.get('PolygonLand'); + } else { + sandContract = await deployments.get('Sand'); + landContract = await deployments.get('Land'); + } const hasERC1776OperatorRole = await read( 'Exchange', @@ -48,6 +57,18 @@ const func: DeployFunction = async function ( ) ); } + + const currentLand = await read('Exchange', 'landContract'); + if (currentLand != landContract.address) { + await catchUnknownSigner( + execute( + 'Exchange', + {from: sandAdmin, log: true}, + 'setLandContract', + landContract.address + ) + ); + } }; export default func; @@ -61,4 +82,13 @@ func.tags = [ DEPLOY_TAGS.L2_PROD, DEPLOY_TAGS.L2_TEST, ]; -func.dependencies = ['Exchange_deploy', 'PolygonSand_deploy']; +func.dependencies = [ + 'Exchange_deploy', + 'Sand_deploy', + 'Land_deploy', + 'PolygonSand_deploy', + 'PolygonLand_deploy', + 'PolygonLandV2_deploy', + 'LandV4_deploy', + 'ExchangeV2_deploy', +]; diff --git a/packages/deploy/hardhat.config.ts b/packages/deploy/hardhat.config.ts index 21fbc671a3..8aa28324b9 100644 --- a/packages/deploy/hardhat.config.ts +++ b/packages/deploy/hardhat.config.ts @@ -32,10 +32,17 @@ const importedPackages = { ], '@sandbox-smart-contracts/giveaway': 'contracts/SignedMultiGiveaway.sol', '@sandbox-smart-contracts/faucets': 'contracts/FaucetsERC1155.sol', + '@sandbox-smart-contracts/marketplace@1.0.1': [ + 'contracts/RoyaltiesRegistry.sol', + 'contracts/OrderValidator.sol', + 'contracts/Exchange.sol', + 'contracts/TransferManager.sol', + ], '@sandbox-smart-contracts/marketplace': [ 'contracts/RoyaltiesRegistry.sol', 'contracts/OrderValidator.sol', 'contracts/Exchange.sol', + 'contracts/TransferManager.sol', ], '@sandbox-smart-contracts/dependency-operator-filter': [ 'contracts/OperatorFilterSubscription.sol', diff --git a/packages/deploy/package.json b/packages/deploy/package.json index a03493e8a9..da5a7b521d 100644 --- a/packages/deploy/package.json +++ b/packages/deploy/package.json @@ -25,7 +25,8 @@ "@sandbox-smart-contracts/faucets": "0.0.1", "@sandbox-smart-contracts/giveaway": "0.0.3", "@sandbox-smart-contracts/land": "1.0.0-rc.1", - "@sandbox-smart-contracts/marketplace": "1.0.1", + "@sandbox-smart-contracts/marketplace": "1.0.2", + "@sandbox-smart-contracts/marketplace@1.0.1": "npm:@sandbox-smart-contracts/marketplace@1.0.1", "@sandbox-smart-contracts/oft-sand": "0.0.1" }, "files": [ diff --git a/packages/land/contracts/interfaces/ILandToken.sol b/packages/land/contracts/interfaces/ILandToken.sol index 7ebf66cd53..e595e63611 100644 --- a/packages/land/contracts/interfaces/ILandToken.sol +++ b/packages/land/contracts/interfaces/ILandToken.sol @@ -23,8 +23,4 @@ interface ILandToken is IQuad { /// @param y y coordinate /// @return does the LAND exist function exists(uint256 size, uint256 x, uint256 y) external view returns (bool); - - /// @notice total width of the map - /// @return width - function width() external pure returns (uint256); } diff --git a/packages/marketplace/contracts/Exchange.sol b/packages/marketplace/contracts/Exchange.sol index 77ad786388..f942f6ae97 100644 --- a/packages/marketplace/contracts/Exchange.sol +++ b/packages/marketplace/contracts/Exchange.sol @@ -2,9 +2,9 @@ pragma solidity 0.8.23; -import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; +import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; import {ERC2771HandlerUpgradeable} from "@sandbox-smart-contracts/dependency-metatx/contracts/ERC2771HandlerUpgradeable.sol"; import {IOrderValidator} from "./interfaces/IOrderValidator.sol"; @@ -14,6 +14,7 @@ import {ExchangeCore} from "./ExchangeCore.sol"; /// @author The Sandbox /// @title Exchange contract with meta transactions +/// @custom:security-contact contact-blockchain@sandbox.game /// @notice Used to exchange assets, that is, tokens. /// @dev Main functions are in ExchangeCore /// @dev TransferManager is used to execute token transfers diff --git a/packages/marketplace/contracts/OrderValidator.sol b/packages/marketplace/contracts/OrderValidator.sol index 5155034d8a..d36a432904 100644 --- a/packages/marketplace/contracts/OrderValidator.sol +++ b/packages/marketplace/contracts/OrderValidator.sol @@ -7,7 +7,7 @@ import {LibAsset} from "./libraries/LibAsset.sol"; import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; import {EIP712Upgradeable, Initializable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; -import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; +import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; import {IOrderValidator} from "./interfaces/IOrderValidator.sol"; import {Whitelist} from "./Whitelist.sol"; @@ -64,6 +64,9 @@ contract OrderValidator is IOrderValidator, Initializable, EIP712Upgradeable, ER require(order.maker.isValidSignatureNow(_hashTypedDataV4(hash), signature), "signature verification error"); } + /// @notice Check if the contract supports an interface + /// @param interfaceId The id of the interface + /// @return true if the interface is supported function supportsInterface( bytes4 interfaceId ) diff --git a/packages/marketplace/contracts/RoyaltiesRegistry.sol b/packages/marketplace/contracts/RoyaltiesRegistry.sol index 46aa89d644..142f2bb051 100644 --- a/packages/marketplace/contracts/RoyaltiesRegistry.sol +++ b/packages/marketplace/contracts/RoyaltiesRegistry.sol @@ -63,7 +63,7 @@ contract RoyaltiesRegistry is OwnableUpgradeable, IRoyaltiesProvider, ERC165Upgr /// @notice Royalties registry initializer function initialize() external initializer { - __Ownable_init(_msgSender()); + __Ownable_init(); } /// @notice Assigns an external provider for a token's royalties and sets the royalty type as 'EXTERNAL_PROVIDER' (2). @@ -158,6 +158,9 @@ contract RoyaltiesRegistry is OwnableUpgradeable, IRoyaltiesProvider, ERC165Upgr return new Part[](0); } + /// @notice Check if the contract supports an interface + /// @param interfaceId The id of the interface + /// @return true if the interface is supported function supportsInterface( bytes4 interfaceId ) public view virtual override(ERC165Upgradeable, IRoyaltiesProvider) returns (bool) { diff --git a/packages/marketplace/contracts/TransferManager.sol b/packages/marketplace/contracts/TransferManager.sol index f44efd664e..b7954decd4 100644 --- a/packages/marketplace/contracts/TransferManager.sol +++ b/packages/marketplace/contracts/TransferManager.sol @@ -33,6 +33,9 @@ abstract contract TransferManager is Initializable, ITransferManager { /// @notice cannot exceed 50% == 0.5 * BASE_POINTS == 5000 uint256 internal constant ROYALTY_SHARE_LIMIT = 5000; + /// @dev grid size of the land used to calculate ids + uint256 internal constant GRID_SIZE = 408; + /// @notice Fee applied to primary sales. /// @return uint256 of primary sale fee in PROTOCOL_FEE_MULTIPLIER units uint256 public protocolFeePrimary; @@ -144,8 +147,10 @@ abstract contract TransferManager is Initializable, ITransferManager { /// @notice Sets the LAND contract address. /// @param newLandContractAddress Address of new LAND contract function _setLandContract(ILandToken newLandContractAddress) internal { - // TODO: uncomment when ILandToken is supported by LandBase - // require(ERC165Checker.supportsInterface(address(newLandContractAddress, type(ILandToken).interfaceId), "invalid LAND address"); + require( + ERC165Checker.supportsInterface(address(newLandContractAddress), type(ILandToken).interfaceId), + "Invalid LAND address" + ); landContract = newLandContractAddress; emit LandContractSet(newLandContractAddress); @@ -277,6 +282,13 @@ abstract contract TransferManager is Initializable, ITransferManager { return remainder; } + /// @notice Apply and transfer royalties based on the asset price and royalties information. + /// @param remainder How much of the amount left after previous transfers + /// @param paymentSide DealSide of the fee-side order + /// @param assetPrice The price of the asset for which royalties are being calculated. + /// @param royalties The array of royalty recipients and their respective basis points. + /// @param recipient The recipient who will receive the remainder after royalties are deducted. + /// @return How much left after paying royalties function _applyRoyalties( uint256 remainder, DealSide memory paymentSide, @@ -309,6 +321,7 @@ abstract contract TransferManager is Initializable, ITransferManager { /// @notice Do a transfer based on a percentage (in basis points) /// @param remainder How much of the amount left after previous transfers /// @param paymentSide DealSide of the fee-side order + /// @param assetPrice The price of the asset for which royalties are being calculated. /// @param to Account that will receive the asset /// @param percentage Percentage to be transferred multiplied by the multiplier /// @param multiplier Percentage is multiplied by this number to avoid rounding (2.5% == 0.025) * multiplier @@ -441,7 +454,7 @@ abstract contract TransferManager is Initializable, ITransferManager { IERC1155(token).safeTransferFrom(from, to, id, supply, ""); } - /// @notice Function deciding if the fees are applied or not, to be override + /// @notice Function deciding if the fees are applied or not, to be overridden /// @param from Address to check function _mustSkipFees(address from) internal virtual returns (bool); @@ -454,11 +467,11 @@ abstract contract TransferManager is Initializable, ITransferManager { /// @dev this method is gas optimized, must be called with verified x,y and size, after a call to _isValidQuad function idInPath(uint256 i, uint256 size, uint256 x, uint256 y) internal view returns (uint256) { unchecked { - return (x + (i % size)) + (y + (i / size)) * landContract.width(); + return (x + (i % size)) + (y + (i / size)) * GRID_SIZE; } } - /// @notice Function deciding if the seller is a TSB seller, to be override + /// @notice Function deciding if the seller is a TSB seller, to be overridden /// @param from Address to check function _isTSBSeller(address from) internal virtual returns (bool); diff --git a/packages/marketplace/contracts/Whitelist.sol b/packages/marketplace/contracts/Whitelist.sol index 0f4190707b..0847a6f473 100644 --- a/packages/marketplace/contracts/Whitelist.sol +++ b/packages/marketplace/contracts/Whitelist.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: MIT + pragma solidity 0.8.23; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; +import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; import {IWhitelist} from "./interfaces/IWhitelist.sol"; /// @author The Sandbox diff --git a/packages/marketplace/contracts/interfaces/IOrderValidator.sol b/packages/marketplace/contracts/interfaces/IOrderValidator.sol index be96ceacd7..dd72abfc5f 100644 --- a/packages/marketplace/contracts/interfaces/IOrderValidator.sol +++ b/packages/marketplace/contracts/interfaces/IOrderValidator.sol @@ -14,5 +14,8 @@ interface IOrderValidator { /// @param sender Order sender function validate(LibOrder.Order memory order, bytes memory signature, address sender) external view; + /// @notice Check if the contract supports an interface + /// @param interfaceId The id of the interface + /// @return true if the interface is supported function supportsInterface(bytes4 interfaceId) external view returns (bool); } diff --git a/packages/marketplace/contracts/interfaces/IRoyaltiesProvider.sol b/packages/marketplace/contracts/interfaces/IRoyaltiesProvider.sol index 85bd7859b5..53981d929d 100644 --- a/packages/marketplace/contracts/interfaces/IRoyaltiesProvider.sol +++ b/packages/marketplace/contracts/interfaces/IRoyaltiesProvider.sol @@ -21,5 +21,8 @@ interface IRoyaltiesProvider { /// @return A part with all royalties for token function getRoyalties(address token, uint256 tokenId) external returns (Part[] memory); + /// @notice Check if the contract supports an interface + /// @param interfaceId The id of the interface + /// @return true if the interface is supported function supportsInterface(bytes4 interfaceId) external view returns (bool); } diff --git a/packages/marketplace/contracts/interfaces/IWhitelist.sol b/packages/marketplace/contracts/interfaces/IWhitelist.sol index 8add166a62..018d2b4789 100644 --- a/packages/marketplace/contracts/interfaces/IWhitelist.sol +++ b/packages/marketplace/contracts/interfaces/IWhitelist.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT + pragma solidity 0.8.23; /// @author The Sandbox diff --git a/packages/marketplace/contracts/libraries/LibAsset.sol b/packages/marketplace/contracts/libraries/LibAsset.sol index dba70d9fdd..2394eed7a8 100644 --- a/packages/marketplace/contracts/libraries/LibAsset.sol +++ b/packages/marketplace/contracts/libraries/LibAsset.sol @@ -36,19 +36,19 @@ library LibAsset { uint256 value; // The amount or value of the asset. } - /// @dev Represents a group (i.e. bundle) of ERC20 assets on the Ethereum blockchain. + /// @dev Represents a group (i.e. bundle) of ERC20 asset. struct BundledERC20 { address erc20Address; uint256 value; } - /// @dev Represents a group (i.e. bundle) of ERC721 assets on the Ethereum blockchain. + /// @dev Represents a group (i.e. bundle) of ERC721 assets. struct BundledERC721 { address erc721Address; uint256[] ids; } - /// @dev Represents a group (i.e. bundle) of ERC1155 assets on the Ethereum blockchain. + /// @dev Represents a group (i.e. bundle) of ERC1155 assets. struct BundledERC1155 { address erc1155Address; uint256[] ids; @@ -63,7 +63,7 @@ library LibAsset { bytes data; } - /// @dev Represents a group (i.e. bundle) of assets on the Ethereum blockchain with its types and values. + /// @dev Represents a group (i.e. bundle) of assets with its types and values. struct Bundle { BundledERC20[] bundledERC20; BundledERC721[] bundledERC721; @@ -157,7 +157,7 @@ library LibAsset { return abi.decode(assetType.data, (Bundle)); } - /// @dev function to verify if the order is bundle and validate the bundle price + /// @dev function to verify if the order is a bundle and validate the bundle price /// @param leftAsset The left asset. /// @param rightAsset The right asset. function verifyPriceDistribution(Asset memory leftAsset, Asset memory rightAsset) internal pure { diff --git a/packages/marketplace/contracts/mocks/LandMetadataRegistryMock.sol b/packages/marketplace/contracts/mocks/LandMetadataRegistryMock.sol index ca065350c8..9f5494c493 100644 --- a/packages/marketplace/contracts/mocks/LandMetadataRegistryMock.sol +++ b/packages/marketplace/contracts/mocks/LandMetadataRegistryMock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; +import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; import {IErrors} from "@sandbox-smart-contracts/land/contracts/interfaces/IErrors.sol"; import {ILandMetadataRegistry} from "@sandbox-smart-contracts/land/contracts/interfaces/ILandMetadataRegistry.sol"; import {LandMetadataBase} from "@sandbox-smart-contracts/land/contracts/registry/LandMetadataBase.sol"; diff --git a/packages/marketplace/docs/Exchange.md b/packages/marketplace/docs/Exchange.md index da6152164e..d52590a1b3 100644 --- a/packages/marketplace/docs/Exchange.md +++ b/packages/marketplace/docs/Exchange.md @@ -4,7 +4,9 @@ The [Exchange contract](../contracts/Exchange.sol) is the entrypoint and main contract to the marketplace protocol. It safely offers a decentralized way to -exchange tokens of any nature (ERC20, ERC1155, ERC721) using signed orders. +exchange tokens of any nature (ERC20, ERC1155, ERC721) using signed orders.It +also supports exchanging bundles of assets, which can include multiple ERC20, +ERC1155, ERC721, and Quads. ## Concepts @@ -24,6 +26,29 @@ The order represents this intent and includes the address of the seller, the address of the NFT, the token id, the address of the ERC20 and the amount the seller is asking for (and more). +### Bundle Order + +In addition to single-asset orders, the protocol supports bundle orders, where +multiple assets are grouped together as a single entity for exchange. Let's +consider this use case: + +Order B + +``` +Alice wants to sell a bundle of assets: + 1 LAND (ERC721) with token id 1000 + - Price: 1200 MATIC + 10 ASSET (ERC1155) with token id 2000 + - Price: 500 MATIC + 50 SAND (ERC20) + - Price: 300 MATIC (6 MATIC per SAND) +against 2000 MATIC (ERC20). +``` + +The order represents this intent and includes the address of the seller, the +address for the tokens(ERC721 and ERC1155), the token ids, the individual prices +of the assets, the address of the ERC20 and the amount the seller is asking for. + ### Maker & Taker The maker and taker represent the 2 parties of the exchange. Each order can @@ -267,6 +292,21 @@ collection or token after a sale. The protocol handles multiple types of royalties (ERC2981, royalties registry, external provider). See the [RoyaltiesRegistry](RoyaltiesRegistry.md) contract for more information. +### Royalties for Bundles + +For bundle orders, royalties are calculated individually for each asset type +within the bundle. Here's how it works: + +- ERC721: The royalty is calculated on the price of each individual ERC721 asset + within the bundle. The royalty fee is then deducted from the price of that + specific ERC721 asset. +- ERC1155: The royalty is calculated based on the asset price for each ERC1155 + token within the bundle. The royalty fee is then deducted from this asset + price. +- Quads: The price of Quad within the bundle is determined by the combined price + of all land in the Quad. The royalty is then calculated using the royalty + fetched for the land token associated with the leftmost land in the Quad. + ### Payouts The payouts define what is due for each party of the exchange. For the buyer diff --git a/packages/marketplace/package.json b/packages/marketplace/package.json index 9e85ead828..76a6d23ec3 100644 --- a/packages/marketplace/package.json +++ b/packages/marketplace/package.json @@ -1,95 +1,95 @@ { - "name": "@sandbox-smart-contracts/marketplace", - "version": "1.0.2", - "description": "", - "mocha": { - "require": "hardhat/register", - "timeout": 40000, - "_": [ - "test/**/*.ts" - ] + "name": "@sandbox-smart-contracts/marketplace", + "version": "1.0.2", + "description": "", + "mocha": { + "require": "hardhat/register", + "timeout": 40000, + "_": [ + "test/**/*.ts" + ] + }, + "files": [ + "contracts", + "docs", + "README.md", + "CHANGELOG.md" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@nomicfoundation/hardhat-chai-matchers": "^2.0.1", + "@nomicfoundation/hardhat-ethers": "^3.0.3", + "@nomicfoundation/hardhat-network-helpers": "^1.0.8", + "@nomicfoundation/hardhat-toolbox": "^3.0.0", + "@nomicfoundation/hardhat-verify": "^1.0.0", + "@nomiclabs/hardhat-etherscan": "^3.1.7", + "@openzeppelin/hardhat-upgrades": "^2.2.1", + "@release-it/keep-a-changelog": "^4.0.0", + "@typechain/ethers-v6": "^0.4.0", + "@typechain/hardhat": "^8.0.0", + "@types/chai": "^4.3.6", + "@types/mocha": "^10.0.1", + "@types/node": "^20.2.5", + "@typescript-eslint/eslint-plugin": "^5.59.8", + "@typescript-eslint/parser": "^5.59.8", + "chai": "^4.3.7", + "eslint": "^8.41.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-mocha": "^10.1.0", + "eslint-plugin-prettier": "^4.2.1", + "ethers": "^6.6.2", + "hardhat": "^2.14.1", + "hardhat-contract-sizer": "^2.10.0", + "hardhat-gas-reporter": "^1.0.9", + "mocha": "^10.2.0", + "prettier": "^2.8.8", + "prettier-plugin-solidity": "^1.1.3", + "release-it": "^16.2.1", + "solhint": "^3.6.2", + "solhint-plugin-prettier": "^0.0.5", + "solidity-coverage": "^0.8.3", + "ts-node": "^10.9.1", + "typechain": "^8.2.0", + "typescript": "5.0.4" + }, + "scripts": { + "lint": "eslint --max-warnings 0 \"**/*.{js,ts}\" && solhint --max-warnings 0 \"contracts/**/*.sol\"", + "lint:fix": "eslint --fix \"**/*.{js,ts}\" && solhint --fix \"contracts/**/*.sol\"", + "format": "prettier --check \"**/*.{ts,js,sol,md}\"", + "format:fix": "prettier --write \"**/*.{ts,js,sol,md}\"", + "test": "hardhat test", + "coverage": "hardhat coverage --testfiles 'test/*.ts''test/*.js'", + "analyze": "slither .", + "hardhat": "hardhat", + "compile": "hardhat compile", + "release": "release-it" + }, + "release-it": { + "git": { + "commitMessage": "chore: @sandbox-smart-contracts/marketplace release v${version}", + "tagAnnotation": "@sandbox-smart-contracts/marketplace release v${version}", + "tagName": "@sandbox-smart-contracts/marketplace@v${version}" }, - "files": [ - "contracts", - "docs", - "README.md", - "CHANGELOG.md" - ], - "publishConfig": { - "access": "public" + "plugins": { + "@release-it/keep-a-changelog": {} }, - "devDependencies": { - "@nomicfoundation/hardhat-chai-matchers": "^2.0.1", - "@nomicfoundation/hardhat-ethers": "^3.0.3", - "@nomicfoundation/hardhat-network-helpers": "^1.0.8", - "@nomicfoundation/hardhat-toolbox": "^3.0.0", - "@nomicfoundation/hardhat-verify": "^1.0.0", - "@nomiclabs/hardhat-etherscan": "^3.1.7", - "@openzeppelin/hardhat-upgrades": "^2.2.1", - "@release-it/keep-a-changelog": "^4.0.0", - "@typechain/ethers-v6": "^0.4.0", - "@typechain/hardhat": "^8.0.0", - "@types/chai": "^4.3.6", - "@types/mocha": "^10.0.1", - "@types/node": "^20.2.5", - "@typescript-eslint/eslint-plugin": "^5.59.8", - "@typescript-eslint/parser": "^5.59.8", - "chai": "^4.3.7", - "eslint": "^8.41.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-mocha": "^10.1.0", - "eslint-plugin-prettier": "^4.2.1", - "ethers": "^6.6.2", - "hardhat": "^2.14.1", - "hardhat-contract-sizer": "^2.10.0", - "hardhat-gas-reporter": "^1.0.9", - "mocha": "^10.2.0", - "prettier": "^2.8.8", - "prettier-plugin-solidity": "^1.1.3", - "release-it": "^16.2.1", - "solhint": "^3.6.2", - "solhint-plugin-prettier": "^0.0.5", - "solidity-coverage": "^0.8.3", - "ts-node": "^10.9.1", - "typechain": "^8.2.0", - "typescript": "5.0.4" - }, - "scripts": { - "lint": "eslint --max-warnings 0 \"**/*.{js,ts}\" && solhint --max-warnings 0 \"contracts/**/*.sol\"", - "lint:fix": "eslint --fix \"**/*.{js,ts}\" && solhint --fix \"contracts/**/*.sol\"", - "format": "prettier --check \"**/*.{ts,js,sol,md}\"", - "format:fix": "prettier --write \"**/*.{ts,js,sol,md}\"", - "test": "hardhat test", - "coverage": "hardhat coverage --testfiles 'test/*.ts''test/*.js'", - "analyze": "slither .", - "hardhat": "hardhat", - "compile": "hardhat compile", - "release": "release-it" - }, - "release-it": { - "git": { - "commitMessage": "chore: @sandbox-smart-contracts/marketplace release v${version}", - "tagAnnotation": "@sandbox-smart-contracts/marketplace release v${version}", - "tagName": "@sandbox-smart-contracts/marketplace@v${version}" - }, - "plugins": { - "@release-it/keep-a-changelog": {} - }, - "hooks": { - "before:init": [ - "yarn lint", - "yarn test" - ] - } - }, - "author": "", - "license": "ISC", - "dependencies": { - "@manifoldxyz/royalty-registry-solidity": "^3.0.0", - "@openzeppelin/contracts": "5.0.2", - "@openzeppelin/contracts-upgradeable": "5.0.2", - "@sandbox-smart-contracts/dependency-metatx": "1.0.1", - "@sandbox-smart-contracts/dependency-royalty-management": "1.0.2", - "@sandbox-smart-contracts/land": "1.0.0-rc.1" + "hooks": { + "before:init": [ + "yarn lint", + "yarn test" + ] } + }, + "author": "", + "license": "ISC", + "dependencies": { + "@manifoldxyz/royalty-registry-solidity": "^3.0.0", + "@openzeppelin/contracts": "^4.9.2", + "@openzeppelin/contracts-upgradeable": "^4.9.2", + "@sandbox-smart-contracts/dependency-metatx": "1.0.1", + "@sandbox-smart-contracts/dependency-royalty-management": "1.0.2", + "@sandbox-smart-contracts/land": "1.0.0-rc.1" + } } diff --git a/packages/marketplace/test/common/AccessControl.behavior.ts b/packages/marketplace/test/common/AccessControl.behavior.ts index b081157fd5..2280083307 100644 --- a/packages/marketplace/test/common/AccessControl.behavior.ts +++ b/packages/marketplace/test/common/AccessControl.behavior.ts @@ -7,38 +7,49 @@ import {Contract, Signer} from 'ethers'; export function checkAccessControl( functionName: string[], eventName: string[], - userContract: string[], - adminContract: string[] + contractAddress: string[] ) { describe('Access Control', function () { - let ExchangeContractAsUser: Contract, - ExchangeContractAsAdmin: Contract, + let ExchangeContractAsAdmin: Contract, + ExchangeContractAsUser: Contract, + RoyaltiesRegistryAsUser: Contract, + OrderValidatorAsUser: Contract, + TrustedForwarderAsUser: Contract, user: Signer, contractMap: {[key: string]: Contract}; beforeEach(async function () { - ({ExchangeContractAsAdmin, ExchangeContractAsUser, user} = - await loadFixture(deployFixturesWithoutWhitelist)); + ({ + ExchangeContractAsAdmin, + ExchangeContractAsUser, + RoyaltiesRegistryAsUser, + OrderValidatorAsUser, + TrustedForwarder2: TrustedForwarderAsUser, + user, + } = await loadFixture(deployFixturesWithoutWhitelist)); contractMap = { ExchangeContractAsAdmin: ExchangeContractAsAdmin, ExchangeContractAsUser: ExchangeContractAsUser, + RoyaltiesRegistryAsUser: RoyaltiesRegistryAsUser, + OrderValidatorAsUser: OrderValidatorAsUser, + TrustedForwarderAsUser: TrustedForwarderAsUser, }; }); // eslint-disable-next-line mocha/no-setup-in-describe for (let i = 0; i < functionName.length; i++) { it(`should not set ${functionName[i]} if caller is not in the role`, async function () { await expect( - contractMap[userContract[i]][functionName[i]](user.getAddress()) + ExchangeContractAsUser[functionName[i]](user.getAddress()) ).to.be.revertedWithCustomError( - await contractMap[userContract[i]], + ExchangeContractAsUser, 'AccessControlUnauthorizedAccount' ); }); it(`should be able to ${functionName[i]}`, async function () { await expect( - contractMap[adminContract[i]][functionName[i]]( - contractMap[adminContract[i]].getAddress() + ExchangeContractAsAdmin[functionName[i]]( + contractMap[contractAddress[i]].getAddress() ) ).to.emit(ExchangeContractAsAdmin, eventName[i]); }); diff --git a/packages/marketplace/test/exchange/Config.behavior.ts b/packages/marketplace/test/exchange/Config.behavior.ts index fcfee73abc..28f19ed469 100644 --- a/packages/marketplace/test/exchange/Config.behavior.ts +++ b/packages/marketplace/test/exchange/Config.behavior.ts @@ -32,30 +32,15 @@ export function exchangeConfig() { describe('default admin', function () { checkAccessControl( [ - // TODO: new tests for setRoyaltiesRegistry and setOrderValidatorContract with correct contracts supporting interfaces - // 'setRoyaltiesRegistry', - // 'setOrderValidatorContract', + 'setRoyaltiesRegistry', + 'setOrderValidatorContract', 'setTrustedForwarder', ], + ['RoyaltiesRegistrySet', 'OrderValidatorSet', 'TrustedForwarderSet'], [ - // 'RoyaltiesRegistrySet', - // 'OrderValidatorSet', - 'TrustedForwarderSet', - ], - [ - // 'ExchangeContractAsUser', - // 'ExchangeContractAsUser', - 'ExchangeContractAsUser', - ], - [ - // 'ExchangeContractAsAdmin', - // 'ExchangeContractAsAdmin', - 'ExchangeContractAsAdmin', - ], - [ - // '0x00', - // '0x00', - '0x00', + 'RoyaltiesRegistryAsUser', + 'OrderValidatorAsUser', + 'TrustedForwarderAsUser', ] ); diff --git a/packages/marketplace/test/fixtures/TrustedForwarderMock.ts b/packages/marketplace/test/fixtures/TrustedForwarderMock.ts index 5d67f3f7a1..5f841c1b34 100644 --- a/packages/marketplace/test/fixtures/TrustedForwarderMock.ts +++ b/packages/marketplace/test/fixtures/TrustedForwarderMock.ts @@ -5,6 +5,7 @@ export async function TrustedForwarderSetup() { 'TrustedForwarderMock' ); const TrustedForwarder = await TrustedForwarderFactory.deploy(); + const TrustedForwarder2 = await TrustedForwarderFactory.deploy(); - return {TrustedForwarder}; + return {TrustedForwarder, TrustedForwarder2}; } diff --git a/packages/marketplace/test/fixtures/exchange.ts b/packages/marketplace/test/fixtures/exchange.ts index 244f882793..400d3300c1 100644 --- a/packages/marketplace/test/fixtures/exchange.ts +++ b/packages/marketplace/test/fixtures/exchange.ts @@ -8,7 +8,7 @@ import {TrustedForwarderSetup} from './TrustedForwarderMock'; export async function exchangeSetup() { const {admin, user, defaultFeeReceiver} = await signerSetup(); - const {TrustedForwarder} = await TrustedForwarderSetup(); + const {TrustedForwarder, TrustedForwarder2} = await TrustedForwarderSetup(); const royaltiesRegistry = await royaltiesRegistrySetup(); const {RoyaltiesRegistryAsDeployer} = royaltiesRegistry; @@ -66,5 +66,6 @@ export async function exchangeSetup() { AssetMatcherAsUser, QuadHelper, TrustedForwarder, + TrustedForwarder2, }; } diff --git a/yarn.lock b/yarn.lock index 84ea755196..723f5142ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2888,7 +2888,8 @@ __metadata: "@sandbox-smart-contracts/faucets": 0.0.1 "@sandbox-smart-contracts/giveaway": 0.0.3 "@sandbox-smart-contracts/land": 1.0.0-rc.1 - "@sandbox-smart-contracts/marketplace": 1.0.1 + "@sandbox-smart-contracts/marketplace": 1.0.2 + "@sandbox-smart-contracts/marketplace@1.0.1": "npm:@sandbox-smart-contracts/marketplace@1.0.1" "@sandbox-smart-contracts/oft-sand": 0.0.1 "@typechain/ethers-v5": ^11.0.0 "@typechain/hardhat": ^8.0.0 @@ -3099,7 +3100,7 @@ __metadata: languageName: unknown linkType: soft -"@sandbox-smart-contracts/marketplace@npm:1.0.1": +"@sandbox-smart-contracts/marketplace@1.0.1@npm:@sandbox-smart-contracts/marketplace@1.0.1": version: 1.0.1 resolution: "@sandbox-smart-contracts/marketplace@npm:1.0.1" dependencies: @@ -3112,7 +3113,7 @@ __metadata: languageName: node linkType: hard -"@sandbox-smart-contracts/marketplace@workspace:packages/marketplace": +"@sandbox-smart-contracts/marketplace@1.0.2, @sandbox-smart-contracts/marketplace@workspace:packages/marketplace": version: 0.0.0-use.local resolution: "@sandbox-smart-contracts/marketplace@workspace:packages/marketplace" dependencies: @@ -3123,8 +3124,8 @@ __metadata: "@nomicfoundation/hardhat-toolbox": ^3.0.0 "@nomicfoundation/hardhat-verify": ^1.0.0 "@nomiclabs/hardhat-etherscan": ^3.1.7 - "@openzeppelin/contracts": 5.0.2 - "@openzeppelin/contracts-upgradeable": 5.0.2 + "@openzeppelin/contracts": ^4.9.2 + "@openzeppelin/contracts-upgradeable": ^4.9.2 "@openzeppelin/hardhat-upgrades": ^2.2.1 "@release-it/keep-a-changelog": ^4.0.0 "@sandbox-smart-contracts/dependency-metatx": 1.0.1