From 38242818ebf357c052fed3da3e68a7cb1d8d1803 Mon Sep 17 00:00:00 2001 From: zajck Date: Wed, 22 Mar 2023 17:46:35 +0100 Subject: [PATCH 01/12] create new collection --- .../interfaces/IInitializableVoucherClone.sol | 2 ++ .../interfaces/events/IBosonAccountEvents.sol | 7 ++++ .../handlers/IBosonAccountHandler.sol | 16 +++++++++- contracts/protocol/bases/SellerBase.sol | 13 ++++++-- .../protocol/clients/voucher/BosonVoucher.sol | 5 ++- .../protocol/facets/SellerHandlerFacet.sol | 32 +++++++++++++++++++ contracts/protocol/libs/ProtocolLib.sol | 2 ++ 7 files changed, 72 insertions(+), 5 deletions(-) diff --git a/contracts/interfaces/IInitializableVoucherClone.sol b/contracts/interfaces/IInitializableVoucherClone.sol index fb43527b3..68b670315 100644 --- a/contracts/interfaces/IInitializableVoucherClone.sol +++ b/contracts/interfaces/IInitializableVoucherClone.sol @@ -15,11 +15,13 @@ interface IInitializableVoucherClone { * @notice Initializes a voucher with the given parameters. * * @param _sellerId - The ID of the seller. + * @param _collectionId - The ID of the collection. * @param _newOwner - The address of the new owner. * @param _voucherInitValues - The voucher initialization values. */ function initializeVoucher( uint256 _sellerId, + uint256 _collectionId, address _newOwner, BosonTypes.VoucherInitValues calldata _voucherInitValues ) external; diff --git a/contracts/interfaces/events/IBosonAccountEvents.sol b/contracts/interfaces/events/IBosonAccountEvents.sol index 29dc19c65..64a567736 100644 --- a/contracts/interfaces/events/IBosonAccountEvents.sol +++ b/contracts/interfaces/events/IBosonAccountEvents.sol @@ -68,4 +68,11 @@ interface IBosonAccountEvents { address indexed executedBy ); event AgentCreated(uint256 indexed agentId, BosonTypes.Agent agent, address indexed executedBy); + event CollectionCreated( + uint256 indexed sellerId, + uint256 collectionIndex, + address collectionAddress, + string indexed externalId, + address indexed executedBy + ); } diff --git a/contracts/interfaces/handlers/IBosonAccountHandler.sol b/contracts/interfaces/handlers/IBosonAccountHandler.sol index 70a013f41..c421fb9e9 100644 --- a/contracts/interfaces/handlers/IBosonAccountHandler.sol +++ b/contracts/interfaces/handlers/IBosonAccountHandler.sol @@ -9,7 +9,7 @@ import { IBosonAccountEvents } from "../events/IBosonAccountEvents.sol"; * * @notice Handles creation, update, retrieval of accounts within the protocol. * - * The ERC-165 identifier for this interface is: 0x1f891681 + * The ERC-165 identifier for this interface is: 0xee3cb2a6 */ interface IBosonAccountHandler is IBosonAccountEvents { /** @@ -301,6 +301,20 @@ interface IBosonAccountHandler is IBosonAccountEvents { */ function removeSellersFromAllowList(uint256 _disputeResolverId, uint256[] calldata _sellerAllowList) external; + /** + * @notice Creates a new seller collection. + * + * Emits a CollectionCreated event if successful. + * + * Reverts if: + * - The offers region of protocol is paused + * - Caller is not the seller assistant + * + * @param _externalId - external collection id + * @param _contractURI - contract URI + */ + function createNewCollection(string calldata _externalId, string calldata _contractURI) external; + /** * @notice Gets the details about a seller. * diff --git a/contracts/protocol/bases/SellerBase.sol b/contracts/protocol/bases/SellerBase.sol index 15f3cff2a..6733f1bcc 100644 --- a/contracts/protocol/bases/SellerBase.sol +++ b/contracts/protocol/bases/SellerBase.sol @@ -99,7 +99,7 @@ contract SellerBase is ProtocolBase, IBosonAccountEvents { storeSeller(_seller, _authToken, lookups); // Create clone and store its address cloneAddress - address voucherCloneAddress = cloneBosonVoucher(sellerId, _seller.assistant, _voucherInitValues); + address voucherCloneAddress = cloneBosonVoucher(sellerId, 0, _seller.assistant, _voucherInitValues); lookups.cloneAddress[sellerId] = voucherCloneAddress; // Notify watchers of state change @@ -152,14 +152,16 @@ contract SellerBase is ProtocolBase, IBosonAccountEvents { * @notice Creates a minimal clone of the Boson Voucher Contract. * * @param _sellerId - id of the seller + * @param _collectionId - id of the collection. * @param _assistant - address of the assistant * @param _voucherInitValues - the fully populated BosonTypes.VoucherInitValues struct * @return cloneAddress - the address of newly created clone */ function cloneBosonVoucher( uint256 _sellerId, + uint256 _collectionId, address _assistant, - VoucherInitValues calldata _voucherInitValues + VoucherInitValues memory _voucherInitValues ) internal returns (address cloneAddress) { // Pointer to stored addresses ProtocolLib.ProtocolAddresses storage pa = protocolAddresses(); @@ -178,7 +180,12 @@ contract SellerBase is ProtocolBase, IBosonAccountEvents { // Initialize the clone IInitializableVoucherClone(cloneAddress).initialize(pa.voucherBeacon); - IInitializableVoucherClone(cloneAddress).initializeVoucher(_sellerId, _assistant, _voucherInitValues); + IInitializableVoucherClone(cloneAddress).initializeVoucher( + _sellerId, + _collectionId, + _assistant, + _voucherInitValues + ); } /** diff --git a/contracts/protocol/clients/voucher/BosonVoucher.sol b/contracts/protocol/clients/voucher/BosonVoucher.sol index 91f73c94e..949ac0e9b 100644 --- a/contracts/protocol/clients/voucher/BosonVoucher.sol +++ b/contracts/protocol/clients/voucher/BosonVoucher.sol @@ -82,10 +82,13 @@ contract BosonVoucherBase is */ function initializeVoucher( uint256 _sellerId, + uint256 _collectionId, address _newOwner, VoucherInitValues calldata voucherInitValues ) public initializer { - string memory sellerId = Strings.toString(_sellerId); + string memory sellerId = string( + abi.encodePacked(Strings.toString(_sellerId), "_", Strings.toString(_collectionId)) + ); string memory voucherName = string(abi.encodePacked(VOUCHER_NAME, " ", sellerId)); string memory voucherSymbol = string(abi.encodePacked(VOUCHER_SYMBOL, "_", sellerId)); diff --git a/contracts/protocol/facets/SellerHandlerFacet.sol b/contracts/protocol/facets/SellerHandlerFacet.sol index 9a9400899..b019de099 100644 --- a/contracts/protocol/facets/SellerHandlerFacet.sol +++ b/contracts/protocol/facets/SellerHandlerFacet.sol @@ -331,6 +331,38 @@ contract SellerHandlerFacet is SellerBase { } } + /** + * @notice Creates a new seller collection. + * + * Emits a CollectionCreated event if successful. + * + * Reverts if: + * - The offers region of protocol is paused + * - Caller is not the seller assistant + * + * @param _externalId - external collection id + * @param _contractURI - contract URI + */ + function createNewCollection(string calldata _externalId, string calldata _contractURI) external { + address assistant = msgSender(); + + (bool exists, uint256 sellerId) = getSellerIdByAssistant(assistant); + require(exists, NO_SUCH_SELLER); + + VoucherInitValues memory _voucherInitValues; + _voucherInitValues.contractURI = _contractURI; + // NB: we don't set any royalties here, since they are managed inside the protocol after BPIP-5 + + address[] storage sellersAdditionalCloneAddresses = protocolLookups().additionalCloneAddresses[sellerId]; + uint256 collectionIndex = sellersAdditionalCloneAddresses.length + 1; // 0 is reserved for the original collection + + // Create clone and store its address cloneAddress + address voucherCloneAddress = cloneBosonVoucher(sellerId, collectionIndex, assistant, _voucherInitValues); + sellersAdditionalCloneAddresses.push(voucherCloneAddress); + + emit CollectionCreated(sellerId, collectionIndex, voucherCloneAddress, _externalId, assistant); + } + /** * @notice Gets the details about a seller. * diff --git a/contracts/protocol/libs/ProtocolLib.sol b/contracts/protocol/libs/ProtocolLib.sol index dd3f18d02..524f6121d 100644 --- a/contracts/protocol/libs/ProtocolLib.sol +++ b/contracts/protocol/libs/ProtocolLib.sol @@ -182,6 +182,8 @@ library ProtocolLib { mapping(uint256 => BosonTypes.AuthToken) pendingAuthTokenUpdatesBySeller; // dispute resolver id => DisputeResolver mapping(uint256 => BosonTypes.DisputeResolver) pendingAddressUpdatesByDisputeResolver; + // seller id => cloneAddresses + mapping(uint256 => address[]) additionalCloneAddresses; } // Incrementing id counters From 47df762a83dfc2b2a5883c50137badbedcf79090 Mon Sep 17 00:00:00 2001 From: zajck Date: Wed, 22 Mar 2023 18:22:10 +0100 Subject: [PATCH 02/12] Manage cloneAddress protocol wide --- contracts/domain/BosonConstants.sol | 1 + contracts/domain/BosonTypes.sol | 1 + .../interfaces/IInitializableVoucherClone.sol | 4 +- .../handlers/IBosonOfferHandler.sol | 2 +- .../handlers/IBosonOrchestrationHandler.sol | 2 +- contracts/protocol/bases/OfferBase.sol | 92 +++++++++++-------- contracts/protocol/bases/ProtocolBase.sol | 11 +++ contracts/protocol/bases/SellerBase.sol | 6 +- .../protocol/clients/voucher/BosonVoucher.sol | 4 +- .../protocol/facets/ExchangeHandlerFacet.sol | 10 +- .../protocol/facets/SellerHandlerFacet.sol | 10 +- 11 files changed, 90 insertions(+), 53 deletions(-) diff --git a/contracts/domain/BosonConstants.sol b/contracts/domain/BosonConstants.sol index d97ee911b..31c791a96 100644 --- a/contracts/domain/BosonConstants.sol +++ b/contracts/domain/BosonConstants.sol @@ -92,6 +92,7 @@ string constant INVALID_DISPUTE_RESOLVER = "Invalid dispute resolver"; string constant INVALID_QUANTITY_AVAILABLE = "Invalid quantity available"; string constant DR_UNSUPPORTED_FEE = "Dispute resolver does not accept this token"; string constant AGENT_FEE_AMOUNT_TOO_HIGH = "Sum of agent fee amount and protocol fee amount should be <= offer fee limit"; +string constant NO_SUCH_COLLECTION = "No such collection"; // Revert Reasons: Group related string constant NO_SUCH_GROUP = "No such group"; diff --git a/contracts/domain/BosonTypes.sol b/contracts/domain/BosonTypes.sol index b5a2a9498..b313fb872 100644 --- a/contracts/domain/BosonTypes.sol +++ b/contracts/domain/BosonTypes.sol @@ -145,6 +145,7 @@ contract BosonTypes { string metadataUri; string metadataHash; bool voided; + uint256 collectionIndex; } struct OfferDates { diff --git a/contracts/interfaces/IInitializableVoucherClone.sol b/contracts/interfaces/IInitializableVoucherClone.sol index 68b670315..9e4279aab 100644 --- a/contracts/interfaces/IInitializableVoucherClone.sol +++ b/contracts/interfaces/IInitializableVoucherClone.sol @@ -15,13 +15,13 @@ interface IInitializableVoucherClone { * @notice Initializes a voucher with the given parameters. * * @param _sellerId - The ID of the seller. - * @param _collectionId - The ID of the collection. + * @param _collectionIndex - The index of the collection. * @param _newOwner - The address of the new owner. * @param _voucherInitValues - The voucher initialization values. */ function initializeVoucher( uint256 _sellerId, - uint256 _collectionId, + uint256 _collectionIndex, address _newOwner, BosonTypes.VoucherInitValues calldata _voucherInitValues ) external; diff --git a/contracts/interfaces/handlers/IBosonOfferHandler.sol b/contracts/interfaces/handlers/IBosonOfferHandler.sol index 6b240f14e..889ca5690 100644 --- a/contracts/interfaces/handlers/IBosonOfferHandler.sol +++ b/contracts/interfaces/handlers/IBosonOfferHandler.sol @@ -9,7 +9,7 @@ import { IBosonOfferEvents } from "../events/IBosonOfferEvents.sol"; * * @notice Handles creation, voiding, and querying of offers within the protocol. * - * The ERC-165 identifier for this interface is: 0xa1598d02 + * The ERC-165 identifier for this interface is: 0xa1e3b91c */ interface IBosonOfferHandler is IBosonOfferEvents { /** diff --git a/contracts/interfaces/handlers/IBosonOrchestrationHandler.sol b/contracts/interfaces/handlers/IBosonOrchestrationHandler.sol index c5e6ab6ad..e20181cc8 100644 --- a/contracts/interfaces/handlers/IBosonOrchestrationHandler.sol +++ b/contracts/interfaces/handlers/IBosonOrchestrationHandler.sol @@ -13,7 +13,7 @@ import { IBosonBundleEvents } from "../events/IBosonBundleEvents.sol"; * * @notice Combines creation of multiple entities (accounts, offers, groups, twins, bundles) in a single transaction * - * The ERC-165 identifier for this interface is: 0xa38bc2e7 + * The ERC-165 identifier for this interface is: 0x31e1e6ae */ interface IBosonOrchestrationHandler is IBosonAccountEvents, diff --git a/contracts/protocol/bases/OfferBase.sol b/contracts/protocol/bases/OfferBase.sol index 3330a64e0..3e62491af 100644 --- a/contracts/protocol/bases/OfferBase.sol +++ b/contracts/protocol/bases/OfferBase.sol @@ -149,46 +149,59 @@ contract OfferBase is ProtocolBase, IBosonOfferEvents { // quantity must be greater than zero require(_offer.quantityAvailable > 0, INVALID_QUANTITY_AVAILABLE); - // Specified resolver must be registered and active, except for absolute zero offers with unspecified dispute resolver. - // If price and sellerDeposit are 0, seller is not obliged to choose dispute resolver, which is done by setting _disputeResolverId to 0. - // In this case, there is no need to check the validity of the dispute resolver. However, if one (or more) of {price, sellerDeposit, _disputeResolverId} - // is different from 0, it must be checked that dispute resolver exists, supports the exchange token and seller is allowed to choose them. DisputeResolutionTerms memory disputeResolutionTerms; - if (_offer.price != 0 || _offer.sellerDeposit != 0 || _disputeResolverId != 0) { - ( - bool exists, - DisputeResolver storage disputeResolver, - DisputeResolverFee[] storage disputeResolverFees - ) = fetchDisputeResolver(_disputeResolverId); - require(exists && disputeResolver.active, INVALID_DISPUTE_RESOLVER); - - // Operate in a block to avoid "stack too deep" error - { - // Cache protocol lookups for reference - ProtocolLib.ProtocolLookups storage lookups = protocolLookups(); - - // check that seller is on the DR allow list - if (lookups.allowedSellers[_disputeResolverId].length > 0) { - // if length == 0, dispute resolver allows any seller - // if length > 0, we check that it is on allow list - require(lookups.allowedSellerIndex[_disputeResolverId][_offer.sellerId] > 0, SELLER_NOT_APPROVED); + { + // Cache protocol lookups for reference + ProtocolLib.ProtocolLookups storage lookups = protocolLookups(); + + // Specified resolver must be registered and active, except for absolute zero offers with unspecified dispute resolver. + // If price and sellerDeposit are 0, seller is not obliged to choose dispute resolver, which is done by setting _disputeResolverId to 0. + // In this case, there is no need to check the validity of the dispute resolver. However, if one (or more) of {price, sellerDeposit, _disputeResolverId} + // is different from 0, it must be checked that dispute resolver exists, supports the exchange token and seller is allowed to choose them. + if (_offer.price != 0 || _offer.sellerDeposit != 0 || _disputeResolverId != 0) { + ( + bool exists, + DisputeResolver storage disputeResolver, + DisputeResolverFee[] storage disputeResolverFees + ) = fetchDisputeResolver(_disputeResolverId); + require(exists && disputeResolver.active, INVALID_DISPUTE_RESOLVER); + + // Operate in a block to avoid "stack too deep" error + { + // check that seller is on the DR allow list + if (lookups.allowedSellers[_disputeResolverId].length > 0) { + // if length == 0, dispute resolver allows any seller + // if length > 0, we check that it is on allow list + require( + lookups.allowedSellerIndex[_disputeResolverId][_offer.sellerId] > 0, + SELLER_NOT_APPROVED + ); + } + + // get the index of DisputeResolverFee and make sure DR supports the exchangeToken + uint256 feeIndex = lookups.disputeResolverFeeTokenIndex[_disputeResolverId][_offer.exchangeToken]; + require(feeIndex > 0, DR_UNSUPPORTED_FEE); + + uint256 feeAmount = disputeResolverFees[feeIndex - 1].feeAmount; + + // store DR terms + disputeResolutionTerms.disputeResolverId = _disputeResolverId; + disputeResolutionTerms.escalationResponsePeriod = disputeResolver.escalationResponsePeriod; + disputeResolutionTerms.feeAmount = feeAmount; + disputeResolutionTerms.buyerEscalationDeposit = + (feeAmount * protocolFees().buyerEscalationDepositPercentage) / + 10000; + + protocolEntities().disputeResolutionTerms[_offer.id] = disputeResolutionTerms; } + } - // get the index of DisputeResolverFee and make sure DR supports the exchangeToken - uint256 feeIndex = lookups.disputeResolverFeeTokenIndex[_disputeResolverId][_offer.exchangeToken]; - require(feeIndex > 0, DR_UNSUPPORTED_FEE); - - uint256 feeAmount = disputeResolverFees[feeIndex - 1].feeAmount; - - // store DR terms - disputeResolutionTerms.disputeResolverId = _disputeResolverId; - disputeResolutionTerms.escalationResponsePeriod = disputeResolver.escalationResponsePeriod; - disputeResolutionTerms.feeAmount = feeAmount; - disputeResolutionTerms.buyerEscalationDeposit = - (feeAmount * protocolFees().buyerEscalationDepositPercentage) / - 10000; - - protocolEntities().disputeResolutionTerms[_offer.id] = disputeResolutionTerms; + // Collection must exist. Collections with index 0 exist by default. + if (_offer.collectionIndex > 0) { + require( + lookups.additionalCloneAddresses[_offer.sellerId][_offer.collectionIndex] != address(0), + NO_SUCH_COLLECTION + ); } } @@ -243,6 +256,7 @@ contract OfferBase is ProtocolBase, IBosonOfferEvents { offer.exchangeToken = _offer.exchangeToken; offer.metadataUri = _offer.metadataUri; offer.metadataHash = _offer.metadataHash; + offer.collectionIndex = _offer.collectionIndex; // Get storage location for offer dates OfferDates storage offerDates = fetchOfferDates(_offer.id); @@ -315,7 +329,9 @@ contract OfferBase is ProtocolBase, IBosonOfferEvents { ProtocolLib.ProtocolCounters storage pc = protocolCounters(); uint256 _startId = pc.nextExchangeId; - IBosonVoucher bosonVoucher = IBosonVoucher(protocolLookups().cloneAddress[offer.sellerId]); + IBosonVoucher bosonVoucher = IBosonVoucher( + getCloneAddress(protocolLookups(), offer.sellerId, offer.collectionIndex) + ); address sender = msgSender(); diff --git a/contracts/protocol/bases/ProtocolBase.sol b/contracts/protocol/bases/ProtocolBase.sol index 06ae0f32b..2fd4afb64 100644 --- a/contracts/protocol/bases/ProtocolBase.sol +++ b/contracts/protocol/bases/ProtocolBase.sol @@ -730,4 +730,15 @@ abstract contract ProtocolBase is PausableBase, ReentrancyGuardBase { // Determine existence exists = (_exchangeId > 0 && condition.method != EvaluationMethod.None); } + + function getCloneAddress( + ProtocolLib.ProtocolLookups storage _lookups, + uint256 _sellerId, + uint256 _collectionIndex + ) internal view returns (address cloneAddress) { + return + _collectionIndex == 0 + ? _lookups.cloneAddress[_sellerId] + : _lookups.additionalCloneAddresses[_sellerId][_collectionIndex - 1]; + } } diff --git a/contracts/protocol/bases/SellerBase.sol b/contracts/protocol/bases/SellerBase.sol index 6733f1bcc..ccb62382d 100644 --- a/contracts/protocol/bases/SellerBase.sol +++ b/contracts/protocol/bases/SellerBase.sol @@ -152,14 +152,14 @@ contract SellerBase is ProtocolBase, IBosonAccountEvents { * @notice Creates a minimal clone of the Boson Voucher Contract. * * @param _sellerId - id of the seller - * @param _collectionId - id of the collection. + * @param _collectionIndex - index of the collection. * @param _assistant - address of the assistant * @param _voucherInitValues - the fully populated BosonTypes.VoucherInitValues struct * @return cloneAddress - the address of newly created clone */ function cloneBosonVoucher( uint256 _sellerId, - uint256 _collectionId, + uint256 _collectionIndex, address _assistant, VoucherInitValues memory _voucherInitValues ) internal returns (address cloneAddress) { @@ -182,7 +182,7 @@ contract SellerBase is ProtocolBase, IBosonAccountEvents { IInitializableVoucherClone(cloneAddress).initialize(pa.voucherBeacon); IInitializableVoucherClone(cloneAddress).initializeVoucher( _sellerId, - _collectionId, + _collectionIndex, _assistant, _voucherInitValues ); diff --git a/contracts/protocol/clients/voucher/BosonVoucher.sol b/contracts/protocol/clients/voucher/BosonVoucher.sol index 949ac0e9b..50b0ba815 100644 --- a/contracts/protocol/clients/voucher/BosonVoucher.sol +++ b/contracts/protocol/clients/voucher/BosonVoucher.sol @@ -82,12 +82,12 @@ contract BosonVoucherBase is */ function initializeVoucher( uint256 _sellerId, - uint256 _collectionId, + uint256 _collectionIndex, address _newOwner, VoucherInitValues calldata voucherInitValues ) public initializer { string memory sellerId = string( - abi.encodePacked(Strings.toString(_sellerId), "_", Strings.toString(_collectionId)) + abi.encodePacked(Strings.toString(_sellerId), "_", Strings.toString(_collectionIndex)) ); string memory voucherName = string(abi.encodePacked(VOUCHER_NAME, " ", sellerId)); string memory voucherSymbol = string(abi.encodePacked(VOUCHER_SYMBOL, "_", sellerId)); diff --git a/contracts/protocol/facets/ExchangeHandlerFacet.sol b/contracts/protocol/facets/ExchangeHandlerFacet.sol index a23f0c90d..2144c1911 100644 --- a/contracts/protocol/facets/ExchangeHandlerFacet.sol +++ b/contracts/protocol/facets/ExchangeHandlerFacet.sol @@ -111,7 +111,7 @@ contract ExchangeHandlerFacet is IBosonExchangeHandler, BuyerBase, DisputeBase { (, Offer storage offer) = fetchOffer(_offerId); // Make sure that the voucher was issued on the clone that is making a call - require(msg.sender == protocolLookups().cloneAddress[offer.sellerId], ACCESS_DENIED); + require(msg.sender == getCloneAddress(protocolLookups(), offer.sellerId, offer.collectionIndex), ACCESS_DENIED); // Exchange must not exist already (bool exists, ) = fetchExchange(_exchangeId); @@ -215,7 +215,9 @@ contract ExchangeHandlerFacet is IBosonExchangeHandler, BuyerBase, DisputeBase { // Issue voucher, unless it already exist (for preminted offers) lookups.voucherCount[buyerId]++; if (!_isPreminted) { - IBosonVoucher bosonVoucher = IBosonVoucher(lookups.cloneAddress[_offer.sellerId]); + IBosonVoucher bosonVoucher = IBosonVoucher( + getCloneAddress(lookups, _offer.sellerId, _offer.collectionIndex) + ); bosonVoucher.issueVoucher(_exchangeId, _buyer); } } @@ -511,7 +513,7 @@ contract ExchangeHandlerFacet is IBosonExchangeHandler, BuyerBase, DisputeBase { (, Offer storage offer) = fetchOffer(exchange.offerId); // Make sure that the voucher was issued on the clone that is making a call - require(msg.sender == lookups.cloneAddress[offer.sellerId], ACCESS_DENIED); + require(msg.sender == getCloneAddress(lookups, offer.sellerId, offer.collectionIndex), ACCESS_DENIED); // Decrease voucher counter for old buyer lookups.voucherCount[exchange.buyerId]--; @@ -678,7 +680,7 @@ contract ExchangeHandlerFacet is IBosonExchangeHandler, BuyerBase, DisputeBase { // Burn the voucher (, Offer storage offer) = fetchOffer(_exchange.offerId); - IBosonVoucher bosonVoucher = IBosonVoucher(lookups.cloneAddress[offer.sellerId]); + IBosonVoucher bosonVoucher = IBosonVoucher(getCloneAddress(lookups, offer.sellerId, offer.collectionIndex)); bosonVoucher.burnVoucher(_exchange.id); } diff --git a/contracts/protocol/facets/SellerHandlerFacet.sol b/contracts/protocol/facets/SellerHandlerFacet.sol index b019de099..1926ead1c 100644 --- a/contracts/protocol/facets/SellerHandlerFacet.sol +++ b/contracts/protocol/facets/SellerHandlerFacet.sol @@ -254,7 +254,13 @@ contract SellerHandlerFacet is SellerBase { seller.assistant = sender; // Transfer ownership of voucher contract to new assistant - IBosonVoucher(lookups.cloneAddress[_sellerId]).transferOwnership(sender); + IBosonVoucher(lookups.cloneAddress[_sellerId]).transferOwnership(sender); // default voucher contract + address[] storage sellersAdditionalCloneAddresses = lookups.additionalCloneAddresses[_sellerId]; + uint256 collectionCount; + for (i = 0; i < collectionCount; i++) { + // Additional collections (if they exist) + IBosonVoucher(sellersAdditionalCloneAddresses[i]).transferOwnership(sender); + } // Store new seller id by assistant mapping lookups.sellerIdByAssistant[sender] = _sellerId; @@ -356,7 +362,7 @@ contract SellerHandlerFacet is SellerBase { address[] storage sellersAdditionalCloneAddresses = protocolLookups().additionalCloneAddresses[sellerId]; uint256 collectionIndex = sellersAdditionalCloneAddresses.length + 1; // 0 is reserved for the original collection - // Create clone and store its address cloneAddress + // Create clone and store its address to additionalCloneAddresses address voucherCloneAddress = cloneBosonVoucher(sellerId, collectionIndex, assistant, _voucherInitValues); sellersAdditionalCloneAddresses.push(voucherCloneAddress); From 7728d090cc9191ca646f28bcb02bfd8b85e7cc90 Mon Sep 17 00:00:00 2001 From: zajck Date: Mon, 27 Mar 2023 10:58:39 +0200 Subject: [PATCH 03/12] Collection struct, domain test + old tests --- contracts/domain/BosonTypes.sol | 5 + .../handlers/IBosonAccountHandler.sol | 14 +- contracts/protocol/bases/OfferBase.sol | 3 +- contracts/protocol/bases/ProtocolBase.sol | 2 +- .../protocol/facets/SellerHandlerFacet.sol | 31 +- contracts/protocol/libs/ProtocolLib.sol | 4 +- scripts/domain/Collection.js | 200 +++++++++++ scripts/domain/Offer.js | 27 +- test/domain/CollectionTest.js | 324 ++++++++++++++++++ test/domain/OfferTest.js | 43 ++- test/protocol/OrchestrationHandlerTest.js | 264 +++++++++++--- test/protocol/SellerHandlerTest.js | 42 ++- test/util/mock.js | 4 +- 13 files changed, 889 insertions(+), 74 deletions(-) create mode 100644 scripts/domain/Collection.js create mode 100644 test/domain/CollectionTest.js diff --git a/contracts/domain/BosonTypes.sol b/contracts/domain/BosonTypes.sol index b313fb872..a2544d309 100644 --- a/contracts/domain/BosonTypes.sol +++ b/contracts/domain/BosonTypes.sol @@ -285,4 +285,9 @@ contract BosonTypes { string contractURI; uint256 royaltyPercentage; } + + struct Collection { + address collectionAddress; + string externalId; + } } diff --git a/contracts/interfaces/handlers/IBosonAccountHandler.sol b/contracts/interfaces/handlers/IBosonAccountHandler.sol index c421fb9e9..d1d7e0dab 100644 --- a/contracts/interfaces/handlers/IBosonAccountHandler.sol +++ b/contracts/interfaces/handlers/IBosonAccountHandler.sol @@ -9,7 +9,7 @@ import { IBosonAccountEvents } from "../events/IBosonAccountEvents.sol"; * * @notice Handles creation, update, retrieval of accounts within the protocol. * - * The ERC-165 identifier for this interface is: 0xee3cb2a6 + * The ERC-165 identifier for this interface is: 0xfe645260 */ interface IBosonAccountHandler is IBosonAccountEvents { /** @@ -374,6 +374,18 @@ interface IBosonAccountHandler is IBosonAccountEvents { BosonTypes.AuthToken memory authToken ); + /** + * @notice Gets the details about a seller's collections. + * + * @param _sellerId - the id of the seller to check + * @return defaultVoucherAddress - the address of the default voucher contract for the seller + * @return additionalCollections - an array of additional collections that the seller has created + */ + function getSellersCollections(uint256 _sellerId) + external + view + returns (address defaultVoucherAddress, BosonTypes.Collection[] memory additionalCollections); + /** * @notice Gets the details about a buyer. * diff --git a/contracts/protocol/bases/OfferBase.sol b/contracts/protocol/bases/OfferBase.sol index 3e62491af..e99c3907c 100644 --- a/contracts/protocol/bases/OfferBase.sol +++ b/contracts/protocol/bases/OfferBase.sol @@ -199,7 +199,8 @@ contract OfferBase is ProtocolBase, IBosonOfferEvents { // Collection must exist. Collections with index 0 exist by default. if (_offer.collectionIndex > 0) { require( - lookups.additionalCloneAddresses[_offer.sellerId][_offer.collectionIndex] != address(0), + lookups.additionalCollections[_offer.sellerId][_offer.collectionIndex].collectionAddress != + address(0), NO_SUCH_COLLECTION ); } diff --git a/contracts/protocol/bases/ProtocolBase.sol b/contracts/protocol/bases/ProtocolBase.sol index 2fd4afb64..56455cb18 100644 --- a/contracts/protocol/bases/ProtocolBase.sol +++ b/contracts/protocol/bases/ProtocolBase.sol @@ -739,6 +739,6 @@ abstract contract ProtocolBase is PausableBase, ReentrancyGuardBase { return _collectionIndex == 0 ? _lookups.cloneAddress[_sellerId] - : _lookups.additionalCloneAddresses[_sellerId][_collectionIndex - 1]; + : _lookups.additionalCollections[_sellerId][_collectionIndex - 1].collectionAddress; } } diff --git a/contracts/protocol/facets/SellerHandlerFacet.sol b/contracts/protocol/facets/SellerHandlerFacet.sol index 1926ead1c..29866ab93 100644 --- a/contracts/protocol/facets/SellerHandlerFacet.sol +++ b/contracts/protocol/facets/SellerHandlerFacet.sol @@ -255,11 +255,11 @@ contract SellerHandlerFacet is SellerBase { // Transfer ownership of voucher contract to new assistant IBosonVoucher(lookups.cloneAddress[_sellerId]).transferOwnership(sender); // default voucher contract - address[] storage sellersAdditionalCloneAddresses = lookups.additionalCloneAddresses[_sellerId]; + Collection[] storage sellersAdditionalCollections = lookups.additionalCollections[_sellerId]; uint256 collectionCount; for (i = 0; i < collectionCount; i++) { // Additional collections (if they exist) - IBosonVoucher(sellersAdditionalCloneAddresses[i]).transferOwnership(sender); + IBosonVoucher(sellersAdditionalCollections[i].collectionAddress).transferOwnership(sender); } // Store new seller id by assistant mapping @@ -359,12 +359,15 @@ contract SellerHandlerFacet is SellerBase { _voucherInitValues.contractURI = _contractURI; // NB: we don't set any royalties here, since they are managed inside the protocol after BPIP-5 - address[] storage sellersAdditionalCloneAddresses = protocolLookups().additionalCloneAddresses[sellerId]; - uint256 collectionIndex = sellersAdditionalCloneAddresses.length + 1; // 0 is reserved for the original collection + Collection[] storage sellersAdditionalCollections = protocolLookups().additionalCollections[sellerId]; + uint256 collectionIndex = sellersAdditionalCollections.length + 1; // 0 is reserved for the original collection - // Create clone and store its address to additionalCloneAddresses + // Create clone and store its address to additionalCollections address voucherCloneAddress = cloneBosonVoucher(sellerId, collectionIndex, assistant, _voucherInitValues); - sellersAdditionalCloneAddresses.push(voucherCloneAddress); + Collection memory newCollection; + newCollection.collectionAddress = voucherCloneAddress; + newCollection.externalId = _externalId; + sellersAdditionalCollections.push(newCollection); emit CollectionCreated(sellerId, collectionIndex, voucherCloneAddress, _externalId, assistant); } @@ -457,6 +460,22 @@ contract SellerHandlerFacet is SellerBase { } } + /** + * @notice Gets the details about a seller's collections. + * + * @param _sellerId - the id of the seller to check + * @return defaultVoucherAddress - the address of the default voucher contract for the seller + * @return additionalCollections - an array of additional collections that the seller has created + */ + function getSellersCollections(uint256 _sellerId) + external + view + returns (address defaultVoucherAddress, Collection[] memory additionalCollections) + { + ProtocolLib.ProtocolLookups storage pl = protocolLookups(); + return (pl.cloneAddress[_sellerId], pl.additionalCollections[_sellerId]); + } + /** * @notice Pre update Seller checks * diff --git a/contracts/protocol/libs/ProtocolLib.sol b/contracts/protocol/libs/ProtocolLib.sol index 524f6121d..e63d79656 100644 --- a/contracts/protocol/libs/ProtocolLib.sol +++ b/contracts/protocol/libs/ProtocolLib.sol @@ -182,8 +182,8 @@ library ProtocolLib { mapping(uint256 => BosonTypes.AuthToken) pendingAuthTokenUpdatesBySeller; // dispute resolver id => DisputeResolver mapping(uint256 => BosonTypes.DisputeResolver) pendingAddressUpdatesByDisputeResolver; - // seller id => cloneAddresses - mapping(uint256 => address[]) additionalCloneAddresses; + // seller id => list of additional collections (address + external id) + mapping(uint256 => BosonTypes.Collection[]) additionalCollections; } // Incrementing id counters diff --git a/scripts/domain/Collection.js b/scripts/domain/Collection.js new file mode 100644 index 000000000..c50b17e8a --- /dev/null +++ b/scripts/domain/Collection.js @@ -0,0 +1,200 @@ +const { stringIsValid, addressIsValid } = require("../util/validations.js"); + +/** + * Boson Protocol Domain Entity: Collection + * + * See: {BosonTypes.Collection} + */ +class Collection { + /* + struct Collection { + address collectionAddress; + string externalId; + } + */ + + constructor(collectionAddress, externalId) { + this.collectionAddress = collectionAddress; + this.externalId = externalId; + } + + /** + * Get a new Collection instance from a pojo representation + * @param o + * @returns {Collection} + */ + static fromObject(o) { + const { collectionAddress, externalId } = o; + return new Collection(collectionAddress, externalId); + } + + /** + * Get a new Collection instance from a returned struct representation + * @param struct + * @returns {*} + */ + static fromStruct(struct) { + let collectionAddress, externalId; + + // destructure struct + [collectionAddress, externalId] = struct; + + return Collection.fromObject({ + collectionAddress, + externalId, + }); + } + + /** + * Get a database representation of this Collection instance + * @returns {object} + */ + toObject() { + return JSON.parse(this.toString()); + } + + /** + * Get a string representation of this Collection instance + * @returns {string} + */ + toString() { + return JSON.stringify(this); + } + + /** + * Get a struct representation of this Collection instance + * @returns {string} + */ + toStruct() { + return [this.collectionAddress, this.externalId]; + } + + /** + * Clone this Collection + * @returns {Collection} + */ + clone() { + return Collection.fromObject(this.toObject()); + } + + /** + * Is this Collection instance's collectionAddress field valid? + * Must be a eip55 compliant Ethereum address + * @returns {boolean} + */ + collectionAddressIsValid() { + return addressIsValid(this.collectionAddress); + } + + /** + * Is this Collection instance's externalId field valid? + * Always present, must be a string + * @returns {boolean} + */ + externalIdIsValid() { + return stringIsValid(this.externalId); + } + + /** + * Is this Collection instance valid? + * @returns {boolean} + */ + isValid() { + return this.collectionAddressIsValid() && this.externalIdIsValid(); + } +} + +/** + * Boson Protocol Domain Entity: Collection of Collection + * + * See: {BosonTypes.Collection} + */ +class CollectionList { + constructor(collections) { + this.collections = collections; + } + + /** + * Get a new CollectionList instance from a pojo representation + * @param o + * @returns {CollectionList} + */ + static fromObject(o) { + const { collections } = o; + return new CollectionList(collections.map((d) => Collection.fromObject(d))); + } + + /** + * Get a new CollectionList instance from a returned struct representation + * @param struct + * @returns {*} + */ + static fromStruct(struct) { + return CollectionList.fromObject({ + collections: struct.map((collections) => Collection.fromStruct(collections)), + }); + } + + /** + * Get a database representation of this CollectionList instance + * @returns {object} + */ + toObject() { + return JSON.parse(this.toString()); + } + + /** + * Get a string representation of this CollectionList instance + * @returns {string} + */ + toString() { + return JSON.stringify(this); + } + + /** + * Get a struct representation of this CollectionList instance + * @returns {string} + */ + toStruct() { + return this.collections.map((d) => d.toStruct()); + } + + /** + * Clone this CollectionList + * @returns {CollectionList} + */ + clone() { + return CollectionList.fromObject(this.toObject()); + } + + /** + * Is this CollectionList instance's collection field valid? + * Must be a list of Collection instances + * @returns {boolean} + */ + collectionIsValid() { + let valid = false; + let { collections } = this; + try { + valid = + Array.isArray(collections) && + collections.reduce( + (previousCollections, currentCollections) => previousCollections && currentCollections.isValid(), + true + ); + } catch (e) {} + return valid; + } + + /** + * Is this CollectionList instance valid? + * @returns {boolean} + */ + isValid() { + return this.collectionIsValid(); + } +} + +// Export +exports.Collection = Collection; +exports.CollectionList = CollectionList; diff --git a/scripts/domain/Offer.js b/scripts/domain/Offer.js index fdaddbc5a..1e020a565 100644 --- a/scripts/domain/Offer.js +++ b/scripts/domain/Offer.js @@ -18,6 +18,7 @@ class Offer { string metadataUri; string metadataHash; bool voided; + uint256 collectionIndex; } */ @@ -31,7 +32,8 @@ class Offer { exchangeToken, metadataUri, metadataHash, - voided + voided, + collectionIndex ) { this.id = id; this.sellerId = sellerId; @@ -43,6 +45,7 @@ class Offer { this.metadataUri = metadataUri; this.metadataHash = metadataHash; this.voided = voided; + this.collectionIndex = collectionIndex; } /** @@ -62,6 +65,7 @@ class Offer { metadataUri, metadataHash, voided, + collectionIndex, } = o; return new Offer( @@ -74,7 +78,8 @@ class Offer { exchangeToken, metadataUri, metadataHash, - voided + voided, + collectionIndex ); } @@ -93,7 +98,8 @@ class Offer { exchangeToken, metadataUri, metadataHash, - voided; + voided, + collectionIndex; // destructure struct [ @@ -107,6 +113,7 @@ class Offer { metadataUri, metadataHash, voided, + collectionIndex, ] = struct; return Offer.fromObject({ @@ -120,6 +127,7 @@ class Offer { metadataUri, metadataHash, voided, + collectionIndex: collectionIndex.toString(), }); } @@ -155,6 +163,7 @@ class Offer { this.metadataUri, this.metadataHash, this.voided, + this.collectionIndex, ]; } @@ -259,6 +268,15 @@ class Offer { return booleanIsValid(this.voided); } + /** + * Is this Offer instance's collectionIndex field valid? + * Must be a string representation of a big number + * @returns {boolean} + */ + collectionIndexIsValid() { + return bigNumberIsValid(this.collectionIndex); + } + /** * Is this Offer instance valid? * @returns {boolean} @@ -274,7 +292,8 @@ class Offer { this.exchangeTokenIsValid() && this.metadataUriIsValid() && this.metadataHashIsValid() && - this.voidedIsValid() + this.voidedIsValid() && + this.collectionIndexIsValid() ); } } diff --git a/test/domain/CollectionTest.js b/test/domain/CollectionTest.js new file mode 100644 index 000000000..35963cf1e --- /dev/null +++ b/test/domain/CollectionTest.js @@ -0,0 +1,324 @@ +const hre = require("hardhat"); +const ethers = hre.ethers; +const { expect } = require("chai"); +const { Collection, CollectionList } = require("../../scripts/domain/Collection"); + +/** + * Test the Collection domain entity + */ +describe("Collection", function () { + // Suite-wide scope + let accounts, collection, object, promoted, clone, dehydrated, rehydrated, key, value, struct; + let collectionAddress, externalId; + + beforeEach(async function () { + // Get a list of accounts + accounts = await ethers.getSigners(); + collectionAddress = accounts[1].address; + + // Required constructor params + externalId = "Brand1"; + }); + + context("📋 Constructor", async function () { + it("Should allow creation of valid, fully populated Collection instance", async function () { + // Create valid collection + collection = new Collection(collectionAddress, externalId); + expect(collection.collectionAddressIsValid()).is.true; + expect(collection.externalIdIsValid()).is.true; + expect(collection.isValid()).is.true; + }); + }); + + context("📋 Field validations", async function () { + beforeEach(async function () { + // Create valid collection, then set fields in tests directly + collection = new Collection(collectionAddress, externalId); + expect(collection.isValid()).is.true; + }); + + it("Always present, collectionAddress must be a string representation of an EIP-55 compliant address", async function () { + // Invalid field value + collection.collectionAddress = "0xASFADF"; + expect(collection.collectionAddressIsValid()).is.false; + expect(collection.isValid()).is.false; + + // Invalid field value + collection.collectionAddress = "zedzdeadbaby"; + expect(collection.collectionAddressIsValid()).is.false; + expect(collection.isValid()).is.false; + + // Valid field value + collection.collectionAddress = accounts[0].address; + expect(collection.collectionAddressIsValid()).is.true; + expect(collection.isValid()).is.true; + + // Valid field value + collection.collectionAddress = "0xec2fd5bd6fc7b576dae82c0b9640969d8de501a2"; + expect(collection.collectionAddressIsValid()).is.true; + expect(collection.isValid()).is.true; + }); + + it("Always present, externalId must be a string", async function () { + // Invalid field value + collection.externalId = 12; + expect(collection.externalIdIsValid()).is.false; + expect(collection.isValid()).is.false; + + // Valid field value + collection.externalId = "zedzdeadbaby"; + expect(collection.externalIdIsValid()).is.true; + expect(collection.isValid()).is.true; + + // Valid field value + collection.externalId = "QmYXc12ov6F2MZVZwPs5XeCBbf61cW3wKRk8h3D5NTYj4T"; + expect(collection.externalIdIsValid()).is.true; + expect(collection.isValid()).is.true; + + // Valid field value + collection.externalId = ""; + expect(collection.externalIdIsValid()).is.true; + expect(collection.isValid()).is.true; + }); + }); + + context("📋 Utility functions", async function () { + beforeEach(async function () { + // Create valid collection, then set fields in tests directly + collection = new Collection(collectionAddress, externalId); + + expect(collection.isValid()).is.true; + + // Get plain object + object = { + collectionAddress, + externalId, + }; + + // Struct representation + struct = [collectionAddress, externalId]; + }); + + context("👉 Static", async function () { + it("Collection.fromObject() should return a Collection instance with the same values as the given plain object", async function () { + // Promote to instance + promoted = Collection.fromObject(object); + + // Is a Collection instance + expect(promoted instanceof Collection).is.true; + + // Key values all match + for ([key, value] of Object.entries(collection)) { + expect(JSON.stringify(promoted[key]) === JSON.stringify(value)).is.true; + } + }); + + it("Collection.fromStruct() should return a Collection instance from a struct representation", async function () { + // Get an instance from the struct + collection = Collection.fromStruct(struct); + + // Ensure it is valid + expect(collection.isValid()).to.be.true; + }); + }); + + context("👉 Instance", async function () { + it("instance.toString() should return a JSON string representation of the Collection instance", async function () { + dehydrated = collection.toString(); + rehydrated = JSON.parse(dehydrated); + + for ([key, value] of Object.entries(collection)) { + expect(JSON.stringify(rehydrated[key]) === JSON.stringify(value)).is.true; + } + }); + + it("instance.toObject() should return a plain object representation of the Collection instance", async function () { + // Get plain object + object = collection.toObject(); + + // Not a Collection instance + expect(object instanceof Collection).is.false; + + // Key values all match + for ([key, value] of Object.entries(collection)) { + expect(JSON.stringify(object[key]) === JSON.stringify(value)).is.true; + } + }); + + it("Collection.toStruct() should return a struct representation of the Collection instance", async function () { + // Get struct from collection + struct = collection.toStruct(); + + // Marshal back to a collection instance + collection = Collection.fromStruct(struct); + + // Ensure it marshals back to a valid collection + expect(collection.isValid()).to.be.true; + }); + + it("instance.clone() should return another Collection instance with the same property values", async function () { + // Get plain object + clone = collection.clone(); + + // Is a Collection instance + expect(clone instanceof Collection).is.true; + + // Key values all match + for ([key, value] of Object.entries(collection)) { + expect(JSON.stringify(clone[key]) === JSON.stringify(value)).is.true; + } + }); + }); + }); +}); + +describe("CollectionList", function () { + // Suite-wide scope + let accounts, collections, collectionList, object, promoted, clone, dehydrated, rehydrated, key, value, struct; + + beforeEach(async function () { + // Get a list of accounts + accounts = await ethers.getSigners(); + + // Required constructor params + collections = [ + new Collection(accounts[1].address, "MockToken1", "100"), + new Collection(accounts[2].address, "MockToken2", "200"), + new Collection(accounts[3].address, "MockToken3", "300"), + ]; + }); + + context("📋 Constructor", async function () { + it("Should allow creation of valid, fully populated CollectionList instance", async function () { + // Create valid CollectionList + collectionList = new CollectionList(collections); + expect(collectionList.collectionIsValid()).is.true; + expect(collectionList.isValid()).is.true; + }); + }); + + context("📋 Field validations", async function () { + beforeEach(async function () { + // Create valid CollectionList, then set fields in tests directly + collectionList = new CollectionList(collections); + expect(collectionList.isValid()).is.true; + }); + + it("Always present, collections must be an array of valid Collection instances", async function () { + // Invalid field value + collectionList.collections = "0xASFADF"; + expect(collectionList.isValid()).is.false; + + // Invalid field value + collectionList.collection = collections[0]; + expect(collectionList.isValid()).is.false; + + // Invalid field value + collectionList.collections = ["0xASFADF", "zedzdeadbaby"]; + expect(collectionList.isValid()).is.false; + + // Invalid field value + collectionList.collections = undefined; + expect(collectionList.isValid()).is.false; + + // Invalid field value + collectionList.collections = [...collections, "zedzdeadbaby"]; + expect(collectionList.isValid()).is.false; + + // Invalid field value + collectionList.collections = [new Collection("111", "mockToken", "100")]; + expect(collectionList.isValid()).is.false; + + // Valid field value + collectionList.collections = [...collections]; + expect(collectionList.isValid()).is.true; + }); + }); + + context("📋 Utility functions", async function () { + beforeEach(async function () { + // Create valid CollectionList, then set fields in tests directly + collectionList = new CollectionList(collections); + expect(collectionList.isValid()).is.true; + + // Get plain object + object = { + collections, + }; + + // Struct representation + struct = collections.map((d) => d.toStruct()); + }); + + context("👉 Static", async function () { + it("CollectionList.fromObject() should return a CollectionList instance with the same values as the given plain object", async function () { + // Promote to instance + promoted = CollectionList.fromObject(object); + + // Is a CollectionList instance + expect(promoted instanceof CollectionList).is.true; + + // Key values all match + for ([key, value] of Object.entries(collectionList)) { + expect(JSON.stringify(promoted[key]) === JSON.stringify(value)).is.true; + } + }); + + it("CollectionList.fromStruct() should return a CollectionList instance from a struct representation", async function () { + // Get an instance from the struct + collectionList = CollectionList.fromStruct(struct); + + // Ensure it is valid + expect(collectionList.isValid()).to.be.true; + }); + }); + + context("👉 Instance", async function () { + it("instance.toString() should return a JSON string representation of the CollectionList instance", async function () { + dehydrated = collectionList.toString(); + rehydrated = JSON.parse(dehydrated); + + for ([key, value] of Object.entries(collectionList)) { + expect(JSON.stringify(rehydrated[key]) === JSON.stringify(value)).is.true; + } + }); + + it("instance.toObject() should return a plain object representation of the CollectionList instance", async function () { + // Get plain object + object = collectionList.toObject(); + + // Not a CollectionList instance + expect(object instanceof CollectionList).is.false; + + // Key values all match + for ([key, value] of Object.entries(collectionList)) { + expect(JSON.stringify(object[key]) === JSON.stringify(value)).is.true; + } + }); + + it("CollectionList.toStruct() should return a struct representation of the CollectionList instance", async function () { + // Get struct from CollectionList + struct = collectionList.toStruct(); + + // Marshal back to a CollectionList instance + collectionList = CollectionList.fromStruct(struct); + + // Ensure it marshals back to a valid CollectionList + expect(collectionList.isValid()).to.be.true; + }); + + it("instance.clone() should return another CollectionList instance with the same property values", async function () { + // Get plain object + clone = collectionList.clone(); + + // Is a CollectionList instance + expect(clone instanceof CollectionList).is.true; + + // Key values all match + for ([key, value] of Object.entries(collectionList)) { + expect(JSON.stringify(clone[key]) === JSON.stringify(value)).is.true; + } + }); + }); + }); +}); diff --git a/test/domain/OfferTest.js b/test/domain/OfferTest.js index d5e4c3064..53dca7d1a 100644 --- a/test/domain/OfferTest.js +++ b/test/domain/OfferTest.js @@ -19,7 +19,8 @@ describe("Offer", function () { exchangeToken, metadataUri, metadataHash, - voided; + voided, + collectionIndex; beforeEach(async function () { // Get a list of accounts @@ -35,6 +36,7 @@ describe("Offer", function () { metadataHash = "QmYXc12ov6F2MZVZwPs5XeCBbf61cW3wKRk8h3D5NTYj4T"; // not an actual metadataHash, just some data for tests metadataUri = `https://ipfs.io/ipfs/${metadataHash}`; voided = false; + collectionIndex = "2"; }); context("📋 Constructor", async function () { @@ -50,7 +52,8 @@ describe("Offer", function () { exchangeToken, metadataUri, metadataHash, - voided + voided, + collectionIndex ); expect(offer.idIsValid()).is.true; expect(offer.sellerIdIsValid()).is.true; @@ -63,6 +66,7 @@ describe("Offer", function () { expect(offer.metadataHashIsValid()).is.true; expect(offer.voidedIsValid()).is.true; expect(offer.isValid()).is.true; + expect(offer.collectionIndexIsValid()).is.true; }); }); @@ -79,7 +83,8 @@ describe("Offer", function () { exchangeToken, metadataUri, metadataHash, - voided + voided, + collectionIndex ); expect(offer.isValid()).is.true; }); @@ -323,6 +328,33 @@ describe("Offer", function () { expect(offer.voidedIsValid()).is.true; expect(offer.isValid()).is.true; }); + + it("Always present, collectionIndex must be the string representation of a BigNumber", async function () { + // Invalid field value + offer.collectionIndex = "zedzdeadbaby"; + expect(offer.collectionIndexIsValid()).is.false; + expect(offer.isValid()).is.false; + + // Invalid field value + offer.collectionIndex = new Date(); + expect(offer.collectionIndexIsValid()).is.false; + expect(offer.isValid()).is.false; + + // Invalid field value + offer.collectionIndex = 12; + expect(offer.collectionIndexIsValid()).is.false; + expect(offer.isValid()).is.false; + + // Valid field value + offer.collectionIndex = "0"; + expect(offer.collectionIndexIsValid()).is.true; + expect(offer.isValid()).is.true; + + // Valid field value + offer.collectionIndex = "126"; + expect(offer.collectionIndexIsValid()).is.true; + expect(offer.isValid()).is.true; + }); }); context("📋 Utility functions", async function () { @@ -341,7 +373,8 @@ describe("Offer", function () { exchangeToken, metadataUri, metadataHash, - voided + voided, + collectionIndex ); expect(offer.isValid()).is.true; @@ -357,6 +390,7 @@ describe("Offer", function () { metadataUri, metadataHash, voided, + collectionIndex, }; }); @@ -386,6 +420,7 @@ describe("Offer", function () { offer.metadataUri, offer.metadataHash, offer.voided, + offer.collectionIndex, ]; // Get struct diff --git a/test/protocol/OrchestrationHandlerTest.js b/test/protocol/OrchestrationHandlerTest.js index 5414c3ec6..6b364d515 100644 --- a/test/protocol/OrchestrationHandlerTest.js +++ b/test/protocol/OrchestrationHandlerTest.js @@ -809,16 +809,26 @@ describe("IBosonOrchestrationHandler", function () { expect(JSON.stringify(returnedDisputeResolutionTermsStruct[key]) === JSON.stringify(value)).is.true; } - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(orchestrationHandler.address, "1"); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); expect(await bosonVoucher.owner()).to.equal(assistant.address, "Wrong voucher clone owner"); bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); }); it("should update state when voucherInitValues has zero royaltyPercentage and exchangeId does not exist", async function () { @@ -842,11 +852,21 @@ describe("IBosonOrchestrationHandler", function () { agentId ); + // Get the collections information expectedCloneAddress = calculateContractAddress(orchestrationHandler.address, "1"); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -890,11 +910,21 @@ describe("IBosonOrchestrationHandler", function () { agentId ); + // Get the collections information expectedCloneAddress = calculateContractAddress(orchestrationHandler.address, "1"); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -1363,16 +1393,29 @@ describe("IBosonOrchestrationHandler", function () { expect(JSON.stringify(returnedDisputeResolutionTermsStruct[key]) === JSON.stringify(value)).is.true; } - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(orchestrationHandler.address, "1"); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); expect(await bosonVoucher.owner()).to.equal(assistant.address, "Wrong voucher clone owner"); bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal( + VOUCHER_NAME + " " + seller.id + "_0", + "Wrong voucher client name" + ); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); const returnedRange = Range.fromStruct(await bosonVoucher.getRangeByOfferId(offer.id)); assert.equal(returnedRange.toString(), range.toString(), "Range mismatch"); const availablePremints = await bosonVoucher.getAvailablePreMints(offer.id); @@ -5398,16 +5441,26 @@ describe("IBosonOrchestrationHandler", function () { expect(JSON.stringify(returnedCondition[key]) === JSON.stringify(value)).is.true; } - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(orchestrationHandler.address, "1"); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); expect(await bosonVoucher.owner()).to.equal(assistant.address, "Wrong voucher clone owner"); bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); }); it("should update state when voucherInitValues has zero royaltyPercentage and exchangeId does not exist", async function () { @@ -5430,12 +5483,22 @@ describe("IBosonOrchestrationHandler", function () { agentId ); - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(orchestrationHandler.address, "1"); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -5478,12 +5541,22 @@ describe("IBosonOrchestrationHandler", function () { agentId ); - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(orchestrationHandler.address, "1"); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -5811,16 +5884,29 @@ describe("IBosonOrchestrationHandler", function () { expect(JSON.stringify(returnedCondition[key]) === JSON.stringify(value)).is.true; } - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(orchestrationHandler.address, "1"); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); expect(await bosonVoucher.owner()).to.equal(assistant.address, "Wrong voucher clone owner"); bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal( + VOUCHER_NAME + " " + seller.id + "_0", + "Wrong voucher client name" + ); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); const returnedRange = Range.fromStruct(await bosonVoucher.getRangeByOfferId(offer.id)); assert.equal(returnedRange.toString(), range.toString(), "Range mismatch"); const availablePremints = await bosonVoucher.getAvailablePreMints(offer.id); @@ -6135,16 +6221,26 @@ describe("IBosonOrchestrationHandler", function () { expect(JSON.stringify(returnedBundle[key]) === JSON.stringify(value)).is.true; } - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(orchestrationHandler.address, "1"); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); expect(await bosonVoucher.owner()).to.equal(assistant.address, "Wrong voucher clone owner"); bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); }); it("should update state when voucherInitValues has zero royaltyPercentage and exchangeId does not exist", async function () { @@ -6170,12 +6266,22 @@ describe("IBosonOrchestrationHandler", function () { agentId ); - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(orchestrationHandler.address, "1"); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -6221,12 +6327,22 @@ describe("IBosonOrchestrationHandler", function () { agentId ); - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(orchestrationHandler.address, "1"); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -6609,16 +6725,29 @@ describe("IBosonOrchestrationHandler", function () { expect(JSON.stringify(returnedBundle[key]) === JSON.stringify(value)).is.true; } - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(orchestrationHandler.address, "1"); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); expect(await bosonVoucher.owner()).to.equal(assistant.address, "Wrong voucher clone owner"); bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal( + VOUCHER_NAME + " " + seller.id + "_0", + "Wrong voucher client name" + ); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); const returnedRange = Range.fromStruct(await bosonVoucher.getRangeByOfferId(offer.id)); assert.equal(returnedRange.toString(), range.toString(), "Range mismatch"); const availablePremints = await bosonVoucher.getAvailablePreMints(offer.id); @@ -7002,16 +7131,26 @@ describe("IBosonOrchestrationHandler", function () { expect(JSON.stringify(returnedBundle[key]) === JSON.stringify(value)).is.true; } - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(orchestrationHandler.address, "1"); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); expect(await bosonVoucher.owner()).to.equal(assistant.address, "Wrong voucher clone owner"); bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); }); it("should update state when voucherInitValues has zero royaltyPercentage and exchangeId does not exist", async function () { @@ -7038,12 +7177,22 @@ describe("IBosonOrchestrationHandler", function () { agentId ); - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(orchestrationHandler.address, "1"); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -7090,12 +7239,22 @@ describe("IBosonOrchestrationHandler", function () { agentId ); - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(orchestrationHandler.address, "1"); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -7538,16 +7697,29 @@ describe("IBosonOrchestrationHandler", function () { expect(JSON.stringify(returnedBundle[key]) === JSON.stringify(value)).is.true; } - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(orchestrationHandler.address, "1"); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); expect(await bosonVoucher.owner()).to.equal(assistant.address, "Wrong voucher clone owner"); bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal( + VOUCHER_NAME + " " + seller.id + "_0", + "Wrong voucher client name" + ); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); const returnedRange = Range.fromStruct(await bosonVoucher.getRangeByOfferId(offer.id)); assert.equal(returnedRange.toString(), range.toString(), "Range mismatch"); const availablePremints = await bosonVoucher.getAvailablePreMints(offer.id); diff --git a/test/protocol/SellerHandlerTest.js b/test/protocol/SellerHandlerTest.js index e49bd6151..b6d4b3c8d 100644 --- a/test/protocol/SellerHandlerTest.js +++ b/test/protocol/SellerHandlerTest.js @@ -297,6 +297,13 @@ describe("SellerHandler", function () { expect(JSON.stringify(returnedAuthToken[key]) === JSON.stringify(value)).is.true; } + // Get the collections information + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + // Voucher clone contract bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); @@ -304,8 +311,11 @@ describe("SellerHandler", function () { bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); }); it("should update state when voucherInitValues has zero royaltyPercentage and exchangeId does not exist", async function () { @@ -318,8 +328,11 @@ describe("SellerHandler", function () { bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -352,8 +365,11 @@ describe("SellerHandler", function () { bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -399,6 +415,13 @@ describe("SellerHandler", function () { expect(JSON.stringify(returnedAuthToken[key]) === JSON.stringify(value)).is.true; } + // Get the collections information + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + // Voucher clone contract bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); @@ -406,8 +429,11 @@ describe("SellerHandler", function () { bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); }); it("should ignore any provided id and assign the next available", async function () { diff --git a/test/util/mock.js b/test/util/mock.js index fe9da9a4d..e784bbfd3 100644 --- a/test/util/mock.js +++ b/test/util/mock.js @@ -78,6 +78,7 @@ async function mockOffer() { const metadataHash = "QmYXc12ov6F2MZVZwPs5XeCBbf61cW3wKRk8h3D5NTYj4T"; // not an actual metadataHash, just some data for tests const metadataUri = `https://ipfs.io/ipfs/${metadataHash}`; const voided = false; + const collectionIndex = "0"; // Create a valid offer, then set fields in tests directly let offer = new Offer( @@ -90,7 +91,8 @@ async function mockOffer() { exchangeToken, metadataUri, metadataHash, - voided + voided, + collectionIndex ); const offerDates = await mockOfferDates(); From 1d84e2d43869db22388fbc9f437222788f5a33df Mon Sep 17 00:00:00 2001 From: zajck Date: Mon, 27 Mar 2023 11:44:11 +0200 Subject: [PATCH 04/12] seller tests --- .../protocol/facets/SellerHandlerFacet.sol | 2 +- test/protocol/SellerHandlerTest.js | 202 ++++++++++++++++++ 2 files changed, 203 insertions(+), 1 deletion(-) diff --git a/contracts/protocol/facets/SellerHandlerFacet.sol b/contracts/protocol/facets/SellerHandlerFacet.sol index 29866ab93..6a06f50f5 100644 --- a/contracts/protocol/facets/SellerHandlerFacet.sol +++ b/contracts/protocol/facets/SellerHandlerFacet.sol @@ -349,7 +349,7 @@ contract SellerHandlerFacet is SellerBase { * @param _externalId - external collection id * @param _contractURI - contract URI */ - function createNewCollection(string calldata _externalId, string calldata _contractURI) external { + function createNewCollection(string calldata _externalId, string calldata _contractURI) external sellersNotPaused { address assistant = msgSender(); (bool exists, uint256 sellerId) = getSellerIdByAssistant(assistant); diff --git a/test/protocol/SellerHandlerTest.js b/test/protocol/SellerHandlerTest.js index b6d4b3c8d..9659f7d4a 100644 --- a/test/protocol/SellerHandlerTest.js +++ b/test/protocol/SellerHandlerTest.js @@ -15,6 +15,7 @@ const { calculateContractAddress, getFacetsWithArgs } = require("../util/utils.j const { oneWeek, oneMonth, VOUCHER_NAME, VOUCHER_SYMBOL, maxPriorityFeePerGas } = require("../util/constants"); const { deployMockTokens } = require("../../scripts/util/deploy-mock-tokens"); const { mockSeller, mockAuthToken, mockVoucherInitValues, accountId } = require("../util/mock"); +const { Collection, CollectionList } = require("../../scripts/domain/Collection"); /** * Test the Boson Seller Handler @@ -2865,5 +2866,206 @@ describe("SellerHandler", function () { }); }); }); + + context("👉 createNewCollection()", async function () { + let externalId, expectedDefaultAddress, expectedCollectionAddress; + + beforeEach(async function () { + // Create a seller + await accountHandler.connect(admin).createSeller(seller, emptyAuthToken, voucherInitValues); + + externalId = "Brand1"; + contractURI = "https://brand1.com"; + expectedDefaultAddress = calculateContractAddress(accountHandler.address, "1"); // default + expectedCollectionAddress = calculateContractAddress(accountHandler.address, "2"); + }); + + it("should emit a CollectionCreated event", async function () { + // Create a new collection, testing for the event + const tx = await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + + await expect(tx) + .to.emit(accountHandler, "CollectionCreated") + .withArgs(seller.id, 1, expectedCollectionAddress, externalId, assistant.address); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCollectionAddress); + + await expect(tx).to.emit(bosonVoucher, "ContractURIChanged").withArgs(contractURI); + + await expect(tx).to.emit(bosonVoucher, "RoyaltyPercentageChanged").withArgs("0"); + + await expect(tx).to.emit(bosonVoucher, "VoucherInitialized").withArgs(seller.id, "0", contractURI); + + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress); + + await expect(tx) + .to.emit(bosonVoucher, "OwnershipTransferred") + .withArgs(ethers.constants.AddressZero, assistant.address); + }); + + it("should update state", async function () { + // Create a new collection + await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + + const expectedCollections = new CollectionList([new Collection(expectedCollectionAddress, externalId)]); + + // Get the collections information + const [defaultVoucherAddress, collections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + const additionalCollections = CollectionList.fromStruct(collections); + expect(defaultVoucherAddress).to.equal(expectedDefaultAddress, "Wrong default voucher address"); + expect(additionalCollections).to.deep.equal(expectedCollections, "Wrong additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress); + + expect(await bosonVoucher.owner()).to.equal(assistant.address, "Wrong voucher clone owner"); + + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCollectionAddress); + expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_1", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_1", + "Wrong voucher client symbol" + ); + }); + + it("create multiple collections", async function () { + const expectedCollections = new CollectionList([]); + + for (let i = 1; i < 4; i++) { + expectedCollectionAddress = calculateContractAddress(accountHandler.address, (i + 1).toString()); + externalId = `Brand${i}`; + contractURI = `https://brand${i}.com`; + + // Create a new collection, testing for the event + const tx = await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + + await expect(tx) + .to.emit(accountHandler, "CollectionCreated") + .withArgs(seller.id, i, expectedCollectionAddress, externalId, assistant.address); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCollectionAddress); + + await expect(tx).to.emit(bosonVoucher, "ContractURIChanged").withArgs(contractURI); + + await expect(tx).to.emit(bosonVoucher, "RoyaltyPercentageChanged").withArgs("0"); + + await expect(tx).to.emit(bosonVoucher, "VoucherInitialized").withArgs(seller.id, "0", contractURI); + + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress); + + await expect(tx) + .to.emit(bosonVoucher, "OwnershipTransferred") + .withArgs(ethers.constants.AddressZero, assistant.address); + + // Get the collections information + expectedCollections.collections.push(new Collection(expectedCollectionAddress, externalId)); + const [defaultVoucherAddress, collections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + const additionalCollections = CollectionList.fromStruct(collections); + expect(defaultVoucherAddress).to.equal(expectedDefaultAddress, "Wrong default voucher address"); + expect(additionalCollections).to.deep.equal(expectedCollections, "Wrong additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress); + + expect(await bosonVoucher.owner()).to.equal(assistant.address, "Wrong voucher clone owner"); + + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCollectionAddress); + expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); + expect(await bosonVoucher.name()).to.equal( + VOUCHER_NAME + " " + seller.id + "_" + i, + "Wrong voucher client name" + ); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_" + i, + "Wrong voucher client symbol" + ); + } + }); + + context("💔 Revert Reasons", async function () { + it("The sellers region of protocol is paused", async function () { + // Pause the sellers region of the protocol + await pauseHandler.connect(pauser).pause([PausableRegion.Sellers]); + + // Attempt to create a new collection expecting revert + await expect(accountHandler.connect(assistant).createNewCollection(externalId, contractURI)).to.revertedWith( + RevertReasons.REGION_PAUSED + ); + }); + + it("Caller is not anyone's assistant", async function () { + // Attempt to create a new collection + await expect(accountHandler.connect(rando).createNewCollection(externalId, contractURI)).to.revertedWith( + RevertReasons.NO_SUCH_SELLER + ); + }); + }); + }); + + context("👉 getSellersCollections()", async function () { + let externalId, expectedDefaultAddress, expectedCollectionAddress; + + beforeEach(async function () { + // Create a seller + await accountHandler.connect(admin).createSeller(seller, emptyAuthToken, voucherInitValues); + + expectedDefaultAddress = calculateContractAddress(accountHandler.address, "1"); // default + }); + + it("should return a default voucher address and an empty collections list if seller does not have any", async function () { + const expectedCollections = new CollectionList([]); + + // Get the collections information + const [defaultVoucherAddress, collections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + const additionalCollections = CollectionList.fromStruct(collections); + expect(defaultVoucherAddress).to.equal(expectedDefaultAddress, "Wrong default voucher address"); + expect(additionalCollections).to.deep.equal(expectedCollections, "Wrong additional collections"); + }); + + it("should return correct collection list", async function () { + const expectedCollections = new CollectionList([]); + + for (let i = 1; i < 4; i++) { + expectedCollectionAddress = calculateContractAddress(accountHandler.address, (i + 1).toString()); + externalId = `Brand${i}`; + contractURI = `https://brand${i}.com`; + + // Create a new collection + await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + + // Add to expected collections + expectedCollections.collections.push(new Collection(expectedCollectionAddress, externalId)); + } + + const [defaultVoucherAddress, collections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + const additionalCollections = CollectionList.fromStruct(collections); + expect(defaultVoucherAddress).to.equal(expectedDefaultAddress, "Wrong default voucher address"); + expect(additionalCollections).to.deep.equal(expectedCollections, "Wrong additional collections"); + }); + + it("should return zero values if seller does not exist ", async function () { + const sellerId = 777; + const expectedCollections = new CollectionList([]); + + // Get the collections information + const [defaultVoucherAddress, collections] = await accountHandler + .connect(rando) + .getSellersCollections(sellerId); + const additionalCollections = CollectionList.fromStruct(collections); + expect(defaultVoucherAddress).to.equal(ethers.constants.AddressZero, "Wrong default voucher address"); + expect(additionalCollections).to.deep.equal(expectedCollections, "Wrong additional collections"); + }); + }); }); }); From 07b630096a6e29f01cf77ced573b745a31a7c2a7 Mon Sep 17 00:00:00 2001 From: zajck Date: Mon, 27 Mar 2023 13:20:57 +0200 Subject: [PATCH 05/12] createOffer tests --- contracts/protocol/bases/OfferBase.sol | 3 +- scripts/config/revert-reasons.js | 1 + test/protocol/OfferHandlerTest.js | 117 ++++++++++++++++++++++ test/protocol/OrchestrationHandlerTest.js | 21 ++++ 4 files changed, 140 insertions(+), 2 deletions(-) diff --git a/contracts/protocol/bases/OfferBase.sol b/contracts/protocol/bases/OfferBase.sol index e99c3907c..516eec1cd 100644 --- a/contracts/protocol/bases/OfferBase.sol +++ b/contracts/protocol/bases/OfferBase.sol @@ -199,8 +199,7 @@ contract OfferBase is ProtocolBase, IBosonOfferEvents { // Collection must exist. Collections with index 0 exist by default. if (_offer.collectionIndex > 0) { require( - lookups.additionalCollections[_offer.sellerId][_offer.collectionIndex].collectionAddress != - address(0), + lookups.additionalCollections[_offer.sellerId].length >= _offer.collectionIndex, NO_SUCH_COLLECTION ); } diff --git a/scripts/config/revert-reasons.js b/scripts/config/revert-reasons.js index ef7a07770..1210373ec 100644 --- a/scripts/config/revert-reasons.js +++ b/scripts/config/revert-reasons.js @@ -50,6 +50,7 @@ exports.RevertReasons = { OFFER_NOT_AVAILABLE: "Offer is not yet available", OFFER_HAS_EXPIRED: "Offer has expired", OFFER_SOLD_OUT: "Offer has sold out", + NO_SUCH_COLLECTION: "No such collection", // Group related NO_SUCH_GROUP: "No such group", diff --git a/test/protocol/OfferHandlerTest.js b/test/protocol/OfferHandlerTest.js index 66426d68a..63ab2dadf 100644 --- a/test/protocol/OfferHandlerTest.js +++ b/test/protocol/OfferHandlerTest.js @@ -107,6 +107,8 @@ describe("IBosonOfferHandler", function () { }); beforeEach(async function () { + accountId.next(true); + // Make accounts available [deployer, pauser, admin, treasury, rando, adminDR, treasuryDR, other, protocolAdmin, protocolTreasury] = await ethers.getSigners(); @@ -235,6 +237,8 @@ describe("IBosonOfferHandler", function () { // All supported methods - single offer context("📋 Offer Handler Methods", async function () { beforeEach(async function () { + accountId.next(true); + // create a seller // Required constructor params id = nextAccountId = "1"; // argument sent to contract for createSeller will be ignored @@ -608,6 +612,69 @@ describe("IBosonOfferHandler", function () { ).to.emit(offerHandler, "OfferCreated"); }); + context("Additional collections", async function () { + let expectedCollectionAddress; + + beforeEach(async function () { + const externalId = "Brand1"; + const contractURI = "https://brand1.com"; + + expectedCollectionAddress = calculateContractAddress(accountHandler.address, "2"); + + // Create a new collection, testing for the event + await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + + // Update collection index + offer.collectionIndex = "1"; + }); + + it("Create offer", async function () { + // Create an offer, testing for the event + await expect( + offerHandler.connect(assistant).createOffer(offer, offerDates, offerDurations, disputeResolver.id, agentId) + ) + .to.emit(offerHandler, "OfferCreated") + .withArgs( + nextOfferId, + offer.sellerId, + offer.toStruct(), + offerDatesStruct, + offerDurationsStruct, + disputeResolutionTermsStruct, + offerFeesStruct, + agentId, + assistant.address + ); + }); + + it("Reserve range", async function () { + offer.quantityAvailable = "200"; + await offerHandler + .connect(assistant) + .createOffer(offer, offerDates, offerDurations, disputeResolver.id, agentId); + + // id of the current offer and increment nextOfferId + id = nextOfferId++; + + // expected address of the first clone + const bosonVoucher = await ethers.getContractAt("BosonVoucher", expectedCollectionAddress); + + const length = 100; + const firstTokenId = 1; + const lastTokenId = firstTokenId + length - 1; + const range = new Range(firstTokenId.toString(), length.toString(), "0", "0", assistant.address); + + // Reserve a range, testing for the event + const tx = await offerHandler.connect(assistant).reserveRange(id, length, assistant.address); + + await expect(tx) + .to.emit(offerHandler, "RangeReserved") + .withArgs(id, offer.sellerId, firstTokenId, lastTokenId, assistant.address, assistant.address); + + await expect(tx).to.emit(bosonVoucher, "RangeReserved").withArgs(id, range.toStruct()); + }); + }); + context("💔 Revert Reasons", async function () { it("The offers region of protocol is paused", async function () { // Pause the offers region of the protocol @@ -851,6 +918,29 @@ describe("IBosonOfferHandler", function () { offerHandler.connect(assistant).createOffer(offer, offerDates, offerDurations, disputeResolver.id, agentId) ).to.revertedWith(RevertReasons.DR_UNSUPPORTED_FEE); }); + + it("Collection does not exist", async function () { + // Set non existent collection index + offer.collectionIndex = "1"; + + // Attempt to Create an offer, expecting revert + await expect( + offerHandler.connect(assistant).createOffer(offer, offerDates, offerDurations, disputeResolver.id, agentId) + ).to.revertedWith(RevertReasons.NO_SUCH_COLLECTION); + + // Create a new collection + const externalId = "Brand1"; + const contractURI = "https://brand1.com"; + await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + + // Set non existent collection index + offer.collectionIndex = "2"; + + // Attempt to Create an offer, expecting revert + await expect( + offerHandler.connect(assistant).createOffer(offer, offerDates, offerDurations, disputeResolver.id, agentId) + ).to.revertedWith(RevertReasons.NO_SUCH_COLLECTION); + }); }); context("When offer has non zero agent id", async function () { @@ -2572,6 +2662,33 @@ describe("IBosonOfferHandler", function () { .createOfferBatch(offers, offerDatesList, offerDurationsList, disputeResolverIds, agentIds) ).to.revertedWith(RevertReasons.ARRAY_LENGTH_MISMATCH); }); + + it("For some offer, collection does not exist", async function () { + // Set non existent collection index + offers[3].collectionIndex = "1"; + + // Attempt to Create an offer, expecting revert + await expect( + offerHandler + .connect(assistant) + .createOfferBatch(offers, offerDatesList, offerDurationsList, disputeResolverIds, agentIds) + ).to.revertedWith(RevertReasons.NO_SUCH_COLLECTION); + + // Create a new collection + const externalId = "Brand1"; + const contractURI = "https://brand1.com"; + await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + + // Index "1" exists now, but "2" does not + offers[3].collectionIndex = "2"; + + // Attempt to Create an offer, expecting revert + await expect( + offerHandler + .connect(assistant) + .createOfferBatch(offers, offerDatesList, offerDurationsList, disputeResolverIds, agentIds) + ).to.revertedWith(RevertReasons.NO_SUCH_COLLECTION); + }); }); context("When offers have non zero agent ids", async function () { diff --git a/test/protocol/OrchestrationHandlerTest.js b/test/protocol/OrchestrationHandlerTest.js index 6b364d515..52a82a02d 100644 --- a/test/protocol/OrchestrationHandlerTest.js +++ b/test/protocol/OrchestrationHandlerTest.js @@ -2180,6 +2180,27 @@ describe("IBosonOrchestrationHandler", function () { ) ).to.revertedWith(RevertReasons.INVALID_RANGE_LENGTH); }); + + it("Collection does not exist", async function () { + // Set inexistent collection index + offer.collectionIndex = "1"; + + // Attempt to create a seller and an offer, expecting revert + await expect( + orchestrationHandler + .connect(assistant) + .createSellerAndOffer( + seller, + offer, + offerDates, + offerDurations, + disputeResolver.id, + emptyAuthToken, + voucherInitValues, + agentId + ) + ).to.revertedWith(RevertReasons.NO_SUCH_COLLECTION); + }); }); context("When offers have non zero agent ids", async function () { From 14ea68b2d42f583d9885cfd4563c0a7afe186532 Mon Sep 17 00:00:00 2001 From: zajck Date: Tue, 28 Mar 2023 10:38:21 +0200 Subject: [PATCH 06/12] exchange handler tests --- test/protocol/ExchangeHandlerTest.js | 227 ++++++++++++++++++++++++++- test/protocol/SellerHandlerTest.js | 2 - 2 files changed, 222 insertions(+), 7 deletions(-) diff --git a/test/protocol/ExchangeHandlerTest.js b/test/protocol/ExchangeHandlerTest.js index 81814bdc2..2e121fb88 100644 --- a/test/protocol/ExchangeHandlerTest.js +++ b/test/protocol/ExchangeHandlerTest.js @@ -505,7 +505,7 @@ describe("IBosonExchangeHandler", function () { await expect(bosonVoucher.ownerOf("1")).to.revertedWith(RevertReasons.ERC721_NON_EXISTENT); await expect(bosonVoucher.ownerOf("2")).to.revertedWith(RevertReasons.ERC721_NON_EXISTENT); - // boson voucher implemenation should not have any vouchers + // boson voucher implementation should not have any vouchers expect(await voucherImplementation.balanceOf(buyer.address)).to.equal( "0", "Voucher implementation: buyer 1 balance should be 0" @@ -515,7 +515,7 @@ describe("IBosonExchangeHandler", function () { "Voucher implementation: buyer 2 balance should be 0" ); - // boson voucher implemenation should not have vouchers with id 1 and 2 + // boson voucher implementation should not have vouchers with id 1 and 2 await expect(voucherImplementation.ownerOf("1")).to.revertedWith(RevertReasons.ERC721_NON_EXISTENT); await expect(voucherImplementation.ownerOf("2")).to.revertedWith(RevertReasons.ERC721_NON_EXISTENT); }); @@ -718,6 +718,46 @@ describe("IBosonExchangeHandler", function () { ); }); + it("should work on an additional collection", async function () { + const externalId = `Brand1`; + const contractURI = `https://brand1.com`; + + // Create a new collection + await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + + offer.collectionIndex = 1; + offer.id = await offerHandler.getNextOfferId(); + const tokenId = await exchangeHandler.getNextExchangeId(); + + // Create the offer + await offerHandler + .connect(assistant) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId); + + // Commit to offer, creating a new exchange + await exchangeHandler.connect(buyer).commitToOffer(buyer.address, offer.id, { value: price }); + + // expected address of the first clone and first additional collection + const defaultCloneAddress = calculateContractAddress(accountHandler.address, "1"); + const defaultBosonVoucher = await ethers.getContractAt("BosonVoucher", defaultCloneAddress); + const additionalCollectionAddress = calculateContractAddress(accountHandler.address, "2"); + const additionalCollection = await ethers.getContractAt("BosonVoucher", additionalCollectionAddress); + + // buyer should own 1 voucher additional collection and 0 vouchers on the default clone + expect(await defaultBosonVoucher.balanceOf(buyer.address)).to.equal( + "0", + "Default clone: buyer's balance should be 0" + ); + expect(await additionalCollection.balanceOf(buyer.address)).to.equal( + "1", + "Additional collection: buyer's balance should be 1" + ); + + // Make sure that vouchers belong to correct buyers and that exist on the correct clone + await expect(defaultBosonVoucher.ownerOf(tokenId)).to.revertedWith(RevertReasons.ERC721_NON_EXISTENT); + expect(await additionalCollection.ownerOf(tokenId)).to.equal(buyer.address, "Wrong buyer address"); + }); + context("💔 Revert Reasons", async function () { it("The exchanges region of protocol is paused", async function () { // Pause the exchanges region of the protocol @@ -911,14 +951,14 @@ describe("IBosonExchangeHandler", function () { }); it("Should not decrement quantityAvailable", async function () { - // Offer qunantityAvailable should be decremented + // Offer quantityAvailable should be decremented let [, offer] = await offerHandler.connect(rando).getOffer(offerId); const quantityAvailableBefore = offer.quantityAvailable; // Commit to preminted offer await bosonVoucher.connect(assistant).transferFrom(assistant.address, buyer.address, tokenId); - // Offer qunantityAvailable should be decremented + // Offer quantityAvailable should be decremented [, offer] = await offerHandler.connect(rando).getOffer(offerId); assert.equal( offer.quantityAvailable.toString(), @@ -953,6 +993,63 @@ describe("IBosonExchangeHandler", function () { ).to.emit(exchangeHandler, "BuyerCommitted"); }); + it("should work on an additional collection", async function () { + const externalId = `Brand1`; + const contractURI = `https://brand1.com`; + + // Create a new collection + await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + + offer.collectionIndex = 1; + offer.id = await offerHandler.getNextOfferId(); + tokenId = exchangeId = await exchangeHandler.getNextExchangeId(); + exchange.offerId = offer.id.toString(); + exchange.id = exchangeId.toString(); + + // Create the offer + await offerHandler + .connect(assistant) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId); + + // Reserve range + await offerHandler.connect(assistant).reserveRange(offer.id, offer.quantityAvailable, assistant.address); + + // expected address of the additional collection + const voucherCloneAddress = calculateContractAddress(accountHandler.address, "2"); + bosonVoucher = await ethers.getContractAt("BosonVoucher", voucherCloneAddress); + await bosonVoucher.connect(assistant).preMint(offer.id, offer.quantityAvailable); + + // Commit to preminted offer, retrieving the event + tx = await bosonVoucher.connect(assistant).transferFrom(assistant.address, buyer.address, tokenId); + txReceipt = await tx.wait(); + event = getEvent(txReceipt, exchangeHandler, "BuyerCommitted"); + + // Get the block timestamp of the confirmed tx + blockNumber = tx.blockNumber; + block = await ethers.provider.getBlock(blockNumber); + + // Update the committed date in the expected exchange struct with the block timestamp of the tx + voucher.committedDate = block.timestamp.toString(); + + // Update the validUntilDate date in the expected exchange struct + voucher.validUntilDate = calculateVoucherExpiry(block, voucherRedeemableFrom, voucherValid); + + // Examine event + assert.equal(event.exchangeId.toString(), exchangeId, "Exchange id is incorrect"); + assert.equal(event.offerId.toString(), offer.id, "Offer id is incorrect"); + assert.equal(event.buyerId.toString(), buyerId, "Buyer id is incorrect"); + + // Examine the exchange struct + assert.equal( + Exchange.fromStruct(event.exchange).toString(), + exchange.toString(), + "Exchange struct is incorrect" + ); + + // Examine the voucher struct + assert.equal(Voucher.fromStruct(event.voucher).toString(), voucher.toString(), "Voucher struct is incorrect"); + }); + context("💔 Revert Reasons", async function () { it("The exchanges region of protocol is paused", async function () { // Pause the exchanges region of the protocol @@ -1761,6 +1858,35 @@ describe("IBosonExchangeHandler", function () { assert.equal(response, ExchangeState.Revoked, "Exchange state is incorrect"); }); + it("should work on an additional collection", async function () { + const externalId = `Brand1`; + const contractURI = `https://brand1.com`; + + // Create a new collection + await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + + offer.collectionIndex = 1; + offer.id = await offerHandler.getNextOfferId(); + const tokenId = (exchange.id = await exchangeHandler.getNextExchangeId()); + + // Create the offer + await offerHandler + .connect(assistant) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId); + + // Commit to offer, creating a new exchange + await exchangeHandler.connect(buyer).commitToOffer(buyer.address, offer.id, { value: price }); + + // expected address of the first additional collection + const additionalCollectionAddress = calculateContractAddress(accountHandler.address, "2"); + const additionalCollection = await ethers.getContractAt("BosonVoucher", additionalCollectionAddress); + + // Revoke the voucher, expecting event + await expect(exchangeHandler.connect(assistant).revokeVoucher(exchange.id)) + .to.emit(additionalCollection, "Transfer") + .withArgs(buyer.address, ethers.constants.AddressZero, tokenId); + }); + context("💔 Revert Reasons", async function () { it("The exchanges region of protocol is paused", async function () { // Pause the exchanges region of the protocol @@ -1837,6 +1963,35 @@ describe("IBosonExchangeHandler", function () { assert.equal(response, ExchangeState.Canceled, "Exchange state is incorrect"); }); + it("should work on an additional collection", async function () { + const externalId = `Brand1`; + const contractURI = `https://brand1.com`; + + // Create a new collection + await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + + offer.collectionIndex = 1; + offer.id = await offerHandler.getNextOfferId(); + const tokenId = (exchange.id = await exchangeHandler.getNextExchangeId()); + + // Create the offer + await offerHandler + .connect(assistant) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId); + + // Commit to offer, creating a new exchange + await exchangeHandler.connect(buyer).commitToOffer(buyer.address, offer.id, { value: price }); + + // expected address of the first additional collection + const additionalCollectionAddress = calculateContractAddress(accountHandler.address, "2"); + const additionalCollection = await ethers.getContractAt("BosonVoucher", additionalCollectionAddress); + + // Cancel the voucher, expecting event + await expect(exchangeHandler.connect(buyer).cancelVoucher(exchange.id)) + .to.emit(additionalCollection, "Transfer") + .withArgs(buyer.address, ethers.constants.AddressZero, tokenId); + }); + context("💔 Revert Reasons", async function () { it("The exchanges region of protocol is paused", async function () { // Pause the exchanges region of the protocol @@ -2050,6 +2205,38 @@ describe("IBosonExchangeHandler", function () { assert.equal(response, ExchangeState.Redeemed, "Exchange state is incorrect"); }); + it("should work on an additional collection", async function () { + const externalId = `Brand1`; + const contractURI = `https://brand1.com`; + + // Create a new collection + await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + + offer.collectionIndex = 1; + offer.id = await offerHandler.getNextOfferId(); + const tokenId = (exchange.id = await exchangeHandler.getNextExchangeId()); + + // Create the offer + await offerHandler + .connect(assistant) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId); + + // Commit to offer, creating a new exchange + await exchangeHandler.connect(buyer).commitToOffer(buyer.address, offer.id, { value: price }); + + // expected address of the first additional collection + const additionalCollectionAddress = calculateContractAddress(accountHandler.address, "2"); + const additionalCollection = await ethers.getContractAt("BosonVoucher", additionalCollectionAddress); + + // Set time forward to the offer's voucherRedeemableFrom + await setNextBlockTimestamp(Number(voucherRedeemableFrom)); + + // Redeem the voucher, expecting event + await expect(exchangeHandler.connect(buyer).redeemVoucher(exchange.id)) + .to.emit(additionalCollection, "Transfer") + .withArgs(buyer.address, ethers.constants.AddressZero, tokenId); + }); + context("💔 Revert Reasons", async function () { it("The exchanges region of protocol is paused", async function () { // Pause the exchanges region of the protocol @@ -3351,6 +3538,36 @@ describe("IBosonExchangeHandler", function () { ).to.not.emit(exchangeHandler, "VoucherTransferred"); }); + it("should work with additional collections", async function () { + const externalId = `Brand1`; + const contractURI = `https://brand1.com`; + + // Create a new collection + await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + + offer.collectionIndex = 1; + offer.id = await offerHandler.getNextOfferId(); + const tokenId = (exchange.id = await exchangeHandler.getNextExchangeId()); + bosonVoucherCloneAddress = calculateContractAddress(exchangeHandler.address, "2"); + bosonVoucherClone = await ethers.getContractAt("IBosonVoucher", bosonVoucherCloneAddress); + + // Create the offer + await offerHandler + .connect(assistant) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId); + + // Commit to offer, creating a new exchange + await exchangeHandler.connect(buyer).commitToOffer(buyer.address, offer.id, { value: price }); + + // Get the next buyer id + nextAccountId = await accountHandler.connect(rando).getNextAccountId(); + + // Call onVoucherTransferred, expecting event + await expect(bosonVoucherClone.connect(buyer).transferFrom(buyer.address, newOwner.address, tokenId)) + .to.emit(exchangeHandler, "VoucherTransferred") + .withArgs(offer.id, exchange.id, nextAccountId, bosonVoucherClone.address); + }); + context("💔 Revert Reasons", async function () { it("The buyers region of protocol is paused", async function () { // Pause the buyers region of the protocol @@ -3433,7 +3650,7 @@ describe("IBosonExchangeHandler", function () { context("👍 undisputed exchange", async function () { it("should return false if exchange does not exists", async function () { let exchangeId = "100"; - // Invalied exchange id, ask if exchange is finalized + // Invalid exchange id, ask if exchange is finalized [exists, response] = await exchangeHandler.connect(rando).isExchangeFinalized(exchangeId); // It should not be exist diff --git a/test/protocol/SellerHandlerTest.js b/test/protocol/SellerHandlerTest.js index 9659f7d4a..2b96a3b98 100644 --- a/test/protocol/SellerHandlerTest.js +++ b/test/protocol/SellerHandlerTest.js @@ -2892,9 +2892,7 @@ describe("SellerHandler", function () { bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCollectionAddress); await expect(tx).to.emit(bosonVoucher, "ContractURIChanged").withArgs(contractURI); - await expect(tx).to.emit(bosonVoucher, "RoyaltyPercentageChanged").withArgs("0"); - await expect(tx).to.emit(bosonVoucher, "VoucherInitialized").withArgs(seller.id, "0", contractURI); bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress); From 9e07f7debef0bd8a5bad11fb9bb874fcd942c243 Mon Sep 17 00:00:00 2001 From: zajck Date: Tue, 28 Mar 2023 10:50:43 +0200 Subject: [PATCH 07/12] update natspec --- .../interfaces/handlers/IBosonOfferHandler.sol | 2 ++ .../handlers/IBosonOrchestrationHandler.sol | 16 ++++++++++++++++ contracts/protocol/facets/OfferHandlerFacet.sol | 2 ++ .../facets/OrchestrationHandlerFacet1.sol | 16 ++++++++++++++++ 4 files changed, 36 insertions(+) diff --git a/contracts/interfaces/handlers/IBosonOfferHandler.sol b/contracts/interfaces/handlers/IBosonOfferHandler.sol index 889ca5690..d646022b7 100644 --- a/contracts/interfaces/handlers/IBosonOfferHandler.sol +++ b/contracts/interfaces/handlers/IBosonOfferHandler.sol @@ -35,6 +35,7 @@ interface IBosonOfferHandler is IBosonOfferEvents { * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When agent id is non zero: * - If Agent does not exist * - If the sum of agent fee amount and protocol fee amount is greater than the offer fee limit @@ -79,6 +80,7 @@ interface IBosonOfferHandler is IBosonOfferEvents { * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When agent ids are non zero: * - If Agent does not exist * - If the sum of agent fee amount and protocol fee amount is greater than the offer fee limit diff --git a/contracts/interfaces/handlers/IBosonOrchestrationHandler.sol b/contracts/interfaces/handlers/IBosonOrchestrationHandler.sol index e20181cc8..a15f5a801 100644 --- a/contracts/interfaces/handlers/IBosonOrchestrationHandler.sol +++ b/contracts/interfaces/handlers/IBosonOrchestrationHandler.sol @@ -90,6 +90,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When agent id is non zero: * - If Agent does not exist * - If the sum of agent fee amount and protocol fee amount is greater than the offer fee limit @@ -159,6 +160,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When agent id is non zero: * - If Agent does not exist * - If the sum of agent fee amount and protocol fee amount is greater than the offer fee limit @@ -216,6 +218,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When agent id is non zero: * - If Agent does not exist @@ -267,6 +270,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When agent id is non zero: * - If Agent does not exist @@ -321,6 +325,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When adding to the group if: * - Group does not exists * - Caller is not the assistant of the group @@ -375,6 +380,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When adding to the group if: * - Group does not exists * - Caller is not the assistant of the group @@ -433,6 +439,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When creating twin if * - Not approved to transfer the seller's token * - SupplyAvailable is zero @@ -492,6 +499,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When creating twin if * - Not approved to transfer the seller's token * - SupplyAvailable is zero @@ -556,6 +564,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When creating twin if * - Not approved to transfer the seller's token @@ -620,6 +629,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When creating twin if * - Not approved to transfer the seller's token @@ -702,6 +712,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When agent id is non zero: * - If Agent does not exist @@ -777,6 +788,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When agent id is non zero: * - If Agent does not exist @@ -856,6 +868,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When creating twin if * - Not approved to transfer the seller's token * - SupplyAvailable is zero @@ -939,6 +952,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When creating twin if * - Not approved to transfer the seller's token * - SupplyAvailable is zero @@ -1026,6 +1040,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When creating twin if * - Not approved to transfer the seller's token @@ -1114,6 +1129,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When creating twin if * - Not approved to transfer the seller's token diff --git a/contracts/protocol/facets/OfferHandlerFacet.sol b/contracts/protocol/facets/OfferHandlerFacet.sol index 9d5a2fae9..0cf10998c 100644 --- a/contracts/protocol/facets/OfferHandlerFacet.sol +++ b/contracts/protocol/facets/OfferHandlerFacet.sol @@ -43,6 +43,7 @@ contract OfferHandlerFacet is IBosonOfferHandler, OfferBase { * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When agent id is non zero: * - If Agent does not exist * - If the sum of agent fee amount and protocol fee amount is greater than the offer fee limit @@ -89,6 +90,7 @@ contract OfferHandlerFacet is IBosonOfferHandler, OfferBase { * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When agent ids are non zero: * - If Agent does not exist * - If the sum of agent fee amount and protocol fee amount is greater than the offer fee limit diff --git a/contracts/protocol/facets/OrchestrationHandlerFacet1.sol b/contracts/protocol/facets/OrchestrationHandlerFacet1.sol index 2d2096d58..4f9c85320 100644 --- a/contracts/protocol/facets/OrchestrationHandlerFacet1.sol +++ b/contracts/protocol/facets/OrchestrationHandlerFacet1.sol @@ -67,6 +67,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When agent id is non zero: * - If Agent does not exist * - If the sum of agent fee amount and protocol fee amount is greater than the offer fee limit @@ -139,6 +140,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When agent id is non zero: * - If Agent does not exist * - If the sum of agent fee amount and protocol fee amount is greater than the offer fee limit @@ -208,6 +210,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When agent id is non zero: * - If Agent does not exist @@ -273,6 +276,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When agent id is non zero: * - If Agent does not exist @@ -329,6 +333,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When adding to the group if: * - Group does not exists * - Caller is not the assistant of the group @@ -391,6 +396,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When adding to the group if: * - Group does not exists * - Caller is not the assistant of the group @@ -452,6 +458,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When creating twin if * - Not approved to transfer the seller's token * - SupplyAvailable is zero @@ -517,6 +524,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When creating twin if * - Not approved to transfer the seller's token * - SupplyAvailable is zero @@ -586,6 +594,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When creating twin if * - Not approved to transfer the seller's token @@ -657,6 +666,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When creating twin if * - Not approved to transfer the seller's token @@ -750,6 +760,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When agent id is non zero: * - If Agent does not exist @@ -830,6 +841,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When agent id is non zero: * - If Agent does not exist @@ -922,6 +934,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When creating twin if * - Not approved to transfer the seller's token * - SupplyAvailable is zero @@ -1010,6 +1023,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When creating twin if * - Not approved to transfer the seller's token * - SupplyAvailable is zero @@ -1110,6 +1124,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When creating twin if * - Not approved to transfer the seller's token @@ -1211,6 +1226,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When creating twin if * - Not approved to transfer the seller's token From ddc26fe8d563a727eaf2ca64906357f0bd654595 Mon Sep 17 00:00:00 2001 From: zajck Date: Wed, 17 May 2023 12:26:05 +0200 Subject: [PATCH 08/12] FIx failing tests --- test/protocol/ExchangeHandlerTest.js | 18 ++++++++++++------ test/protocol/MetaTransactionsHandlerTest.js | 2 +- test/protocol/OfferHandlerTest.js | 13 ++++++------- test/protocol/clients/BosonVoucherTest.js | 8 ++++---- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/test/protocol/ExchangeHandlerTest.js b/test/protocol/ExchangeHandlerTest.js index b28375c06..302bb2a52 100644 --- a/test/protocol/ExchangeHandlerTest.js +++ b/test/protocol/ExchangeHandlerTest.js @@ -645,7 +645,8 @@ describe("IBosonExchangeHandler", function () { offer.collectionIndex = 1; offer.id = await offerHandler.getNextOfferId(); - const tokenId = await exchangeHandler.getNextExchangeId(); + exchangeId = await exchangeHandler.getNextExchangeId(); + const tokenId = deriveTokenId(offer.id, exchangeId); // Create the offer await offerHandler @@ -921,9 +922,10 @@ describe("IBosonExchangeHandler", function () { offer.collectionIndex = 1; offer.id = await offerHandler.getNextOfferId(); - tokenId = exchangeId = await exchangeHandler.getNextExchangeId(); + exchangeId = await exchangeHandler.getNextExchangeId(); exchange.offerId = offer.id.toString(); exchange.id = exchangeId.toString(); + const tokenId = deriveTokenId(offer.id, exchangeId); // Create the offer await offerHandler @@ -1828,7 +1830,8 @@ describe("IBosonExchangeHandler", function () { offer.collectionIndex = 1; offer.id = await offerHandler.getNextOfferId(); - const tokenId = (exchange.id = await exchangeHandler.getNextExchangeId()); + exchange.id = await exchangeHandler.getNextExchangeId(); + const tokenId = deriveTokenId(offer.id, exchange.id); // Create the offer await offerHandler @@ -1934,7 +1937,8 @@ describe("IBosonExchangeHandler", function () { offer.collectionIndex = 1; offer.id = await offerHandler.getNextOfferId(); - const tokenId = (exchange.id = await exchangeHandler.getNextExchangeId()); + exchange.id = await exchangeHandler.getNextExchangeId(); + const tokenId = deriveTokenId(offer.id, exchange.id); // Create the offer await offerHandler @@ -2176,7 +2180,8 @@ describe("IBosonExchangeHandler", function () { offer.collectionIndex = 1; offer.id = await offerHandler.getNextOfferId(); - const tokenId = (exchange.id = await exchangeHandler.getNextExchangeId()); + exchange.id = await exchangeHandler.getNextExchangeId(); + const tokenId = deriveTokenId(offer.id, exchange.id); // Create the offer await offerHandler @@ -3514,9 +3519,10 @@ describe("IBosonExchangeHandler", function () { offer.collectionIndex = 1; offer.id = await offerHandler.getNextOfferId(); - const tokenId = (exchange.id = await exchangeHandler.getNextExchangeId()); + exchange.id = await exchangeHandler.getNextExchangeId(); bosonVoucherCloneAddress = calculateContractAddress(exchangeHandler.address, "2"); bosonVoucherClone = await ethers.getContractAt("IBosonVoucher", bosonVoucherCloneAddress); + const tokenId = deriveTokenId(offer.id, exchange.id); // Create the offer await offerHandler diff --git a/test/protocol/MetaTransactionsHandlerTest.js b/test/protocol/MetaTransactionsHandlerTest.js index 245f81539..d61437102 100644 --- a/test/protocol/MetaTransactionsHandlerTest.js +++ b/test/protocol/MetaTransactionsHandlerTest.js @@ -3058,7 +3058,7 @@ describe("IBosonMetaTransactionsHandler", function () { message.from = assistant.address; message.contractAddress = offerHandler.address; message.functionName = - "createOffer((uint256,uint256,uint256,uint256,uint256,uint256,address,string,string,bool),(uint256,uint256,uint256,uint256),(uint256,uint256,uint256),uint256,uint256)"; + "createOffer((uint256,uint256,uint256,uint256,uint256,uint256,address,string,string,bool,uint256),(uint256,uint256,uint256,uint256),(uint256,uint256,uint256),uint256,uint256)"; message.functionSignature = functionSignature; }); diff --git a/test/protocol/OfferHandlerTest.js b/test/protocol/OfferHandlerTest.js index 04148ff7b..fc3082a79 100644 --- a/test/protocol/OfferHandlerTest.js +++ b/test/protocol/OfferHandlerTest.js @@ -570,15 +570,14 @@ describe("IBosonOfferHandler", function () { .connect(assistant) .createOffer(offer, offerDates, offerDurations, disputeResolver.id, agentId); - // id of the current offer and increment nextOfferId - id = nextOfferId++; - // expected address of the first clone const bosonVoucher = await ethers.getContractAt("BosonVoucher", expectedCollectionAddress); const length = 100; - const firstTokenId = 1; - const lastTokenId = firstTokenId + length - 1; + const exchangeId = "1"; + const lastExchangeId = ethers.BigNumber.from(exchangeId).add(length - 1); + const firstTokenId = deriveTokenId(nextOfferId, exchangeId); + const range = new Range(firstTokenId.toString(), length.toString(), "0", "0", assistant.address); // Reserve a range, testing for the event @@ -586,9 +585,9 @@ describe("IBosonOfferHandler", function () { await expect(tx) .to.emit(offerHandler, "RangeReserved") - .withArgs(id, offer.sellerId, firstTokenId, lastTokenId, assistant.address, assistant.address); + .withArgs(nextOfferId, offer.sellerId, exchangeId, lastExchangeId, assistant.address, assistant.address); - await expect(tx).to.emit(bosonVoucher, "RangeReserved").withArgs(id, range.toStruct()); + await expect(tx).to.emit(bosonVoucher, "RangeReserved").withArgs(nextOfferId, range.toStruct()); }); }); diff --git a/test/protocol/clients/BosonVoucherTest.js b/test/protocol/clients/BosonVoucherTest.js index 7071c5db7..228aadd8c 100644 --- a/test/protocol/clients/BosonVoucherTest.js +++ b/test/protocol/clients/BosonVoucherTest.js @@ -108,7 +108,7 @@ describe("IBosonVoucher", function () { voucherInitValues = mockVoucherInitValues(); const bosonVoucherInit = await ethers.getContractAt("BosonVoucher", bosonVoucher.address); - await bosonVoucherInit.initializeVoucher(sellerId, assistant.address, voucherInitValues); + await bosonVoucherInit.initializeVoucher(sellerId, "1", assistant.address, voucherInitValues); [foreign20] = await deployMockTokens(["Foreign20", "BosonToken"]); @@ -154,9 +154,9 @@ describe("IBosonVoucher", function () { it("Cannot initialize voucher twice", async function () { const initalizableClone = await ethers.getContractAt("IInitializableVoucherClone", bosonVoucher.address); - await expect(initalizableClone.initializeVoucher(2, assistant.address, voucherInitValues)).to.be.revertedWith( - RevertReasons.INITIALIZABLE_ALREADY_INITIALIZED - ); + await expect( + initalizableClone.initializeVoucher(2, "1", assistant.address, voucherInitValues) + ).to.be.revertedWith(RevertReasons.INITIALIZABLE_ALREADY_INITIALIZED); }); }); From d8dcb4445edb2b5fd235859f9144ad42cf35bddd Mon Sep 17 00:00:00 2001 From: zajck Date: Wed, 17 May 2023 13:15:11 +0200 Subject: [PATCH 09/12] add missing tests --- .../protocol/facets/SellerHandlerFacet.sol | 2 +- test/protocol/SellerHandlerTest.js | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/contracts/protocol/facets/SellerHandlerFacet.sol b/contracts/protocol/facets/SellerHandlerFacet.sol index 8f9359eb5..1fe0948fa 100644 --- a/contracts/protocol/facets/SellerHandlerFacet.sol +++ b/contracts/protocol/facets/SellerHandlerFacet.sol @@ -276,7 +276,7 @@ contract SellerHandlerFacet is SellerBase { // Transfer ownership of voucher contract to new assistant IBosonVoucher(lookups.cloneAddress[_sellerId]).transferOwnership(sender); // default voucher contract Collection[] storage sellersAdditionalCollections = lookups.additionalCollections[_sellerId]; - uint256 collectionCount; + uint256 collectionCount = sellersAdditionalCollections.length; for (i = 0; i < collectionCount; i++) { // Additional collections (if they exist) IBosonVoucher(sellersAdditionalCollections[i].collectionAddress).transferOwnership(sender); diff --git a/test/protocol/SellerHandlerTest.js b/test/protocol/SellerHandlerTest.js index f02114231..8d3f16526 100644 --- a/test/protocol/SellerHandlerTest.js +++ b/test/protocol/SellerHandlerTest.js @@ -2636,6 +2636,56 @@ describe("SellerHandler", function () { ).to.not.emit(accountHandler, "SellerUpdateApplied"); }); + it("Transfers the ownerships of the default boson voucher.", async function () { + const expectedDefaultAddress = calculateContractAddress(accountHandler.address, "1"); // default + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedDefaultAddress); + + // original voucher contract owner + expect(await bosonVoucher.owner()).to.equal(assistant.address); + + seller.assistant = other1.address; + sellerStruct = seller.toStruct(); + + await accountHandler.connect(admin).updateSeller(seller, emptyAuthToken); + await accountHandler.connect(other1).optInToSellerUpdate(seller.id, [SellerUpdateFields.Assistant]); + + // new voucher contract owner + expect(await bosonVoucher.owner()).to.equal(other1.address); + }); + + it("Transfers ownerships of all additional collections", async function () { + const expectedDefaultAddress = calculateContractAddress(accountHandler.address, "1"); // default + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedDefaultAddress); + + const additionalCollections = []; + // create 3 additional collections + for (let i = 0; i < 3; i++) { + const externalId = `Brand${i}`; + const contractURI = `https://brand${i}.com`; + const expectedCollectionAddress = calculateContractAddress(accountHandler.address, i + 2); + await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + additionalCollections.push(await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress)); + } + + // original voucher and collections contract owner + expect(await bosonVoucher.owner()).to.equal(assistant.address); + for (const collection of additionalCollections) { + expect(await collection.owner()).to.equal(assistant.address); + } + + seller.assistant = other1.address; + sellerStruct = seller.toStruct(); + + await accountHandler.connect(admin).updateSeller(seller, emptyAuthToken); + await accountHandler.connect(other1).optInToSellerUpdate(seller.id, [SellerUpdateFields.Assistant]); + + // new voucher and collections contract owner + expect(await bosonVoucher.owner()).to.equal(other1.address); + for (const collection of additionalCollections) { + expect(await collection.owner()).to.equal(other1.address); + } + }); + context("💔 Revert Reasons", async function () { it("There are no pending updates", async function () { seller.clerk = other1.address; From 2218b9eabb7dbf492799cde4a64b91b8aa016f76 Mon Sep 17 00:00:00 2001 From: zajck Date: Tue, 6 Jun 2023 10:09:16 +0200 Subject: [PATCH 10/12] review fixes --- .../handlers/IBosonAccountHandler.sol | 9 ++-- contracts/protocol/bases/ProtocolBase.sol | 10 ++++ contracts/protocol/bases/SellerBase.sol | 2 +- .../protocol/clients/voucher/BosonVoucher.sol | 8 ++-- .../protocol/facets/SellerHandlerFacet.sol | 11 ++--- test/protocol/ExchangeHandlerTest.js | 36 +++++--------- test/protocol/OfferHandlerTest.js | 12 ++--- test/protocol/SellerHandlerTest.js | 47 ++++++++++--------- 8 files changed, 67 insertions(+), 68 deletions(-) diff --git a/contracts/interfaces/handlers/IBosonAccountHandler.sol b/contracts/interfaces/handlers/IBosonAccountHandler.sol index 72ae23d23..c48a9e0ec 100644 --- a/contracts/interfaces/handlers/IBosonAccountHandler.sol +++ b/contracts/interfaces/handlers/IBosonAccountHandler.sol @@ -9,7 +9,7 @@ import { IBosonAccountEvents } from "../events/IBosonAccountEvents.sol"; * * @notice Handles creation, update, retrieval of accounts within the protocol. * - * The ERC-165 identifier for this interface is: 0xf4de1a36 + * The ERC-165 identifier for this interface is: 0x868de65b */ interface IBosonAccountHandler is IBosonAccountEvents { /** @@ -313,9 +313,12 @@ interface IBosonAccountHandler is IBosonAccountEvents { * - Caller is not the seller assistant * * @param _externalId - external collection id - * @param _contractURI - contract URI + * @param _voucherInitValues - the fully populated BosonTypes.VoucherInitValues struct */ - function createNewCollection(string calldata _externalId, string calldata _contractURI) external; + function createNewCollection( + string calldata _externalId, + BosonTypes.VoucherInitValues calldata _voucherInitValues + ) external; /** * @notice Gets the details about a seller. diff --git a/contracts/protocol/bases/ProtocolBase.sol b/contracts/protocol/bases/ProtocolBase.sol index 23c9079ba..36151d378 100644 --- a/contracts/protocol/bases/ProtocolBase.sol +++ b/contracts/protocol/bases/ProtocolBase.sol @@ -702,6 +702,16 @@ abstract contract ProtocolBase is PausableBase, ReentrancyGuardBase { exists = (_exchangeId > 0 && condition.method != EvaluationMethod.None); } + /** + * @notice Fetches a clone address from storage by seller id and collection index + * If the collection index is 0, the clone address is the seller's main collection, + * otherwise it is the clone address of the additional collection at the given index. + * + * @param _lookups - storage slot for protocol lookups + * @param _sellerId - the id of the seller + * @param _collectionIndex - the index of the collection + * @return cloneAddress - the clone address + */ function getCloneAddress( ProtocolLib.ProtocolLookups storage _lookups, uint256 _sellerId, diff --git a/contracts/protocol/bases/SellerBase.sol b/contracts/protocol/bases/SellerBase.sol index d4c973e88..9b2609efd 100644 --- a/contracts/protocol/bases/SellerBase.sol +++ b/contracts/protocol/bases/SellerBase.sol @@ -162,7 +162,7 @@ contract SellerBase is ProtocolBase, IBosonAccountEvents { uint256 _sellerId, uint256 _collectionIndex, address _assistant, - VoucherInitValues memory _voucherInitValues + VoucherInitValues calldata _voucherInitValues ) internal returns (address cloneAddress) { // Pointer to stored addresses ProtocolLib.ProtocolAddresses storage pa = protocolAddresses(); diff --git a/contracts/protocol/clients/voucher/BosonVoucher.sol b/contracts/protocol/clients/voucher/BosonVoucher.sol index 4977a615b..4adb0b844 100644 --- a/contracts/protocol/clients/voucher/BosonVoucher.sol +++ b/contracts/protocol/clients/voucher/BosonVoucher.sol @@ -72,11 +72,9 @@ contract BosonVoucherBase is IBosonVoucher, BeaconClientBase, OwnableUpgradeable address _newOwner, VoucherInitValues calldata voucherInitValues ) public initializer { - string memory sellerId = string( - abi.encodePacked(Strings.toString(_sellerId), "_", Strings.toString(_collectionIndex)) - ); - string memory voucherName = string(abi.encodePacked(VOUCHER_NAME, " ", sellerId)); - string memory voucherSymbol = string(abi.encodePacked(VOUCHER_SYMBOL, "_", sellerId)); + string memory sellerId = string.concat(Strings.toString(_sellerId), "_", Strings.toString(_collectionIndex)); + string memory voucherName = string.concat(VOUCHER_NAME, " ", sellerId); + string memory voucherSymbol = string.concat(VOUCHER_SYMBOL, "_", sellerId); __ERC721_init_unchained(voucherName, voucherSymbol); diff --git a/contracts/protocol/facets/SellerHandlerFacet.sol b/contracts/protocol/facets/SellerHandlerFacet.sol index 68cb19ee2..e34c0c0d8 100644 --- a/contracts/protocol/facets/SellerHandlerFacet.sol +++ b/contracts/protocol/facets/SellerHandlerFacet.sol @@ -367,18 +367,17 @@ contract SellerHandlerFacet is SellerBase { * - Caller is not the seller assistant * * @param _externalId - external collection id - * @param _contractURI - contract URI + * @param _voucherInitValues - the fully populated BosonTypes.VoucherInitValues struct */ - function createNewCollection(string calldata _externalId, string calldata _contractURI) external sellersNotPaused { + function createNewCollection( + string calldata _externalId, + VoucherInitValues calldata _voucherInitValues + ) external sellersNotPaused { address assistant = msgSender(); (bool exists, uint256 sellerId) = getSellerIdByAssistant(assistant); require(exists, NO_SUCH_SELLER); - VoucherInitValues memory _voucherInitValues; - _voucherInitValues.contractURI = _contractURI; - // NB: we don't set any royalties here, since they are managed inside the protocol after BPIP-5 - Collection[] storage sellersAdditionalCollections = protocolLookups().additionalCollections[sellerId]; uint256 collectionIndex = sellersAdditionalCollections.length + 1; // 0 is reserved for the original collection diff --git a/test/protocol/ExchangeHandlerTest.js b/test/protocol/ExchangeHandlerTest.js index 302bb2a52..aace041f0 100644 --- a/test/protocol/ExchangeHandlerTest.js +++ b/test/protocol/ExchangeHandlerTest.js @@ -637,11 +637,9 @@ describe("IBosonExchangeHandler", function () { }); it("should work on an additional collection", async function () { - const externalId = `Brand1`; - const contractURI = `https://brand1.com`; - // Create a new collection - await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + const externalId = `Brand1`; + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); offer.collectionIndex = 1; offer.id = await offerHandler.getNextOfferId(); @@ -914,11 +912,9 @@ describe("IBosonExchangeHandler", function () { }); it("should work on an additional collection", async function () { - const externalId = `Brand1`; - const contractURI = `https://brand1.com`; - // Create a new collection - await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + const externalId = `Brand1`; + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); offer.collectionIndex = 1; offer.id = await offerHandler.getNextOfferId(); @@ -1822,11 +1818,9 @@ describe("IBosonExchangeHandler", function () { }); it("should work on an additional collection", async function () { - const externalId = `Brand1`; - const contractURI = `https://brand1.com`; - // Create a new collection - await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + const externalId = `Brand1`; + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); offer.collectionIndex = 1; offer.id = await offerHandler.getNextOfferId(); @@ -1929,11 +1923,9 @@ describe("IBosonExchangeHandler", function () { }); it("should work on an additional collection", async function () { - const externalId = `Brand1`; - const contractURI = `https://brand1.com`; - // Create a new collection - await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + const externalId = `Brand1`; + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); offer.collectionIndex = 1; offer.id = await offerHandler.getNextOfferId(); @@ -2172,11 +2164,9 @@ describe("IBosonExchangeHandler", function () { }); it("should work on an additional collection", async function () { - const externalId = `Brand1`; - const contractURI = `https://brand1.com`; - // Create a new collection - await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + const externalId = `Brand1`; + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); offer.collectionIndex = 1; offer.id = await offerHandler.getNextOfferId(); @@ -3511,11 +3501,9 @@ describe("IBosonExchangeHandler", function () { }); it("should work with additional collections", async function () { - const externalId = `Brand1`; - const contractURI = `https://brand1.com`; - // Create a new collection - await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + const externalId = `Brand1`; + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); offer.collectionIndex = 1; offer.id = await offerHandler.getNextOfferId(); diff --git a/test/protocol/OfferHandlerTest.js b/test/protocol/OfferHandlerTest.js index fc3082a79..bd5491122 100644 --- a/test/protocol/OfferHandlerTest.js +++ b/test/protocol/OfferHandlerTest.js @@ -534,12 +534,10 @@ describe("IBosonOfferHandler", function () { beforeEach(async function () { const externalId = "Brand1"; - const contractURI = "https://brand1.com"; - expectedCollectionAddress = calculateContractAddress(accountHandler.address, "2"); - // Create a new collection, testing for the event - await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + // Create a new collection + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); // Update collection index offer.collectionIndex = "1"; @@ -857,8 +855,7 @@ describe("IBosonOfferHandler", function () { // Create a new collection const externalId = "Brand1"; - const contractURI = "https://brand1.com"; - await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); // Set non existent collection index offer.collectionIndex = "2"; @@ -2604,8 +2601,7 @@ describe("IBosonOfferHandler", function () { // Create a new collection const externalId = "Brand1"; - const contractURI = "https://brand1.com"; - await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); // Index "1" exists now, but "2" does not offers[3].collectionIndex = "2"; diff --git a/test/protocol/SellerHandlerTest.js b/test/protocol/SellerHandlerTest.js index 8d3f16526..b95924c29 100644 --- a/test/protocol/SellerHandlerTest.js +++ b/test/protocol/SellerHandlerTest.js @@ -2661,9 +2661,9 @@ describe("SellerHandler", function () { // create 3 additional collections for (let i = 0; i < 3; i++) { const externalId = `Brand${i}`; - const contractURI = `https://brand${i}.com`; + voucherInitValues.contractURI = `https://brand${i}.com`; const expectedCollectionAddress = calculateContractAddress(accountHandler.address, i + 2); - await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); additionalCollections.push(await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress)); } @@ -2861,20 +2861,22 @@ describe("SellerHandler", function () { context("👉 createNewCollection()", async function () { let externalId, expectedDefaultAddress, expectedCollectionAddress; + let royaltyPercentage; beforeEach(async function () { // Create a seller await accountHandler.connect(admin).createSeller(seller, emptyAuthToken, voucherInitValues); externalId = "Brand1"; - contractURI = "https://brand1.com"; + voucherInitValues.contractURI = contractURI = "https://brand1.com"; + voucherInitValues.royaltyPercentage = royaltyPercentage = "100"; // 1% expectedDefaultAddress = calculateContractAddress(accountHandler.address, "1"); // default expectedCollectionAddress = calculateContractAddress(accountHandler.address, "2"); }); it("should emit a CollectionCreated event", async function () { // Create a new collection, testing for the event - const tx = await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + const tx = await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); await expect(tx) .to.emit(accountHandler, "CollectionCreated") @@ -2884,8 +2886,10 @@ describe("SellerHandler", function () { bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCollectionAddress); await expect(tx).to.emit(bosonVoucher, "ContractURIChanged").withArgs(contractURI); - await expect(tx).to.emit(bosonVoucher, "RoyaltyPercentageChanged").withArgs("0"); - await expect(tx).to.emit(bosonVoucher, "VoucherInitialized").withArgs(seller.id, "0", contractURI); + await expect(tx).to.emit(bosonVoucher, "RoyaltyPercentageChanged").withArgs(royaltyPercentage); + await expect(tx) + .to.emit(bosonVoucher, "VoucherInitialized") + .withArgs(seller.id, royaltyPercentage, contractURI); bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress); @@ -2896,7 +2900,7 @@ describe("SellerHandler", function () { it("should update state", async function () { // Create a new collection - await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); const expectedCollections = new CollectionList([new Collection(expectedCollectionAddress, externalId)]); @@ -2928,10 +2932,11 @@ describe("SellerHandler", function () { for (let i = 1; i < 4; i++) { expectedCollectionAddress = calculateContractAddress(accountHandler.address, (i + 1).toString()); externalId = `Brand${i}`; - contractURI = `https://brand${i}.com`; + voucherInitValues.contractURI = contractURI = `https://brand${i}.com`; + voucherInitValues.royaltyPercentage = royaltyPercentage = (i * 100).toString(); // 1%, 2%, 3% // Create a new collection, testing for the event - const tx = await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + const tx = await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); await expect(tx) .to.emit(accountHandler, "CollectionCreated") @@ -2941,10 +2946,10 @@ describe("SellerHandler", function () { bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCollectionAddress); await expect(tx).to.emit(bosonVoucher, "ContractURIChanged").withArgs(contractURI); - - await expect(tx).to.emit(bosonVoucher, "RoyaltyPercentageChanged").withArgs("0"); - - await expect(tx).to.emit(bosonVoucher, "VoucherInitialized").withArgs(seller.id, "0", contractURI); + await expect(tx).to.emit(bosonVoucher, "RoyaltyPercentageChanged").withArgs(royaltyPercentage); + await expect(tx) + .to.emit(bosonVoucher, "VoucherInitialized") + .withArgs(seller.id, royaltyPercentage, contractURI); bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress); @@ -2985,16 +2990,16 @@ describe("SellerHandler", function () { await pauseHandler.connect(pauser).pause([PausableRegion.Sellers]); // Attempt to create a new collection expecting revert - await expect(accountHandler.connect(assistant).createNewCollection(externalId, contractURI)).to.revertedWith( - RevertReasons.REGION_PAUSED - ); + await expect( + accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues) + ).to.revertedWith(RevertReasons.REGION_PAUSED); }); it("Caller is not anyone's assistant", async function () { // Attempt to create a new collection - await expect(accountHandler.connect(rando).createNewCollection(externalId, contractURI)).to.revertedWith( - RevertReasons.NO_SUCH_SELLER - ); + await expect( + accountHandler.connect(rando).createNewCollection(externalId, voucherInitValues) + ).to.revertedWith(RevertReasons.NO_SUCH_SELLER); }); }); }); @@ -3027,10 +3032,10 @@ describe("SellerHandler", function () { for (let i = 1; i < 4; i++) { expectedCollectionAddress = calculateContractAddress(accountHandler.address, (i + 1).toString()); externalId = `Brand${i}`; - contractURI = `https://brand${i}.com`; + voucherInitValues.contractURI = `https://brand${i}.com`; // Create a new collection - await accountHandler.connect(assistant).createNewCollection(externalId, contractURI); + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); // Add to expected collections expectedCollections.collections.push(new Collection(expectedCollectionAddress, externalId)); From a7662adfc2f676783001f4222d449e29d88dd642 Mon Sep 17 00:00:00 2001 From: zajck Date: Tue, 6 Jun 2023 16:53:43 +0200 Subject: [PATCH 11/12] minor optimization --- contracts/protocol/facets/SellerHandlerFacet.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/protocol/facets/SellerHandlerFacet.sol b/contracts/protocol/facets/SellerHandlerFacet.sol index e34c0c0d8..c7544a8a8 100644 --- a/contracts/protocol/facets/SellerHandlerFacet.sol +++ b/contracts/protocol/facets/SellerHandlerFacet.sol @@ -383,10 +383,11 @@ contract SellerHandlerFacet is SellerBase { // Create clone and store its address to additionalCollections address voucherCloneAddress = cloneBosonVoucher(sellerId, collectionIndex, assistant, _voucherInitValues); - Collection memory newCollection; + + // Store collection details + Collection storage newCollection = sellersAdditionalCollections.push(); newCollection.collectionAddress = voucherCloneAddress; newCollection.externalId = _externalId; - sellersAdditionalCollections.push(newCollection); emit CollectionCreated(sellerId, collectionIndex, voucherCloneAddress, _externalId, assistant); } From e20839de09029ee45cb03e4ea88925cfacba047f Mon Sep 17 00:00:00 2001 From: zajck Date: Wed, 5 Jul 2023 10:19:09 +0200 Subject: [PATCH 12/12] fix tests --- test/protocol/SellerHandlerTest.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/test/protocol/SellerHandlerTest.js b/test/protocol/SellerHandlerTest.js index 611740463..ca4af59d4 100644 --- a/test/protocol/SellerHandlerTest.js +++ b/test/protocol/SellerHandlerTest.js @@ -2683,8 +2683,8 @@ describe("SellerHandler", function () { externalId = "Brand1"; voucherInitValues.contractURI = contractURI = "https://brand1.com"; voucherInitValues.royaltyPercentage = royaltyPercentage = "100"; // 1% - expectedDefaultAddress = calculateContractAddress(accountHandler.address, "1"); // default - expectedCollectionAddress = calculateContractAddress(accountHandler.address, "2"); + expectedDefaultAddress = calculateContractAddress(await accountHandler.getAddress(), "1"); // default + expectedCollectionAddress = calculateContractAddress(await accountHandler.getAddress(), "2"); }); it("should emit a CollectionCreated event", async function () { @@ -2706,9 +2706,7 @@ describe("SellerHandler", function () { bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress); - await expect(tx) - .to.emit(bosonVoucher, "OwnershipTransferred") - .withArgs(ethers.constants.AddressZero, assistant.address); + await expect(tx).to.emit(bosonVoucher, "OwnershipTransferred").withArgs(ZeroAddress, assistant.address); }); it("should update state", async function () { @@ -2743,7 +2741,7 @@ describe("SellerHandler", function () { const expectedCollections = new CollectionList([]); for (let i = 1; i < 4; i++) { - expectedCollectionAddress = calculateContractAddress(accountHandler.address, (i + 1).toString()); + expectedCollectionAddress = calculateContractAddress(await accountHandler.getAddress(), (i + 1).toString()); externalId = `Brand${i}`; voucherInitValues.contractURI = contractURI = `https://brand${i}.com`; voucherInitValues.royaltyPercentage = royaltyPercentage = (i * 100).toString(); // 1%, 2%, 3% @@ -2766,9 +2764,7 @@ describe("SellerHandler", function () { bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress); - await expect(tx) - .to.emit(bosonVoucher, "OwnershipTransferred") - .withArgs(ethers.constants.AddressZero, assistant.address); + await expect(tx).to.emit(bosonVoucher, "OwnershipTransferred").withArgs(ZeroAddress, assistant.address); // Get the collections information expectedCollections.collections.push(new Collection(expectedCollectionAddress, externalId));