diff --git a/packages/deploy/test/asset/assetCreateFixture.ts b/packages/deploy/test/asset/assetCreateFixture.ts index ac7eda30a0..220bdb5b13 100644 --- a/packages/deploy/test/asset/assetCreateFixture.ts +++ b/packages/deploy/test/asset/assetCreateFixture.ts @@ -78,6 +78,17 @@ const setupAssetCreateTests = deployments.createFixture( const createBatchLazyMintSignature = (data: LazyMintBatchData) => createMultipleLazyMintSignature(data, AssetCreateContract, network); + const OrderValidatorAsAdmin = OrderValidatorContract.connect( + await ethers.provider.getSigner(assetAdmin) + ); + + const ERC20_ROLE = await OrderValidatorContract.ERC20_ROLE(); + await OrderValidatorAsAdmin.grantRole( + ERC20_ROLE, + await SandContract.getAddress() + ); + await OrderValidatorAsAdmin.disableWhitelists(); + return { AssetContract, AssetCreateContract, diff --git a/packages/marketplace/contracts/mocks/LandMock.sol b/packages/marketplace/contracts/mocks/LandMock.sol deleted file mode 100644 index 2e25e116bb..0000000000 --- a/packages/marketplace/contracts/mocks/LandMock.sol +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.23; - -import {IOperatorFilterRegistry} from "@sandbox-smart-contracts/land/contracts/interfaces/IOperatorFilterRegistry.sol"; -import {Land} from "@sandbox-smart-contracts/land/contracts/Land.sol"; - -contract LandMock is Land { - bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; - - function simulateUpgrade(address admin) external { - InitializableStorage storage $; - // solhint-disable-next-line no-inline-assembly - assembly { - $.slot := INITIALIZABLE_STORAGE - } - $._initialized = 0; - $._initializing = false; - _writeAdmin(admin); - } - - struct VarsStorage { - uint256 _admin; - uint256 _superOperators; - uint256 _metaTransactionContracts; - uint256 _numNFTPerAddress; - uint256 _owners; - uint256 _operatorsForAll; - uint256 _operators; - uint256 _initialized; // not used after the upgrade - uint256 _minters; - uint256 operatorFilterRegistry; - } - - /// @notice sets Approvals with operator filterer check in case to test the transfer. - /// @param operator address of the operator to be approved - /// @param approved bool value denoting approved (true) or not Approved(false) - function setApprovalForAllWithOutFilter(address operator, bool approved) external { - super._setApprovalForAll(msg.sender, operator, approved); - } - - /// @notice This function is used to register Land contract on the Operator Filterer Registry of Opensea.can only be called by admin. - /// @dev used to register contract and subscribe to the subscriptionOrRegistrantToCopy's black list. - /// @param subscriptionOrRegistrantToCopy registration address of the list to subscribe. - /// @param subscribe bool to signify subscription "true"" or to copy the list "false". - function registerFilterer(address subscriptionOrRegistrantToCopy, bool subscribe) external { - _register(subscriptionOrRegistrantToCopy, subscribe); - } - - function getStorageStructure() external pure returns (VarsStorage memory ret) { - // solhint-disable-next-line no-inline-assembly - assembly { - let i := 0 - mstore(add(ret, i), _admin.slot) - i := add(i, 0x20) - mstore(add(ret, i), _superOperators.slot) - i := add(i, 0x20) - mstore(add(ret, i), _metaTransactionContracts.slot) - i := add(i, 0x20) - mstore(add(ret, i), _numNFTPerAddress.slot) - i := add(i, 0x20) - mstore(add(ret, i), _owners.slot) - i := add(i, 0x20) - mstore(add(ret, i), _operatorsForAll.slot) - i := add(i, 0x20) - mstore(add(ret, i), _operators.slot) - i := add(i, 0x20) - mstore(add(ret, i), _initialized.slot) - i := add(i, 0x20) - mstore(add(ret, i), _minters.slot) - i := add(i, 0x20) - mstore(add(ret, i), _operatorFilterRegistry.slot) - i := add(i, 0x20) - } - } - - /// @notice Burns token `id`. - /// @param id token which will be burnt. - function burn(uint256 id) external { - _burn(_msgSender(), id); - } - - /// @notice Burn token`id` from `from`. - /// @param from address whose token is to be burnt. - /// @param id token which will be burnt. - function burnFrom(address from, uint256 id) external { - _burn(from, id); - } - - /// @dev just to get 100% coverage report - function writeMixingForCoverage( - address admin, - address superOperator, - address owner, - uint256 quantity, - uint256 tokenId, - uint256 ownerData, - address operator, - address minter, - IOperatorFilterRegistry registry - ) external { - _writeAdmin(admin); - _writeSuperOperator(superOperator, true); - _writeNumNFTPerAddress(owner, quantity); - _writeOwnerData(tokenId, ownerData); - _writeOperatorForAll(owner, operator, true); - _writeOperator(tokenId, operator); - _writeMinter(minter, true); - _writeOperatorFilterRegistry(registry); - } -} diff --git a/packages/marketplace/contracts/mocks/land/ERC721BaseToken.sol b/packages/marketplace/contracts/mocks/land/ERC721BaseToken.sol new file mode 100644 index 0000000000..fd6c1ee6c8 --- /dev/null +++ b/packages/marketplace/contracts/mocks/land/ERC721BaseToken.sol @@ -0,0 +1,405 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {IERC721MandatoryTokenReceiver} from "@sandbox-smart-contracts/land/contracts/interfaces/IERC721MandatoryTokenReceiver.sol"; +import {IErrors} from "./IErrors.sol"; +import {IERC721BatchOps} from "@sandbox-smart-contracts/land/contracts/interfaces/IERC721BatchOps.sol"; +import {WithSuperOperators} from "./WithSuperOperators.sol"; + +/// @title ERC721BaseTokenCommon +/// @author The Sandbox +/// @custom:security-contact contact-blockchain@sandbox.game +/// @notice Basic functionalities of a NFT +/// @dev ERC721 implementation that supports meta-transactions and super operators +abstract contract ERC721BaseToken is IERC721, IERC721BatchOps, IErrors, Context, WithSuperOperators { + using Address for address; + + uint256 internal constant NOT_ADDRESS = 0xFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000; + uint256 internal constant OPERATOR_FLAG = (2 ** 255); + uint256 internal constant NOT_OPERATOR_FLAG = OPERATOR_FLAG - 1; + uint256 internal constant BURNED_FLAG = (2 ** 160); + + /// @notice Get the number of tokens owned by an address. + /// @param owner The address to look for. + /// @return The number of tokens owned by the address. + function balanceOf(address owner) external view virtual override returns (uint256) { + if (owner == address(0)) { + revert ERC721InvalidOwner(address(0)); + } + return _readNumNFTPerAddress(owner); + } + + /// @notice Get the owner of a token. + /// @param tokenId The id of the token. + /// @return owner The address of the token owner. + function ownerOf(uint256 tokenId) external view virtual override returns (address owner) { + owner = _ownerOf(tokenId); + if (owner == address(0)) { + revert ERC721NonexistentToken(tokenId); + } + return owner; + } + + /// @notice Get the approved operator for a specific token. + /// @param tokenId The id of the token. + /// @return The address of the operator. + function getApproved(uint256 tokenId) external view virtual override returns (address) { + (address owner, bool operatorEnabled) = _ownerAndOperatorEnabledOf(tokenId); + if (owner == address(0)) { + revert ERC721NonexistentToken(tokenId); + } + if (operatorEnabled) { + return _readOperator(tokenId); + } + return address(0); + } + + /// @notice Return the internal owner data of a Land + /// @param tokenId The id of the Land + /// @return the owner data (address + burn flag + operatorEnabled) + /// @dev for debugging purposes + function getOwnerData(uint256 tokenId) external view virtual returns (uint256) { + return _readOwnerData(tokenId); + } + + /// @notice Check if the sender approved the operator. + /// @param owner The address of the owner. + /// @param operator The address of the operator. + /// @return isOperator The status of the approval. + function isApprovedForAll(address owner, address operator) external view virtual override returns (bool) { + return _isApprovedForAllOrSuperOperator(owner, operator); + } + + /// @param from The address who initiated the transfer (may differ from msg.sender). + /// @param to The address receiving the token. + /// @param tokenId The token being transferred. + function _transferFrom(address from, address to, uint256 tokenId) internal { + address msgSender = _msgSender(); + _doTransfer(msgSender, from, to, tokenId); + if (to.code.length > 0 && _checkIERC721MandatoryTokenReceiver(to)) { + _checkOnERC721Received(msgSender, from, to, tokenId, ""); + } + } + + /// @notice Transfer a token between 2 addresses letting the receiver know of the transfer. + /// @param from The sender of the token. + /// @param to The recipient of the token. + /// @param tokenId The id of the token. + /// @param data Additional data. + function _safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) internal { + address msgSender = _msgSender(); + _doTransfer(msgSender, from, to, tokenId); + if (to.code.length > 0) { + _checkOnERC721Received(msgSender, from, to, tokenId, data); + } + } + + /// @param msgSender The sender of the transaction + /// @param from The address who initiated the transfer (may differ from msg.sender). + /// @param to The address receiving the token. + /// @param tokenId The token being transferred. + function _doTransfer(address msgSender, address from, address to, uint256 tokenId) internal { + if (to == address(0)) { + revert InvalidAddress(); + } + bool operatorEnabled = _checkFromIsOwner(from, tokenId); + bool authorized = msgSender == from || _isApprovedForAllOrSuperOperator(from, msgSender); + if (!authorized && !(operatorEnabled && _readOperator(tokenId) == msgSender)) { + revert ERC721InsufficientApproval(msgSender, tokenId); + } + _transferNumNFTPerAddress(from, to, 1); + _updateOwnerData(tokenId, to, false); + emit Transfer(from, to, tokenId); + } + + /// @param from The sender of the token + /// @param to The recipient of the token + /// @param ids The ids of the tokens + /// @param data additional data + /// @param safe checks the target contract + function _batchTransferFrom( + address from, + address to, + uint256[] calldata ids, + bytes memory data, + bool safe + ) internal { + if (from == address(0) || to == address(0)) { + revert InvalidAddress(); + } + + address msgSender = _msgSender(); + bool authorized = msgSender == from || _isApprovedForAllOrSuperOperator(from, msgSender); + uint256 numTokens = ids.length; + for (uint256 i = 0; i < numTokens; i++) { + uint256 tokenId = ids[i]; + (address owner, bool operatorEnabled) = _ownerAndOperatorEnabledOf(tokenId); + if (from != owner) { + revert ERC721InvalidOwner(from); + } + if (!authorized && !(operatorEnabled && _readOperator(tokenId) == msgSender)) { + revert ERC721InsufficientApproval(msgSender, tokenId); + } + _updateOwnerData(tokenId, to, false); + emit Transfer(from, to, tokenId); + } + _transferNumNFTPerAddress(from, to, numTokens); + + if (to.code.length > 0) { + if (_checkIERC721MandatoryTokenReceiver(to)) { + _checkOnERC721BatchReceived(msgSender, from, to, ids, data); + } else if (safe) { + for (uint256 i = 0; i < numTokens; i++) { + _checkOnERC721Received(msgSender, from, to, ids[i], data); + } + } + } + } + + /// @param from The address who initiated the transfer (may differ from msg.sender). + /// @param operator The address receiving the approval + /// @param approved The determination of the approval + function _setApprovalForAll(address from, address operator, bool approved) internal { + if (from == address(0)) { + revert ERC721InvalidSender(from); + } + address msgSender = _msgSender(); + if (msgSender != from && !_isSuperOperator(msgSender)) { + revert ERC721InvalidApprover(msgSender); + } + if (_isSuperOperator(operator)) { + revert ERC721InvalidOperator(operator); + } + _writeOperatorForAll(from, operator, approved); + emit ApprovalForAll(from, operator, approved); + } + + /// @param from The address who initiated the transfer (may differ from msg.sender). + /// @param operator The address receiving the approval + /// @param tokenId The id of the token + function _approveFor(address from, address operator, uint256 tokenId) internal { + _checkFromIsOwner(from, tokenId); + + address msgSender = _msgSender(); + bool authorized = msgSender == from || _isApprovedForAllOrSuperOperator(from, msgSender); + if (!authorized) { + revert ERC721InvalidApprover(msgSender); + } + if (operator == address(0)) { + _updateOwnerData(tokenId, from, false); + } else { + _updateOwnerData(tokenId, from, true); + _writeOperator(tokenId, operator); + } + emit Approval(from, operator, tokenId); + } + + /// @param from The address who initiated the transfer (may differ from msg.sender). + /// @param tokenId token id to burn + function _burn(address from, uint256 tokenId) internal { + bool operatorEnabled = _checkFromIsOwner(from, tokenId); + address msgSender = _msgSender(); + bool authorized = msgSender == from || _isApprovedForAllOrSuperOperator(from, msgSender); + if (!authorized && !(operatorEnabled && _readOperator(tokenId) == msgSender)) { + revert ERC721InsufficientApproval(msgSender, tokenId); + } + _writeOwnerData(tokenId, (_readOwnerData(tokenId) & (NOT_ADDRESS & NOT_OPERATOR_FLAG)) | BURNED_FLAG); + _subNumNFTPerAddress(from, 1); + emit Transfer(from, address(0), tokenId); + } + + /// @notice checks that the token is taken from the owner after the call (from == owner) + /// @param from sender address + /// @param tokenId The id of the token + /// @return operatorEnabled Whether or not operators are enabled for this token. + function _checkFromIsOwner(address from, uint256 tokenId) internal view returns (bool) { + if (from == address(0)) { + revert ERC721InvalidSender(from); + } + (address owner, bool operatorEnabled) = _ownerAndOperatorEnabledOf(tokenId); + // As from == owner, this is the same check as from == address(0) but we want a specific error for this one. + if (owner == address(0)) { + revert ERC721NonexistentToken(tokenId); + } + if (from != owner) { + revert ERC721InvalidOwner(from); + } + return operatorEnabled; + } + + /// @param tokenId The id of the token + /// @param newOwner The new owner of the token + /// @param hasOperator if true the operator flag is set + function _updateOwnerData(uint256 tokenId, address newOwner, bool hasOperator) internal { + uint256 oldData = (_readOwnerData(tokenId) & (NOT_ADDRESS & NOT_OPERATOR_FLAG)) | uint256(uint160(newOwner)); + if (hasOperator) { + oldData = oldData | OPERATOR_FLAG; + } + _writeOwnerData(tokenId, oldData); + } + + /// @param tokenId token id + /// @return owner address of the owner + function _ownerOf(uint256 tokenId) internal view returns (address owner) { + (owner, ) = _ownerAndOperatorEnabledOf(tokenId); + } + + /// @notice Get the owner and operatorEnabled flag of a token. + /// @param tokenId The token to query. + /// @return owner The owner of the token. + /// @return operatorEnabled Whether or not operators are enabled for this token. + /// @dev must extract the owner, burn and operator flag from _readOwnerData(tokenId) if burned must return owner = address(0) + function _ownerAndOperatorEnabledOf( + uint256 tokenId + ) internal view virtual returns (address owner, bool operatorEnabled); + + /// @notice Check if receiving contract accepts erc721 transfers. + /// @param operator The address of the operator. + /// @param from The from address, may be different from msg.sender. + /// @param to The address we want to transfer to. + /// @param tokenId The id of the token we would like to transfer. + /// @param data Any additional data to send with the transfer. + function _checkOnERC721Received( + address operator, + address from, + address to, + uint256 tokenId, + bytes memory data + ) internal { + /* solhint-disable no-empty-blocks */ + try IERC721Receiver(to).onERC721Received(operator, from, tokenId, data) returns (bytes4 retval) { + if (retval == IERC721Receiver.onERC721Received.selector) { + return; + } + } catch (bytes memory) {} + /* solhint-enable no-empty-blocks */ + revert ERC721InvalidReceiver(to); + } + + /// @notice Check if receiving contract accepts erc721 batch transfers. + /// @param operator The address of the operator. + /// @param from The from address, may be different from msg.sender. + /// @param to The address we want to transfer to. + /// @param ids The ids of the tokens we would like to transfer. + /// @param _data Any additional data to send with the transfer. + function _checkOnERC721BatchReceived( + address operator, + address from, + address to, + uint256[] memory ids, + bytes memory _data + ) internal { + /* solhint-disable no-empty-blocks */ + try IERC721MandatoryTokenReceiver(to).onERC721BatchReceived(operator, from, ids, _data) returns ( + bytes4 retval + ) { + if (retval == IERC721MandatoryTokenReceiver.onERC721BatchReceived.selector) { + return; + } + } catch (bytes memory) {} + /* solhint-enable no-empty-blocks */ + revert ERC721InvalidReceiver(to); + } + + /// @notice Check if there was enough gas. + /// @param to The address of the contract to check. + /// @return Whether or not this check succeeded. + function _checkIERC721MandatoryTokenReceiver(address to) internal view returns (bool) { + return ERC165Checker.supportsERC165InterfaceUnchecked(to, type(IERC721MandatoryTokenReceiver).interfaceId); + } + + /// @notice Check if the sender approved the operator. + /// @param owner The address of the owner. + /// @param operator The address of the operator. + /// @return isOperator The status of the approval. + function _isApprovedForAllOrSuperOperator(address owner, address operator) internal view returns (bool) { + return _isOperatorForAll(owner, operator) || _isSuperOperator(operator); + } + + /// @notice Add tokens to the owner balance + /// @param who the owner of the token + /// @param val how much to add to the owner's balance + /// @dev we can use unchecked because there is a limited number of lands 408x408 + function _addNumNFTPerAddress(address who, uint256 val) internal { + unchecked { + _writeNumNFTPerAddress(who, _readNumNFTPerAddress(who) + val); + } + } + + /// @notice Subtract tokens to the owner balance + /// @param who the owner of the token + /// @param val how much to subtract from the owner's balance + /// @dev we can use unchecked because there is a limited number of lands 408x408 + function _subNumNFTPerAddress(address who, uint256 val) internal { + unchecked { + _writeNumNFTPerAddress(who, _readNumNFTPerAddress(who) - val); + } + } + + /// @notice Move balance between two users + /// @param from address to subtract from + /// @param to address to add from + /// @param quantity how many tokens to move + function _transferNumNFTPerAddress(address from, address to, uint256 quantity) internal virtual { + if (from != to) { + _subNumNFTPerAddress(from, quantity); + _addNumNFTPerAddress(to, quantity); + } + } + + /// @notice get the number of nft for an address + /// @param owner address to check + /// @return the number of nfts + function _readNumNFTPerAddress(address owner) internal view virtual returns (uint256); + + /// @notice set the number of nft for an address + /// @param owner address to set + /// @param quantity the number of nfts to set for the owner + function _writeNumNFTPerAddress(address owner, uint256 quantity) internal virtual; + + /// @notice Get the owner data of a token for a user + /// @param tokenId The id of the token. + /// @return the owner data + /// @dev The owner data has three fields: owner address, operator flag and burn flag. See: _owners declaration. + function _readOwnerData(uint256 tokenId) internal view virtual returns (uint256); + + /// @notice Get the owner address of a token (included in the ownerData, see: _getOwnerData) + /// @param tokenId The id of the token. + /// @return the owner address + function _getOwnerAddress(uint256 tokenId) internal view virtual returns (address) { + return address(uint160(_readOwnerData(tokenId))); + } + + /// @notice Set the owner data of a token + /// @param tokenId the token Id + /// @param data the owner data + /// @dev The owner data has three fields: owner address, operator flag and burn flag. See: _owners declaration. + function _writeOwnerData(uint256 tokenId, uint256 data) internal virtual; + + /// @notice check if an operator was enabled by a given owner + /// @param owner that enabled the operator + /// @param operator address to check if it was enabled + /// @return true if the operator has access + function _isOperatorForAll(address owner, address operator) internal view virtual returns (bool); + + /// @notice Provides an operator access to all the tokens of an owner + /// @param owner that enabled the operator + /// @param operator address to check if it was enabled + /// @param enabled if true give access to the operator, else disable it + function _writeOperatorForAll(address owner, address operator, bool enabled) internal virtual; + + /// @notice get the operator for a specific token, the operator can transfer on the owner behalf + /// @param tokenId The id of the token. + /// @return the operator address + function _readOperator(uint256 tokenId) internal view virtual returns (address); + + /// @notice set the operator for a specific token, the operator can transfer on the owner behalf + /// @param tokenId the id of the token. + /// @param operator the operator address + function _writeOperator(uint256 tokenId, address operator) internal virtual; +} diff --git a/packages/marketplace/contracts/mocks/land/IErrors.sol b/packages/marketplace/contracts/mocks/land/IErrors.sol new file mode 100644 index 0000000000..51c8f160f2 --- /dev/null +++ b/packages/marketplace/contracts/mocks/land/IErrors.sol @@ -0,0 +1,35 @@ +//SPDX-License-Identifier: MIT + +pragma solidity 0.8.23; + +/// @title Errors +/// @author The Sandbox +/// @custom:security-contact contact-blockchain@sandbox.game +/// @notice Common errors +interface IErrors { + /// @notice an address passed as argument is invalid + error InvalidAddress(); + + /// @notice an argument passed is invalid + error InvalidArgument(); + + /// @notice an array argument has an invalid length + error InvalidLength(); + + /// @notice only admin can call this function + error OnlyAdmin(); + + error ERC721InvalidOwner(address owner); + + error ERC721NonexistentToken(uint256 tokenId); + + error ERC721InvalidSender(address sender); + + error ERC721InvalidReceiver(address receiver); + + error ERC721InsufficientApproval(address operator, uint256 tokenId); + + error ERC721InvalidApprover(address approver); + + error ERC721InvalidOperator(address operator); +} diff --git a/packages/marketplace/contracts/mocks/land/LandBase.sol b/packages/marketplace/contracts/mocks/land/LandBase.sol new file mode 100644 index 0000000000..f4e72e5c6a --- /dev/null +++ b/packages/marketplace/contracts/mocks/land/LandBase.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import {IOperatorFilterRegistry} from "@sandbox-smart-contracts/land/contracts/interfaces/IOperatorFilterRegistry.sol"; +import {IERC173} from "@sandbox-smart-contracts/land/contracts/interfaces/IERC173.sol"; +import {ILandToken} from "@sandbox-smart-contracts/land/contracts/interfaces/ILandToken.sol"; +import {IQuad} from "@sandbox-smart-contracts/land/contracts/interfaces/IQuad.sol"; +import {ILandMetadataRegistry} from "@sandbox-smart-contracts/land/contracts/interfaces/ILandMetadataRegistry.sol"; +import {IERC721BatchOps} from "@sandbox-smart-contracts/land/contracts/interfaces/IERC721BatchOps.sol"; +import {WithAdmin} from "./WithAdmin.sol"; +import {OperatorFiltererUpgradeable} from "@sandbox-smart-contracts/land/contracts/common/OperatorFiltererUpgradeable.sol"; +import {WithMetadataRegistry} from "./WithMetadataRegistry.sol"; +import {WithRoyalties} from "@sandbox-smart-contracts/land/contracts/common/WithRoyalties.sol"; +import {WithOwner} from "@sandbox-smart-contracts/land/contracts/common/WithOwner.sol"; +import {LandBaseToken} from "./LandBaseToken.sol"; + +/// @title Land Contract +/// @author The Sandbox +/// @custom:security-contact contact-blockchain@sandbox.game +/// @notice LAND contract +/// @dev LAND contract implements ERC721, quads, metadata, royalties and marketplace filtering functionalities. +/// @dev The contract also implements EIP173 because it is needed by some marketplaces. The owner() doesn't have +/// @dev any privileged roles within the contract. It can be set by the admin to any value. +abstract contract LandBase is + LandBaseToken, + Initializable, + OperatorFiltererUpgradeable, + WithAdmin, + WithMetadataRegistry, + WithRoyalties, + WithOwner +{ + /// @dev this protects the implementation contract from being initialized. + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the contract admin + /// @param admin Admin of the contract + function initialize(address admin) external initializer { + _setAdmin(admin); + } + + /// @notice This function is used to register Land contract on the Operator Filterer Registry of Opensea. + /// @param subscriptionOrRegistrantToCopy registration address of the list to subscribe. + /// @param subscribe bool to signify subscription 'true' or to copy the list 'false'. + function register(address subscriptionOrRegistrantToCopy, bool subscribe) external onlyAdmin { + if (subscriptionOrRegistrantToCopy == address(0)) { + revert InvalidAddress(); + } + _register(subscriptionOrRegistrantToCopy, subscribe); + } + + /// @notice Change the admin of the contract + /// @dev Change the administrator to be `newAdmin`. + /// @param newAdmin The address of the new administrator. + function changeAdmin(address newAdmin) external onlyAdmin { + _changeAdmin(newAdmin); + } + + /// @notice Enable or disable the ability of `superOperator` to transfer tokens of all (superOperator rights). + /// @param superOperator address that will be given/removed superOperator right. + /// @param enabled set whether the superOperator is enabled or disabled. + function setSuperOperator(address superOperator, bool enabled) external onlyAdmin { + _setSuperOperator(superOperator, enabled); + } + + /// @notice Enable or disable the ability of `minter` to mint tokens + /// @param minter address that will be given/removed minter right. + /// @param enabled set whether the minter is enabled or disabled. + function setMinter(address minter, bool enabled) external onlyAdmin { + _setMinter(minter, enabled); + } + + /// @notice sets filter registry address deployed in test + /// @param registry the address of the registry + function setOperatorRegistry(IOperatorFilterRegistry registry) external virtual onlyAdmin { + _setOperatorRegistry(registry); + } + + /// @notice set royalty manager + /// @param royaltyManager address of the manager contract for common royalty recipient + function setRoyaltyManager(address royaltyManager) external onlyAdmin { + _setRoyaltyManager(royaltyManager); + } + + /// @notice sets address of the Metadata Registry + /// @param metadataRegistry The address of the Metadata Registry + function setMetadataRegistry(address metadataRegistry) external onlyAdmin { + _setMetadataRegistry(metadataRegistry); + } + + /// @notice Set the address of the new owner of the contract + /// @param newOwner address of new owner + /// @dev This owner doesn't have any privileged role within this contract + /// @dev It is set by the admin to comply with EIP173 which is needed by some marketplaces + /// @dev Even when set to address(0) ownership is never permanently renounced the admin can always set any value + function transferOwnership(address newOwner) external onlyAdmin { + _transferOwnership(newOwner); + } + + /// @notice Approve an operator to spend tokens on the sender behalf + /// @param sender The address giving the approval + /// @param operator The address receiving the approval + /// @param tokenId The id of the token + function approveFor( + address sender, + address operator, + uint256 tokenId + ) external onlyAllowedOperatorApproval(operator) { + _approveFor(sender, operator, tokenId); + } + + /// @notice Set the approval for an operator to manage all the tokens of the msgSender + /// @param operator The address receiving the approval + /// @param approved The determination of the approval + function setApprovalForAll( + address operator, + bool approved + ) external override onlyAllowedOperatorApproval(operator) { + _setApprovalForAll(_msgSender(), operator, approved); + } + + /// @notice Set the approval for an operator to manage all the tokens of the sender (may differ from msgSender) + /// @param sender The address giving the approval + /// @param operator The address receiving the approval + /// @param approved The determination of the approval + function setApprovalForAllFor( + address sender, + address operator, + bool approved + ) external onlyAllowedOperatorApproval(operator) { + _setApprovalForAll(sender, operator, approved); + } + + /// @notice Approve an operator to spend tokens on the sender behalf + /// @param operator The address receiving the approval + /// @param tokenId The id of the token + function approve(address operator, uint256 tokenId) external override onlyAllowedOperatorApproval(operator) { + _approveFor(_msgSender(), operator, tokenId); + } + + /// @notice Transfer a token between 2 addresses + /// @param from The sender of the token + /// @param to The recipient of the token + /// @param tokenId The id of the token + function transferFrom(address from, address to, uint256 tokenId) external override onlyAllowedOperator(from) { + _transferFrom(from, to, tokenId); + } + + /// @notice Transfer many tokens between 2 addresses. + /// @param from The sender of the token. + /// @param to The recipient of the token. + /// @param ids The ids of the tokens. + /// @param data Additional data. + function batchTransferFrom( + address from, + address to, + uint256[] calldata ids, + bytes calldata data + ) external virtual override onlyAllowedOperator(from) { + _batchTransferFrom(from, to, ids, data, false); + } + + /// @notice Transfer a token between 2 addresses letting the receiver know of the transfer + /// @param from The sender of the token + /// @param to The recipient of the token + /// @param tokenId The id of the token + function safeTransferFrom(address from, address to, uint256 tokenId) external override onlyAllowedOperator(from) { + _safeTransferFrom(from, to, tokenId, ""); + } + + /// @notice Transfer a token between 2 addresses letting the receiver know of the transfer + /// @param from The sender of the token + /// @param to The recipient of the token + /// @param tokenId The id of the token + /// @param data Additional data + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory data + ) external override onlyAllowedOperator(from) { + _safeTransferFrom(from, to, tokenId, data); + } + + /// @notice Transfer many tokens between 2 addresses, while + /// ensuring the receiving contract has a receiver method. + /// @param from The sender of the token. + /// @param to The recipient of the token. + /// @param ids The ids of the tokens. + /// @param data Additional data. + function safeBatchTransferFrom( + address from, + address to, + uint256[] calldata ids, + bytes calldata data + ) external virtual onlyAllowedOperator(from) { + _batchTransferFrom(from, to, ids, data, true); + } + + /// @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 pure override returns (bool) { + return + interfaceId == type(IERC721).interfaceId || + interfaceId == type(IERC721BatchOps).interfaceId || + interfaceId == type(IERC721Metadata).interfaceId || + interfaceId == type(IERC165).interfaceId || + interfaceId == type(IERC173).interfaceId || + interfaceId == type(IERC2981).interfaceId || + interfaceId == type(ILandToken).interfaceId || + interfaceId == type(ILandToken).interfaceId ^ type(IQuad).interfaceId || + interfaceId == type(IQuad).interfaceId || + interfaceId == type(ILandMetadataRegistry).interfaceId; + } +} diff --git a/packages/marketplace/contracts/mocks/land/LandBaseToken.sol b/packages/marketplace/contracts/mocks/land/LandBaseToken.sol new file mode 100644 index 0000000000..0d73e37e8e --- /dev/null +++ b/packages/marketplace/contracts/mocks/land/LandBaseToken.sol @@ -0,0 +1,803 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {IErrors} from "./IErrors.sol"; +import {ILandToken} from "@sandbox-smart-contracts/land/contracts/interfaces/ILandToken.sol"; +import {ERC721BaseToken} from "./ERC721BaseToken.sol"; + +/// @title LandBaseToken +/// @author The Sandbox +/// @custom:security-contact contact-blockchain@sandbox.game +/// @notice Implement LAND and quad functionalities on top of an ERC721 token +/// @dev This contract implements a quad tree structure to handle groups of ERC721 tokens at once +abstract contract LandBaseToken is IErrors, ILandToken, ERC721BaseToken { + using Address for address; + + /// @notice the coordinates are invalid + /// @param size The size of the quad + /// @param x The bottom left x coordinate of the quad + /// @param y The bottom left y coordinate of the quad + error InvalidCoordinates(uint256 size, uint256 x, uint256 y); + + /// @notice is not the owner of the quad + /// @param x The bottom left x coordinate of the quad + /// @param y The bottom left y coordinate of the quad + error NotOwner(uint256 x, uint256 y); + + /// @notice the token is already minted + /// @param tokenId the id of land + error AlreadyMinted(uint256 tokenId); + + uint256 internal constant GRID_SIZE = 408; + + /* solhint-disable const-name-snakecase */ + uint256 internal constant LAYER = 0xFF00000000000000000000000000000000000000000000000000000000000000; + uint256 internal constant LAYER_1x1 = 0x0000000000000000000000000000000000000000000000000000000000000000; + uint256 internal constant LAYER_3x3 = 0x0100000000000000000000000000000000000000000000000000000000000000; + uint256 internal constant LAYER_6x6 = 0x0200000000000000000000000000000000000000000000000000000000000000; + uint256 internal constant LAYER_12x12 = 0x0300000000000000000000000000000000000000000000000000000000000000; + uint256 internal constant LAYER_24x24 = 0x0400000000000000000000000000000000000000000000000000000000000000; + /* solhint-enable const-name-snakecase */ + + /// @notice emitted when a minter right is changed. + /// @param minter address that will be given/removed minter right. + /// @param enabled set whether the minter is enabled or disabled. + event Minter(address indexed minter, bool enabled); + + /// @dev helper struct to store arguments in memory instead of the stack. + struct Land { + uint256 x; + uint256 y; + uint256 size; + } + + /// @notice transfer multiple quad (aligned to a quad tree with size 3, 6, 12 or 24 only) + /// @param from current owner of the quad + /// @param to destination + /// @param sizes list of sizes for each quad + /// @param xs list of bottom left x coordinates for each quad + /// @param ys list of bottom left y coordinates for each quad + /// @param data additional data + function batchTransferQuad( + address from, + address to, + uint256[] calldata sizes, + uint256[] calldata xs, + uint256[] calldata ys, + bytes calldata data + ) external override { + if (from == address(0) || to == address(0)) { + revert InvalidAddress(); + } + if (sizes.length != xs.length || xs.length != ys.length) { + revert InvalidLength(); + } + address msgSender = _msgSender(); + if (msgSender != from && !_isApprovedForAllOrSuperOperator(from, msgSender)) { + revert ERC721InvalidOwner(msgSender); + } + uint256 numTokensTransferred = 0; + for (uint256 i = 0; i < sizes.length; i++) { + uint256 size = sizes[i]; + _isValidQuad(size, xs[i], ys[i]); + _transferQuad(from, to, size, xs[i], ys[i]); + numTokensTransferred += size * size; + } + _transferNumNFTPerAddress(from, to, numTokensTransferred); + + if (to.code.length > 0 && _checkIERC721MandatoryTokenReceiver(to)) { + uint256[] memory ids = new uint256[](numTokensTransferred); + uint256 counter = 0; + for (uint256 j = 0; j < sizes.length; j++) { + uint256 size = sizes[j]; + for (uint256 i = 0; i < size * size; i++) { + ids[counter] = _idInPath(i, size, xs[j], ys[j]); + counter++; + } + } + _checkOnERC721BatchReceived(msgSender, from, to, ids, data); + } + } + + /// @notice transfer one quad (aligned to a quad tree with size 3, 6, 12 or 24 only) + /// @param from current owner of the quad + /// @param to destination + /// @param size The size of the quad + /// @param x The bottom left x coordinate of the quad + /// @param y The bottom left y coordinate of the quad + /// @param data additional data for transfer + function transferQuad( + address from, + address to, + uint256 size, + uint256 x, + uint256 y, + bytes calldata data + ) external override { + address msgSender = _msgSender(); + if (from == address(0) || to == address(0)) { + revert InvalidAddress(); + } + if (msgSender != from && !_isApprovedForAllOrSuperOperator(from, msgSender)) { + revert ERC721InvalidOwner(msgSender); + } + _isValidQuad(size, x, y); + _transferQuad(from, to, size, x, y); + _transferNumNFTPerAddress(from, to, size * size); + _checkBatchReceiverAcceptQuad(msgSender, from, to, size, x, y, data); + } + + /// @notice Mint a new quad (aligned to a quad tree with size 1, 3, 6, 12 or 24 only) + /// @param to The recipient of the new quad + /// @param size The size of the new quad + /// @param x The bottom left x coordinate of the new quad + /// @param y The bottom left y coordinate of the new quad + /// @param data extra data to pass to the transfer + function mintQuad(address to, uint256 size, uint256 x, uint256 y, bytes memory data) external virtual override { + address msgSender = _msgSender(); + if (!_isMinter(msgSender)) { + revert ERC721InvalidOwner(msgSender); + } + _isValidQuad(size, x, y); + _mintQuad(msgSender, to, size, x, y, data); + } + + /// @notice Checks if a parent quad has child quads already minted. + /// @notice Then mints the rest child quads and transfers the parent quad. + /// @notice Should only be called by the tunnel. + /// @param to The recipient of the new quad + /// @param size The size of the new quad + /// @param x The bottom left x coordinate of the new quad + /// @param y The bottom left y coordinate of the new quad + /// @param data extra data to pass to the transfer + function mintAndTransferQuad(address to, uint256 size, uint256 x, uint256 y, bytes calldata data) external virtual { + address msgSender = _msgSender(); + if (!_isMinter(msgSender)) { + revert ERC721InvalidOwner(msgSender); + } + if (to == address(0)) { + revert InvalidAddress(); + } + + _isValidQuad(size, x, y); + if (_ownerOfQuad(size, x, y) != address(0)) { + _transferQuad(msgSender, to, size, x, y); + _transferNumNFTPerAddress(msgSender, to, size * size); + _checkBatchReceiverAcceptQuad(msgSender, msgSender, to, size, x, y, data); + } else { + _mintAndTransferQuad(msgSender, to, size, x, y, data); + } + } + + /// @notice x coordinate of Land token + /// @param tokenId the id of land + /// @return the x coordinates + function getX(uint256 tokenId) external pure returns (uint256) { + return _getX(tokenId); + } + + /// @notice y coordinate of Land token + /// @param tokenId the id of land + /// @return the y coordinates + function getY(uint256 tokenId) external pure returns (uint256) { + return _getY(tokenId); + } + + /// @notice Return the name of the token contract + /// @return The name of the token contract + function name() external pure virtual returns (string memory) { + return "Sandbox's LANDs"; + } + + /// @notice check whether address `who` is given minter rights. + /// @param who The address to query. + /// @return whether the address has minter rights. + function isMinter(address who) external view virtual returns (bool) { + return _isMinter(who); + } + + /// @notice checks if Land has been minted or not + /// @param size The size of the quad + /// @param x The bottom left x coordinate of the quad + /// @param y The bottom left y coordinate of the quad + /// @return bool for if Land has been minted or not + function exists(uint256 size, uint256 x, uint256 y) external view virtual override returns (bool) { + _isValidQuad(size, x, y); + return _ownerOfQuad(size, x, y) != address(0); + } + + /// @notice Return the symbol of the token contract + /// @return The symbol of the token contract + function symbol() external pure virtual returns (string memory) { + return "LAND"; + } + + /// @notice total width of the map + /// @return width + function width() external pure virtual returns (uint256) { + return GRID_SIZE; + } + + /// @notice total height of the map + /// @return height + function height() public pure returns (uint256) { + return GRID_SIZE; + } + + /// @notice Return the URI of a specific token + /// @param tokenId The id of the token + /// @return The URI of the token + function tokenURI(uint256 tokenId) external view virtual returns (string memory) { + if (_ownerOf(tokenId) == address(0)) { + revert ERC721NonexistentToken(tokenId); + } + return string(abi.encodePacked("https://api.sandbox.game/lands/", Strings.toString(tokenId), "/metadata.json")); + } + + /// @notice Check size and coordinate of a quad + /// @param size The size of the quad + /// @param x The bottom left x coordinate of the quad + /// @param y The bottom left y coordinate of the quad + /// @dev after calling this function we can safely use unchecked math for x,y,size + function _isValidQuad(uint256 size, uint256 x, uint256 y) internal pure { + if (size != 1 && size != 3 && size != 6 && size != 12 && size != 24) { + revert InvalidCoordinates(size, x, y); + } + if (x % size != 0 || y % size != 0 || x > GRID_SIZE - size || y > GRID_SIZE - size) { + revert InvalidCoordinates(size, x, y); + } + } + + /// @param from current owner of the quad + /// @param to destination + /// @param size The size of the quad + /// @param x The bottom left x coordinate of the quad + /// @param y The bottom left y coordinate of the quad + function _transferQuad(address from, address to, uint256 size, uint256 x, uint256 y) internal { + if (size == 1) { + uint256 id1x1 = _getQuadId(LAYER_1x1, x, y); + address owner = _ownerOf(id1x1); + if (owner == address(0)) { + revert NotOwner(x, y); + } + if (owner != from) { + revert ERC721InvalidOwner(from); + } + _writeOwnerData(id1x1, uint160(to)); + } else { + _regroupQuad(from, to, Land({x: x, y: y, size: size}), true, size / 2); + } + for (uint256 i = 0; i < size * size; i++) { + emit Transfer(from, to, _idInPath(i, size, x, y)); + } + } + + /// @notice Mint a new quad + /// @param msgSender The original sender of the transaction + /// @param to The recipient of the new quad + /// @param size The size of the new quad + /// @param x The bottom left x coordinate of the new quad + /// @param y The bottom left y coordinate of the new quad + /// @param data extra data to pass to the transfer + function _mintQuad(address msgSender, address to, uint256 size, uint256 x, uint256 y, bytes memory data) internal { + if (to == address(0)) { + revert InvalidAddress(); + } + + (uint256 layer, , ) = _getQuadLayer(size); + uint256 quadId = _getQuadId(layer, x, y); + + _checkQuadIsNotMinted(size, x, y, 24); + for (uint256 i = 0; i < size * size; i++) { + uint256 _id = _idInPath(i, size, x, y); + if (_readOwnerData(_id) != 0) { + revert AlreadyMinted(_id); + } + emit Transfer(address(0), to, _id); + } + + _writeOwnerData(quadId, uint160(to)); + _addNumNFTPerAddress(to, size * size); + _checkBatchReceiverAcceptQuad(msgSender, address(0), to, size, x, y, data); + } + + /// @notice checks if the child quads in the parent quad (size, x, y) are owned by msgSender. + /// @param msgSender The original sender of the transaction + /// @param to The address to which the ownership of the quad will be transferred + /// @param size The size of the quad being minted and transferred + /// @param x The x-coordinate of the top-left corner of the quad being minted. + /// @param y The y-coordinate of the top-left corner of the quad being minted. + /// @dev It recursively checks whether child quad of every size (excluding Lands of 1x1 size) are minted or not. + /// @dev Quad which are minted are pushed into quadMinted to also check if every Land of size 1x1 in + /// @dev the parent quad is minted or not. While checking if every child Quad and Land is minted it + /// @dev also checks and clears the owner for quads which are minted. Finally it checks if the new owner + /// @dev is a contract, can handle ERC-721 tokens, and transfers the parent quad to new owner. + function _mintAndTransferQuad( + address msgSender, + address to, + uint256 size, + uint256 x, + uint256 y, + bytes memory data + ) internal { + (uint256 layer, , ) = _getQuadLayer(size); + uint256 quadId = _getQuadId(layer, x, y); + + // Length of array is equal to number of 3x3 child quad a 24x24 quad can have. Would be used to push the minted Quads. + Land[] memory quadMinted = new Land[](64); + // index of last minted quad pushed on quadMinted Array + uint256 index = 0; + uint256 landMinted = 0; + + // if size of the Quad in land struct to be transferred is greater than 3 we check recursively if the child quads are minted or not. + if (size > 3) { + (index, landMinted) = _checkQuadIsNotMintedAndClearOwner( + msgSender, + Land({x: x, y: y, size: size}), + quadMinted, + landMinted, + index, + size / 2 + ); + } + + // Looping around the Quad in land struct to generate ids of 1x1 land token and checking if they are owned by msg.sender + for (uint256 i = 0; i < size * size; i++) { + uint256 _id = _idInPath(i, size, x, y); + // checking land with token id "_id" is in the quadMinted array. + bool isAlreadyMinted = _isQuadMinted(quadMinted, Land({x: _getX(_id), y: _getY(_id), size: 1}), index); + if (isAlreadyMinted) { + // if land is in the quadMinted array, emit transfer event + emit Transfer(msgSender, to, _id); + } else { + if (_getOwnerAddress(_id) == msgSender) { + if (_readOperator(_id) != address(0)) _writeOperator(_id, address(0)); + landMinted += 1; + emit Transfer(msgSender, to, _id); + } else { + // else check if owned by the msgSender or not. If it is not owned by msgSender it should not have an owner. + if (_readOwnerData(_id) != 0) { + revert AlreadyMinted(_id); + } + + emit Transfer(address(0), to, _id); + } + } + } + + // checking if the new owner "to" is a contract. If yes, checking if it could handle ERC721 tokens. + _checkBatchReceiverAcceptQuadAndClearOwner(msgSender, quadMinted, index, landMinted, to, size, x, y, data); + + _writeOwnerData(quadId, uint160(to)); + _addNumNFTPerAddress(to, size * size); + _subNumNFTPerAddress(msgSender, landMinted); + } + + /// @notice recursively checks if the child quads are minted. + /// @param size The size of the quad + /// @param x The x-coordinate of the top-left corner of the quad being minted. + /// @param y The y-coordinate of the top-left corner of the quad being minted. + /// @param quadCompareSize the size of the child quads to be checked. + function _checkQuadIsNotMinted(uint256 size, uint256 x, uint256 y, uint256 quadCompareSize) internal { + (uint256 layer, , ) = _getQuadLayer(quadCompareSize); + + if (size <= quadCompareSize) { + // when the size of the quad is smaller than the quadCompareSize(size to be compared with), + // then it is checked if the bigger quad which encapsulates the quad to be minted + // of with size equals the quadCompareSize has been minted or not + uint256 id = _getQuadId( + layer, + (x / quadCompareSize) * quadCompareSize, + (y / quadCompareSize) * quadCompareSize + ); + if (_readOwnerData(id) != 0) { + revert AlreadyMinted(id); + } + } else { + // when the size is greater than the quadCompare size the owner of all the greater quads with size + // quadCompare size in the quad to be minted are checked if they are minted or not + uint256 toX = x + size; + uint256 toY = y + size; + for (uint256 xi = x; xi < toX; xi += quadCompareSize) { + for (uint256 yi = y; yi < toY; yi += quadCompareSize) { + uint256 id = _getQuadId(layer, xi, yi); + if (_readOwnerData(id) != 0) { + revert AlreadyMinted(id); + } + } + } + } + + quadCompareSize = quadCompareSize / 2; + if (quadCompareSize >= 3) _checkQuadIsNotMinted(size, x, y, quadCompareSize); + } + + /// @notice recursively checks if the child quads are minted in land and push them to the quadMinted array. + /// @param msgSender The original sender of the transaction + /// @param land the struct which has the size x and y co-ordinate of Quad to be checked + /// @param quadMinted array in which the minted child quad would be pushed + /// @param landMinted total 1x1 land already minted + /// @param index index of last element of quadMinted array + /// @param quadCompareSize the size of the child quads to be checked. + /// @return the index of last quad pushed in quadMinted array and the total land already minted + /// @dev if a child quad is minted in land such quads child quads will be skipped such that there is no overlapping + /// @dev in quads which are minted. it clears the minted child quads owners. + function _checkQuadIsNotMintedAndClearOwner( + address msgSender, + Land memory land, + Land[] memory quadMinted, + uint256 landMinted, + uint256 index, + uint256 quadCompareSize + ) internal returns (uint256, uint256) { + (uint256 layer, , ) = _getQuadLayer(quadCompareSize); + uint256 toX = land.x + land.size; + uint256 toY = land.y + land.size; + + // Looping around the Quad in land struct to check if the child quad are minted or not + for (uint256 xi = land.x; xi < toX; xi += quadCompareSize) { + for (uint256 yi = land.y; yi < toY; yi += quadCompareSize) { + //checking if the child Quad is minted or not. i.e Checks if the quad is in the quadMinted array. + bool isQuadChecked = _isQuadMinted(quadMinted, Land({x: xi, y: yi, size: quadCompareSize}), index); + // if child quad is not already in the quadMinted array. + if (!isQuadChecked) { + uint256 id = _getQuadId(layer, xi, yi); + address owner = _getOwnerAddress(id); + // owner of the child quad is checked to be owned by msgSender else should not be owned by anyone. + if (owner == msgSender) { + // if child quad is minted it would be pushed in quadMinted array. + quadMinted[index] = Land({x: xi, y: yi, size: quadCompareSize}); + // index of quadMinted is increased + index++; + // total land minted is increase by the number if land of 1x1 in child quad + landMinted += quadCompareSize * quadCompareSize; + //owner is cleared + _writeOwnerData(id, 0); + } else { + if (owner != address(0)) { + revert AlreadyMinted(id); + } + } + } + } + } + + // size of the child quad is set to be the next smaller child quad size (12 => 6 => 3) + quadCompareSize = quadCompareSize / 2; + // if child quad size is greater than 3 _checkAndClearOwner is checked for new child quads in the quad in land struct. + if (quadCompareSize >= 3) + (index, landMinted) = _checkQuadIsNotMintedAndClearOwner( + msgSender, + land, + quadMinted, + landMinted, + index, + quadCompareSize + ); + return (index, landMinted); + } + + /// @dev checks the owner of land with 'tokenId' to be 'from' and clears it + /// @param from the address to be checked against the owner of the land + /// @param x The x-coordinate of the top-left corner of the quad being minted. + /// @param y The y-coordinate of the top-left corner of the quad being minted. + /// @return bool for if land is owned by 'from' or not. + function _checkAndClearLandOwner(address from, uint256 x, uint256 y) internal returns (bool) { + uint256 tokenId = _getQuadId(LAYER_1x1, x, y); + uint256 currentOwner = _readOwnerData(tokenId); + if (currentOwner != 0) { + if ((currentOwner & BURNED_FLAG) == BURNED_FLAG) { + revert NotOwner(x, y); + } + if (address(uint160(currentOwner)) != from) { + revert ERC721InvalidOwner(from); + } + _writeOwnerData(tokenId, 0); + return true; + } + return false; + } + + function _checkBatchReceiverAcceptQuad( + address operator, + address from, + address to, + uint256 size, + uint256 x, + uint256 y, + bytes memory data + ) internal { + if (to.code.length > 0 && _checkIERC721MandatoryTokenReceiver(to)) { + uint256[] memory ids = new uint256[](size * size); + for (uint256 i = 0; i < size * size; i++) { + ids[i] = _idInPath(i, size, x, y); + } + _checkOnERC721BatchReceived(operator, from, to, ids, data); + } + } + + /// @param msgSender The original sender of the transaction + /// @param quadMinted - an array of Land structs in which the minted child quad or Quad to be transferred are. + /// @param landMinted - the total amount of land that has been minted + /// @param index - the index of the last element in the quadMinted array + /// @param to the address of the new owner of Quad to be transferred + /// @param size The size of the quad + /// @param x The x-coordinate of the top-left corner of the quad being minted. + /// @param y The y-coordinate of the top-left corner of the quad being minted. + /// @dev checks if the receiver of the quad(size, x, y) is a contact. If yes can it handle ERC721 tokens. It also clears owner of 1x1 land's owned by msgSender. + function _checkBatchReceiverAcceptQuadAndClearOwner( + address msgSender, + Land[] memory quadMinted, + uint256 index, + uint256 landMinted, + address to, + uint256 size, + uint256 x, + uint256 y, + bytes memory data + ) internal { + // checks if to is a contract and supports ERC721_MANDATORY_RECEIVER interfaces. if it doesn't it just clears the owner of 1x1 lands in quad(size, x, y) + if (to.code.length > 0 && _checkIERC721MandatoryTokenReceiver(to)) { + // array to push minted 1x1 land + uint256[] memory idsToTransfer = new uint256[](landMinted); + // index of last land pushed in idsToTransfer array + uint256 transferIndex = 0; + // array to push ids to be minted + uint256[] memory idsToMint = new uint256[]((size * size) - landMinted); + // index of last land pushed in idsToMint array + uint256 mintIndex = 0; + + // iterating over every 1x1 land in the quad to be pushed in the above arrays + for (uint256 i = 0; i < size * size; i++) { + uint256 id = _idInPath(i, size, x, y); + + if (_isQuadMinted(quadMinted, Land({x: _getX(id), y: _getY(id), size: 1}), index)) { + // if land is in the quads already minted it just pushed in to the idsToTransfer array + idsToTransfer[transferIndex] = id; + transferIndex++; + } else if (_getOwnerAddress(id) == msgSender) { + // if it is owned by the msgSender owner data is removed and it is pushed in to idsToTransfer array + _writeOwnerData(id, 0); + idsToTransfer[transferIndex] = id; + transferIndex++; + } else { + // else it is not owned by any one and and pushed in the idsToMint array + idsToMint[mintIndex] = id; + mintIndex++; + } + } + + // checking if "to" contact can handle ERC721 tokens + _checkOnERC721BatchReceived(msgSender, address(0), to, idsToMint, data); + _checkOnERC721BatchReceived(msgSender, msgSender, to, idsToTransfer, data); + } else { + for (uint256 i = 0; i < size * size; i++) { + uint256 id = _idInPath(i, size, x, y); + if (_getOwnerAddress(id) == msgSender) _writeOwnerData(id, 0); + } + } + } + + /// @notice x coordinate of Land token + /// @param tokenId The token id + /// @return the x coordinates + function _getX(uint256 tokenId) internal pure returns (uint256) { + return (tokenId & ~LAYER) % GRID_SIZE; + } + + /// @notice y coordinate of Land token + /// @param tokenId The token id + /// @return the y coordinates + function _getY(uint256 tokenId) internal pure returns (uint256) { + return (tokenId & ~LAYER) / GRID_SIZE; + } + + /// @notice check if a quad is in the array of minted lands + /// @param quad the quad that will be searched through mintedLand + /// @param quadMinted array of quads that are minted in the current transaction + /// @param index the amount of entries in mintedQuad + /// @return true if a quad is minted + function _isQuadMinted(Land[] memory quadMinted, Land memory quad, uint256 index) internal pure returns (bool) { + for (uint256 i = 0; i < index; i++) { + Land memory land = quadMinted[i]; + if ( + land.size > quad.size && + quad.x >= land.x && + quad.x < land.x + land.size && + quad.y >= land.y && + quad.y < land.y + land.size + ) { + return true; + } + } + return false; + } + + /// @notice get size related information (there is one-to-one relationship between layer and size) + /// @param size The size of the quad + /// @return layer the layers that corresponds to the size + /// @return parentSize the size of the parent (bigger quad that contains the current one) + /// @return childLayer the layer of the child (smaller quad contained by this one) + function _getQuadLayer(uint256 size) internal pure returns (uint256 layer, uint256 parentSize, uint256 childLayer) { + if (size == 1) { + layer = LAYER_1x1; + parentSize = 3; + } else if (size == 3) { + layer = LAYER_3x3; + parentSize = 6; + } else if (size == 6) { + layer = LAYER_6x6; + parentSize = 12; + childLayer = LAYER_3x3; + } else if (size == 12) { + layer = LAYER_12x12; + parentSize = 24; + childLayer = LAYER_6x6; + } else { + layer = LAYER_24x24; + childLayer = LAYER_12x12; + } + } + + /// @notice get the quad id given the layer and coordinates. + /// @param layer the layer of the quad see: _getQuadLayer + /// @param x The bottom left x coordinate of the quad + /// @param y The bottom left y coordinate of the quad + /// @return the tokenId of the quad + /// @dev this method is gas optimized, must be called with verified x,y and size, after a call to _isValidQuad + function _getQuadId(uint256 layer, uint256 x, uint256 y) internal pure returns (uint256) { + unchecked { + return layer + x + y * GRID_SIZE; + } + } + + /// @notice return the quadId given and index, size and coordinates + /// @param i the index to be added to x,y to get row and column + /// @param size The bottom left x coordinate of the quad + /// @param x The bottom left x coordinate of the quad + /// @param y The bottom left y coordinate of the quad + /// @return the tokenId of the quad + /// @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 pure returns (uint256) { + unchecked { + // This is an inlined/optimized version of: _getQuadId(LAYER_1x1, x + (i % size), y + (i / size)) + return (x + (i % size)) + (y + (i / size)) * GRID_SIZE; + } + } + + /// @notice checks if the Land's child quads are owned by the from address and clears all the previous owners + /// @param from address of the previous owner + /// @param to address of the new owner + /// @param land the quad to be regrouped and transferred + /// @param set for setting the new owner + /// @param childQuadSize size of the child quad to be checked for owner in the regrouping + /// @dev if all the child quads are not owned by the "from" address then the owner of parent quad to the land + /// @dev is checked if owned by the "from" address. If from is the owner then land owner is set to "to" address + function _regroupQuad( + address from, + address to, + Land memory land, + bool set, + uint256 childQuadSize + ) internal returns (bool) { + (uint256 layer, , uint256 childLayer) = _getQuadLayer(land.size); + uint256 quadId = _getQuadId(layer, land.x, land.y); + bool ownerOfAll = true; + + // double for loop iterates and checks owner of all the smaller quads in land + for (uint256 xi = land.x; xi < land.x + land.size; xi += childQuadSize) { + for (uint256 yi = land.y; yi < land.y + land.size; yi += childQuadSize) { + uint256 ownerChild = 0; + bool ownAllIndividual = false; + if (childQuadSize < 3) { + // case when the smaller quad is 1x1, + ownAllIndividual = _checkAndClearLandOwner(from, xi, yi) && ownerOfAll; + } else { + // recursively calling the _regroupQuad function to check the owner of child quads. + ownAllIndividual = _regroupQuad( + from, + to, + Land({x: xi, y: yi, size: childQuadSize}), + false, + childQuadSize / 2 + ); + uint256 idChild = _getQuadId(childLayer, xi, yi); + ownerChild = _readOwnerData(idChild); + if (ownerChild != 0) { + // checking the owner of child quad + if (!ownAllIndividual && ownerChild != uint256(uint160(from))) { + revert NotOwner(xi, yi); + } + // clearing owner of child quad + _writeOwnerData(idChild, 0); + } + } + // ownerOfAll should be true if "from" is owner of all the child quads iterated over + ownerOfAll = (ownAllIndividual || ownerChild != 0) && ownerOfAll; + } + } + + // if set is true it check if the "from" is owner of all else checks for the owner of parent quad is + // owned by "from" and sets the owner for the id of land to "to" address. + if (set) { + if (!ownerOfAll && _ownerOfQuad(land.size, land.x, land.y) != from) { + revert ERC721InvalidOwner(from); + } + _writeOwnerData(quadId, uint160(to)); + return true; + } + + return ownerOfAll; + } + + /// @notice return the owner of a quad given his size and coordinates or zero if is not minted yet. + /// @param size The size of the quad + /// @param x coordinate inside the quad + /// @param y coordinate inside the quad + /// @return the address of the owner + function _ownerOfQuad(uint256 size, uint256 x, uint256 y) internal view returns (address) { + (uint256 layer, uint256 parentSize, ) = _getQuadLayer(size); + address owner = _getOwnerAddress(_getQuadId(layer, (x / size) * size, (y / size) * size)); + if (owner != address(0)) { + return owner; + } else if (size < 24) { + return _ownerOfQuad(parentSize, x, y); + } + return address(0); + } + + /// @notice Get the owner and operatorEnabled flag of a token. + /// @param tokenId The token to query. + /// @return owner The owner of the token. + /// @return operatorEnabled Whether or not operators are enabled for this token. + function _ownerAndOperatorEnabledOf( + uint256 tokenId + ) internal view override returns (address owner, bool operatorEnabled) { + if (tokenId & LAYER != 0) { + revert ERC721NonexistentToken(tokenId); + } + uint256 x = tokenId % GRID_SIZE; + uint256 y = tokenId / GRID_SIZE; + uint256 owner1x1 = _readOwnerData(tokenId); + + if ((owner1x1 & BURNED_FLAG) == BURNED_FLAG) { + owner = address(0); + operatorEnabled = (owner1x1 & OPERATOR_FLAG) == OPERATOR_FLAG; + return (owner, operatorEnabled); + } + + if (owner1x1 != 0) { + owner = address(uint160(owner1x1)); + operatorEnabled = (owner1x1 & OPERATOR_FLAG) == OPERATOR_FLAG; + } else { + owner = _ownerOfQuad(3, x, y); + operatorEnabled = false; + } + } + + /// @notice Enable or disable the ability of `minter` to mint tokens + /// @param minter address that will be given/removed minter right. + /// @param enabled set whether the minter is enabled or disabled. + function _setMinter(address minter, bool enabled) internal { + if (minter == address(0)) { + revert InvalidAddress(); + } + if (enabled == _isMinter(minter)) { + revert InvalidArgument(); + } + _writeMinter(minter, enabled); + emit Minter(minter, enabled); + } + + /// @notice checks if an address is enabled as minter + /// @param minter the address to check + /// @return true if the address is a minter + function _isMinter(address minter) internal view virtual returns (bool); + + /// @notice set an address as minter + /// @param minter the address to set + /// @param enabled true enable the address, false disable it. + function _writeMinter(address minter, bool enabled) internal virtual; +} diff --git a/packages/marketplace/contracts/mocks/land/LandMock.sol b/packages/marketplace/contracts/mocks/land/LandMock.sol new file mode 100644 index 0000000000..3ed8661160 --- /dev/null +++ b/packages/marketplace/contracts/mocks/land/LandMock.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {IOperatorFilterRegistry} from "@sandbox-smart-contracts/land/contracts/interfaces/IOperatorFilterRegistry.sol"; +import {WithAdmin} from "./WithAdmin.sol"; +import {WithSuperOperators} from "./WithSuperOperators.sol"; +import {OperatorFiltererUpgradeable} from "@sandbox-smart-contracts/land/contracts/common/OperatorFiltererUpgradeable.sol"; +import {ERC721BaseToken} from "./ERC721BaseToken.sol"; +import {LandBaseToken} from "./LandBaseToken.sol"; +import {LandBase} from "./LandBase.sol"; +import {LandStorageMixin} from "@sandbox-smart-contracts/land/contracts/mainnet/LandStorageMixin.sol"; + +/// @title Land Mock Contrac +contract LandMock is LandStorageMixin, LandBase { + /// @notice Implements the Context msg sender + /// @return the address of the message sender + function _msgSender() internal view virtual override returns (address) { + return msg.sender; + } + + /// @notice get the admin address + /// @return the admin address + function _readAdmin() internal view override(LandStorageMixin, WithAdmin) returns (address) { + return LandStorageMixin._readAdmin(); + } + + /// @notice set the admin address + /// @param admin the admin address + function _writeAdmin(address admin) internal override(LandStorageMixin, WithAdmin) { + LandStorageMixin._writeAdmin(admin); + } + + /// @notice check if an address is a super-operator + /// @param superOperator the operator address to check + /// @return true if an address is a super-operator + function _isSuperOperator( + address superOperator + ) internal view override(LandStorageMixin, WithSuperOperators) returns (bool) { + return LandStorageMixin._isSuperOperator(superOperator); + } + + /// @notice enable an address to be super-operator + /// @param superOperator the address to set + /// @param enabled true enable the address, false disable it. + function _writeSuperOperator( + address superOperator, + bool enabled + ) internal override(LandStorageMixin, WithSuperOperators) { + LandStorageMixin._writeSuperOperator(superOperator, enabled); + } + + /// @notice get the number of nft for an address + /// @param owner address to check + /// @return the number of nfts + function _readNumNFTPerAddress( + address owner + ) internal view override(LandStorageMixin, ERC721BaseToken) returns (uint256) { + return LandStorageMixin._readNumNFTPerAddress(owner); + } + + /// @notice set the number of nft for an address + /// @param owner address to set + /// @param quantity the number of nfts to set for the owner + function _writeNumNFTPerAddress( + address owner, + uint256 quantity + ) internal override(LandStorageMixin, ERC721BaseToken) { + LandStorageMixin._writeNumNFTPerAddress(owner, quantity); + } + + /// @notice get the owner data, this includes: owner address, burn flag and operator flag (see: _owners declaration) + /// @param tokenId the token Id + /// @return the owner data + function _readOwnerData( + uint256 tokenId + ) internal view override(LandStorageMixin, ERC721BaseToken) returns (uint256) { + return LandStorageMixin._readOwnerData(tokenId); + } + + /// @notice sets Approvals with operator filterer check in case to test the transfer. + /// @param operator address of the operator to be approved + /// @param approved bool value denoting approved (true) or not Approved(false) + function setApprovalForAllWithOutFilter(address operator, bool approved) external { + super._setApprovalForAll(msg.sender, operator, approved); + } + + /// @notice set the owner data, this includes: owner address, burn flag and operator flag (see: _owners declaration) + /// @param tokenId the token Id + /// @param data the owner data + function _writeOwnerData(uint256 tokenId, uint256 data) internal override(LandStorageMixin, ERC721BaseToken) { + LandStorageMixin._writeOwnerData(tokenId, data); + } + + /// @notice check if an operator was enabled by a given owner + /// @param owner that enabled the operator + /// @param operator address to check if it was enabled + /// @return true if the operator has access + function _isOperatorForAll( + address owner, + address operator + ) internal view override(LandStorageMixin, ERC721BaseToken) returns (bool) { + return LandStorageMixin._isOperatorForAll(owner, operator); + } + + /// @notice Let an operator to access to all the tokens of a owner + /// @param owner that enabled the operator + /// @param operator address to check if it was enabled + /// @param enabled if true give access to the operator, else disable it + function _writeOperatorForAll( + address owner, + address operator, + bool enabled + ) internal override(LandStorageMixin, ERC721BaseToken) { + LandStorageMixin._writeOperatorForAll(owner, operator, enabled); + } + + /// @notice get the operator for a specific token, the operator can transfer on the owner behalf + /// @param tokenId The id of the token. + /// @return the operator address + function _readOperator( + uint256 tokenId + ) internal view override(LandStorageMixin, ERC721BaseToken) returns (address) { + return LandStorageMixin._readOperator(tokenId); + } + + /// @notice set the operator for a specific token, the operator can transfer on the owner behalf + /// @param tokenId the id of the token. + /// @param operator the operator address + function _writeOperator(uint256 tokenId, address operator) internal override(LandStorageMixin, ERC721BaseToken) { + LandStorageMixin._writeOperator(tokenId, operator); + } + + /// @notice checks if an address is enabled as minter + /// @param minter the address to check + /// @return true if the address is a minter + function _isMinter(address minter) internal view override(LandStorageMixin, LandBaseToken) returns (bool) { + return LandStorageMixin._isMinter(minter); + } + + /// @notice set an address as minter + /// @param minter the address to set + /// @param enabled true enable the address, false disable it. + function _writeMinter(address minter, bool enabled) internal override(LandStorageMixin, LandBaseToken) { + LandStorageMixin._writeMinter(minter, enabled); + } + + /// @notice get the OpenSea operator filter + /// @return the address of the OpenSea operator filter registry + function _readOperatorFilterRegistry() + internal + view + override(LandStorageMixin, OperatorFiltererUpgradeable) + returns (IOperatorFilterRegistry) + { + return LandStorageMixin._readOperatorFilterRegistry(); + } + + /// @notice set the OpenSea operator filter + /// @param registry the address of the OpenSea operator filter registry + function _writeOperatorFilterRegistry( + IOperatorFilterRegistry registry + ) internal override(LandStorageMixin, OperatorFiltererUpgradeable) { + LandStorageMixin._writeOperatorFilterRegistry(registry); + } +} diff --git a/packages/marketplace/contracts/mocks/land/WithAdmin.sol b/packages/marketplace/contracts/mocks/land/WithAdmin.sol new file mode 100644 index 0000000000..f8a30ea8b2 --- /dev/null +++ b/packages/marketplace/contracts/mocks/land/WithAdmin.sol @@ -0,0 +1,67 @@ +//SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {IErrors} from "./IErrors.sol"; +import {Context} from "@openzeppelin/contracts/utils/Context.sol"; + +/// @title WithAdmin +/// @author The Sandbox +/// @custom:security-contact contact-blockchain@sandbox.game +/// @notice Add an admin to the contract +abstract contract WithAdmin is IErrors, Context { + /// @notice Emits when the contract administrator is changed. + /// @param oldAdmin The address of the previous administrator. + /// @param newAdmin The address of the new administrator. + event AdminChanged(address indexed oldAdmin, address indexed newAdmin); + + /// @notice checks if the sender is admin + modifier onlyAdmin() { + if (_msgSender() != _readAdmin()) { + revert OnlyAdmin(); + } + _; + } + + /// @notice Get the current admin + /// @dev Get the current administrator of this contract. + /// @return The current administrator of this contract. + function getAdmin() external view returns (address) { + return _readAdmin(); + } + + /// @notice Change the admin of the contract + /// @dev Change the administrator to be `newAdmin`. + /// @param newAdmin The address of the new administrator. + function _changeAdmin(address newAdmin) internal { + address oldAdmin = _readAdmin(); + if (oldAdmin == address(0)) { + revert InvalidAddress(); + } + if (oldAdmin == newAdmin) { + revert InvalidArgument(); + } + _setAdmin(newAdmin); + } + + /// @notice Change the admin of the contract + /// @dev Change the administrator to be `newAdmin`. + /// @param newAdmin The address of the new administrator. + function _setAdmin(address newAdmin) internal { + if (newAdmin == address(0)) { + revert InvalidAddress(); + } + address oldAdmin = _readAdmin(); + emit AdminChanged(oldAdmin, newAdmin); + _writeAdmin(newAdmin); + } + + /// @notice get the admin address + /// @return the admin address + ///@dev Implement + function _readAdmin() internal view virtual returns (address); + + /// @notice set the admin address + /// @param admin the admin address + ///@dev Implement + function _writeAdmin(address admin) internal virtual; +} diff --git a/packages/marketplace/contracts/mocks/land/WithMetadataRegistry.sol b/packages/marketplace/contracts/mocks/land/WithMetadataRegistry.sol new file mode 100644 index 0000000000..66c72f1b54 --- /dev/null +++ b/packages/marketplace/contracts/mocks/land/WithMetadataRegistry.sol @@ -0,0 +1,85 @@ +//SPDX-License-Identifier: MIT +// solhint-disable-next-line compiler-version +pragma solidity 0.8.23; + +import {IErrors} from "./IErrors.sol"; +import {ILandMetadataRegistry} from "@sandbox-smart-contracts/land/contracts/interfaces/ILandMetadataRegistry.sol"; + +/// @title WithMetadataRegistry +/// @author The Sandbox +/// @custom:security-contact contact-blockchain@sandbox.game +/// @notice Add support for the metadata registry +abstract contract WithMetadataRegistry is IErrors { + /// @notice value returned when the neighborhood is not set yet. + string private constant UNKNOWN_NEIGHBORHOOD = "unknown"; + + /// @notice emitted when the metadata registry is set + /// @param metadataRegistry the address of the metadata registry + event MetadataRegistrySet(address indexed metadataRegistry); + + struct MetadataRegistryStorage { + ILandMetadataRegistry _metadataRegistry; + } + + /// @custom:storage-location erc7201:thesandbox.storage.land.common.WithMetadataRegistry + bytes32 internal constant METADATA_REGISTRY_STORAGE_LOCATION = + 0x3899f13de39885dfce849839be8330453b5866928dd0e5933e36794349628400; + + function _getMetadataRegistryStorage() private pure returns (MetadataRegistryStorage storage $) { + // solhint-disable-next-line no-inline-assembly + assembly { + $.slot := METADATA_REGISTRY_STORAGE_LOCATION + } + } + + /// @notice Get the address of the Metadata Registry + /// @return The address of the Metadata Registry + function getMetadataRegistry() external view returns (ILandMetadataRegistry) { + MetadataRegistryStorage storage $ = _getMetadataRegistryStorage(); + return $._metadataRegistry; + } + + /// @notice return the metadata for one land + /// @param tokenId the token id + /// @return premium true if the land is premium + /// @return neighborhoodId the number that identifies the neighborhood + /// @return neighborhoodName the neighborhood name + function getMetadata(uint256 tokenId) external view returns (bool, uint256, string memory) { + ILandMetadataRegistry registry = _getMetadataRegistryStorage()._metadataRegistry; + if (registry == ILandMetadataRegistry(address(0))) { + return (false, 0, UNKNOWN_NEIGHBORHOOD); + } + return registry.getMetadata(tokenId); + } + + /// @notice return true if a land is premium + /// @param tokenId the token id + function isPremium(uint256 tokenId) external view returns (bool) { + ILandMetadataRegistry registry = _getMetadataRegistryStorage()._metadataRegistry; + if (registry == ILandMetadataRegistry(address(0))) { + return false; + } + return registry.isPremium(tokenId); + } + + /// @notice return the id that identifies the neighborhood + /// @param tokenId the token id + function getNeighborhoodId(uint256 tokenId) external view returns (uint256) { + ILandMetadataRegistry registry = _getMetadataRegistryStorage()._metadataRegistry; + if (registry == ILandMetadataRegistry(address(0))) { + return 0; + } + return registry.getNeighborhoodId(tokenId); + } + + /// @notice set the address of the metadata registry + /// @param metadataRegistry the address of the metadata registry + function _setMetadataRegistry(address metadataRegistry) internal { + if (metadataRegistry == address(0)) { + revert InvalidAddress(); + } + MetadataRegistryStorage storage $ = _getMetadataRegistryStorage(); + $._metadataRegistry = ILandMetadataRegistry(metadataRegistry); + emit MetadataRegistrySet(metadataRegistry); + } +} diff --git a/packages/marketplace/contracts/mocks/land/WithSuperOperators.sol b/packages/marketplace/contracts/mocks/land/WithSuperOperators.sol new file mode 100644 index 0000000000..37d390c182 --- /dev/null +++ b/packages/marketplace/contracts/mocks/land/WithSuperOperators.sol @@ -0,0 +1,47 @@ +//SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {IErrors} from "./IErrors.sol"; + +/// @title WithSuperOperators +/// @author The Sandbox +/// @custom:security-contact contact-blockchain@sandbox.game +/// @notice Add super operators handled by an admin +abstract contract WithSuperOperators is IErrors { + /// @param superOperator address that will be given/removed superOperator right. + /// @param enabled set whether the superOperator is enabled or disabled. + event SuperOperator(address indexed superOperator, bool indexed enabled); + + /// @notice check if an address is a super-operator + /// @param superOperator the operator address to check + /// @return true if an address is a super-operator + function isSuperOperator(address superOperator) external view returns (bool) { + return _isSuperOperator(superOperator); + } + + /// @notice Enable or disable the ability of `superOperator` to transfer tokens of all (superOperator rights). + /// @param superOperator address that will be given/removed superOperator right. + /// @param enabled set whether the superOperator is enabled or disabled. + function _setSuperOperator(address superOperator, bool enabled) internal { + if (superOperator == address(0)) { + revert InvalidAddress(); + } + if (enabled == _isSuperOperator(superOperator)) { + revert InvalidArgument(); + } + _writeSuperOperator(superOperator, enabled); + emit SuperOperator(superOperator, enabled); + } + + /// @notice check if an address is a super-operator + /// @param superOperator the operator address to check + /// @return true if an address is a super-operator + /// @dev Implement + function _isSuperOperator(address superOperator) internal view virtual returns (bool); + + /// @notice enable an address to be super-operator + /// @param superOperator the address to set + /// @param enabled true enable the address, false disable it. + /// @dev Implement + function _writeSuperOperator(address superOperator, bool enabled) internal virtual; +} diff --git a/packages/marketplace/test/Exchange.test.ts b/packages/marketplace/test/Exchange.test.ts index 2955f9b20a..fa043bb1c0 100644 --- a/packages/marketplace/test/Exchange.test.ts +++ b/packages/marketplace/test/Exchange.test.ts @@ -311,7 +311,7 @@ describe('Exchange.sol', function () { signatureRight: takerSig, }, ]) - ).to.be.revertedWithCustomError(ExchangeContractAsUser, 'EnforcedPause'); + ).to.be.revertedWith('Pausable: paused'); }); it('should not execute match order with an empty order array', async function () { diff --git a/packages/marketplace/test/OrderValidator.test.ts b/packages/marketplace/test/OrderValidator.test.ts index 0e74ef8dd7..0ff2113ba9 100644 --- a/packages/marketplace/test/OrderValidator.test.ts +++ b/packages/marketplace/test/OrderValidator.test.ts @@ -30,6 +30,7 @@ describe('OrderValidator.sol', function () { ERC20Contract: Contract, ERC721Contract: Contract, ERC1271Contract: Contract, + user: Signer, user1: Signer, user2: Signer; @@ -42,6 +43,7 @@ describe('OrderValidator.sol', function () { ERC20Contract, ERC721Contract, ERC1271Contract, + user, user1, user2, } = await loadFixture(deployFixturesWithoutWhitelist)); @@ -324,11 +326,10 @@ describe('OrderValidator.sol', function () { }); it('should not set permission for token if caller is not owner', async function () { - await expect( - OrderValidatorAsUser.enableRole(TSBRole) - ).to.be.revertedWithCustomError( - OrderValidatorAsUser, - 'AccessControlUnauthorizedAccount' + await expect(OrderValidatorAsUser.enableRole(TSBRole)).to.revertedWith( + `AccessControl: account ${( + await user.getAddress() + ).toLowerCase()} is missing role 0x0000000000000000000000000000000000000000000000000000000000000000` ); }); @@ -361,9 +362,10 @@ describe('OrderValidator.sol', function () { it('should not be able to add token to tsb list if caller is not owner', async function () { await expect( OrderValidatorAsUser.grantRole(TSBRole, await ERC20Contract.getAddress()) - ).to.be.revertedWithCustomError( - OrderValidatorAsUser, - 'AccessControlUnauthorizedAccount' + ).to.be.revertedWith( + `AccessControl: account ${( + await user.getAddress() + ).toLowerCase()} is missing role 0x0000000000000000000000000000000000000000000000000000000000000000` ); }); @@ -389,9 +391,10 @@ describe('OrderValidator.sol', function () { it('should not be able to remove token from tsb list if caller is not owner', async function () { await expect( OrderValidatorAsUser.revokeRole(TSBRole, await ERC20Contract.getAddress()) - ).to.be.revertedWithCustomError( - OrderValidatorAsUser, - 'AccessControlUnauthorizedAccount' + ).to.be.revertedWith( + `AccessControl: account ${( + await user.getAddress() + ).toLowerCase()} is missing role 0x0000000000000000000000000000000000000000000000000000000000000000` ); }); @@ -431,9 +434,10 @@ describe('OrderValidator.sol', function () { PartnerRole, await ERC20Contract.getAddress() ) - ).to.be.revertedWithCustomError( - OrderValidatorAsUser, - 'AccessControlUnauthorizedAccount' + ).to.be.revertedWith( + `AccessControl: account ${( + await user.getAddress() + ).toLowerCase()} is missing role 0x0000000000000000000000000000000000000000000000000000000000000000` ); }); @@ -462,9 +466,10 @@ describe('OrderValidator.sol', function () { PartnerRole, await ERC20Contract.getAddress() ) - ).to.be.revertedWithCustomError( - OrderValidatorAsUser, - 'AccessControlUnauthorizedAccount' + ).to.be.revertedWith( + `AccessControl: account ${( + await user.getAddress() + ).toLowerCase()} is missing role 0x0000000000000000000000000000000000000000000000000000000000000000` ); }); @@ -504,9 +509,10 @@ describe('OrderValidator.sol', function () { ERC20Role, await ERC20Contract.getAddress() ) - ).to.be.revertedWithCustomError( - OrderValidatorAsUser, - 'AccessControlUnauthorizedAccount' + ).to.be.revertedWith( + `AccessControl: account ${( + await user.getAddress() + ).toLowerCase()} is missing role 0x0000000000000000000000000000000000000000000000000000000000000000` ); }); @@ -535,9 +541,10 @@ describe('OrderValidator.sol', function () { ERC20Role, await ERC20Contract.getAddress() ) - ).to.be.revertedWithCustomError( - OrderValidatorAsUser, - 'AccessControlUnauthorizedAccount' + ).to.be.revertedWith( + `AccessControl: account ${( + await user.getAddress() + ).toLowerCase()} is missing role 0x0000000000000000000000000000000000000000000000000000000000000000` ); }); diff --git a/packages/marketplace/test/common/AccessControl.behavior.ts b/packages/marketplace/test/common/AccessControl.behavior.ts index 2280083307..a35ca842e5 100644 --- a/packages/marketplace/test/common/AccessControl.behavior.ts +++ b/packages/marketplace/test/common/AccessControl.behavior.ts @@ -16,6 +16,7 @@ export function checkAccessControl( OrderValidatorAsUser: Contract, TrustedForwarderAsUser: Contract, user: Signer, + DEFAULT_ADMIN_ROLE: string, contractMap: {[key: string]: Contract}; beforeEach(async function () { @@ -26,6 +27,7 @@ export function checkAccessControl( OrderValidatorAsUser, TrustedForwarder2: TrustedForwarderAsUser, user, + DEFAULT_ADMIN_ROLE, } = await loadFixture(deployFixturesWithoutWhitelist)); contractMap = { ExchangeContractAsAdmin: ExchangeContractAsAdmin, @@ -40,9 +42,10 @@ export function checkAccessControl( it(`should not set ${functionName[i]} if caller is not in the role`, async function () { await expect( ExchangeContractAsUser[functionName[i]](user.getAddress()) - ).to.be.revertedWithCustomError( - ExchangeContractAsUser, - 'AccessControlUnauthorizedAccount' + ).to.be.revertedWith( + `AccessControl: account ${( + await user.getAddress() + ).toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}` ); }); diff --git a/packages/marketplace/test/exchange/Config.behavior.ts b/packages/marketplace/test/exchange/Config.behavior.ts index 28f19ed469..1841d2df36 100644 --- a/packages/marketplace/test/exchange/Config.behavior.ts +++ b/packages/marketplace/test/exchange/Config.behavior.ts @@ -57,11 +57,10 @@ export function exchangeConfig() { describe('pauser role', function () { it('should not pause if caller is not in the role', async function () { - await expect( - ExchangeContractAsUser.pause() - ).to.be.revertedWithCustomError( - ExchangeContractAsUser, - 'AccessControlUnauthorizedAccount' + await expect(ExchangeContractAsUser.pause()).to.be.revertedWith( + `AccessControl: account ${( + await user.getAddress() + ).toLowerCase()} is missing role ${PAUSER_ROLE}` ); }); @@ -82,9 +81,10 @@ export function exchangeConfig() { newProtocolFeePrimary, newProtocolFeeSecondary ) - ).to.be.revertedWithCustomError( - ExchangeContractAsUser, - 'AccessControlUnauthorizedAccount' + ).to.be.revertedWith( + `AccessControl: account ${( + await user.getAddress() + ).toLowerCase()} is missing role ${EXCHANGE_ADMIN_ROLE}` ); }); @@ -106,11 +106,10 @@ export function exchangeConfig() { const {ExchangeContractAsUser} = await loadFixture( deployFixturesWithoutWhitelist ); - await expect( - ExchangeContractAsUser.unpause() - ).to.be.revertedWithCustomError( - ExchangeContractAsUser, - 'AccessControlUnauthorizedAccount' + await expect(ExchangeContractAsUser.unpause()).to.be.revertedWith( + `AccessControl: account ${( + await user.getAddress() + ).toLowerCase()} is missing role ${EXCHANGE_ADMIN_ROLE}` ); }); @@ -128,9 +127,10 @@ export function exchangeConfig() { it('should not set setDefaultFeeReceiver if caller is not in the role', async function () { await expect( ExchangeContractAsUser.setDefaultFeeReceiver(user.getAddress()) - ).to.be.revertedWithCustomError( - ExchangeContractAsUser, - 'AccessControlUnauthorizedAccount' + ).to.be.revertedWith( + `AccessControl: account ${( + await user.getAddress() + ).toLowerCase()} is missing role ${EXCHANGE_ADMIN_ROLE}` ); }); @@ -155,9 +155,10 @@ export function exchangeConfig() { const newMatchOrdersLimit = 200; await expect( ExchangeContractAsUser.setMatchOrdersLimit(newMatchOrdersLimit) - ).to.be.revertedWithCustomError( - ExchangeContractAsUser, - 'AccessControlUnauthorizedAccount' + ).to.be.revertedWith( + `AccessControl: account ${( + await user.getAddress() + ).toLowerCase()} is missing role ${EXCHANGE_ADMIN_ROLE}` ); }); diff --git a/packages/marketplace/test/exchange/MatchOrders.behavior.ts b/packages/marketplace/test/exchange/MatchOrders.behavior.ts index 3270e8552c..66cde8e0e2 100644 --- a/packages/marketplace/test/exchange/MatchOrders.behavior.ts +++ b/packages/marketplace/test/exchange/MatchOrders.behavior.ts @@ -1095,10 +1095,7 @@ export function shouldMatchOrders() { }, ] ) - ).to.be.revertedWithCustomError( - ExchangeContractAsUser, - 'EnforcedPause' - ); + ).to.be.revertedWith('Pausable: paused'); }); it('should not execute matchOrdersFrom if caller do not have ERC1776 operator role', async function () { @@ -1111,9 +1108,10 @@ export function shouldMatchOrders() { signatureRight: takerSig, }, ]) - ).to.be.revertedWithCustomError( - ExchangeContractAsUser, - 'AccessControlUnauthorizedAccount' + ).to.be.revertedWith( + `AccessControl: account ${( + await user.getAddress() + ).toLowerCase()} is missing role ${ERC1776_OPERATOR_ROLE}` ); });