From 9ed49d780f04b2f168001f72d6c09f5fc94818b7 Mon Sep 17 00:00:00 2001 From: Klemen <64400885+zajck@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:48:13 +0100 Subject: [PATCH] [FLB-02M] Potential Gas Bombing Attack Vector (#897) * Make price discovery client * pricediscovery unit tests and fixes * Unit tests pass * support erc721 unsafe transfer * Protocol can move the vouchers not the client * PriceDiscoveryAddress part of protocol addresses * ConfigHandler tests * Initialize price discovery address during upgrade * Price discovery client tests * Prevent wrong tokenId transfer + natspec update * Fix failing unit tests * Remove unused variables and clean up code * Fix failing unit tests * WIP withdraw royalties * Update existing unit tests * Withdraw royalty recipient funds * Fix failing unit tests * Remove wrong comment --- contracts/domain/BosonConstants.sol | 3 - contracts/domain/BosonTypes.sol | 16 +- .../interfaces/events/IBosonAccountEvents.sol | 2 +- .../handlers/IBosonAccountHandler.sol | 6 +- contracts/mock/BuyerContract.sol | 22 +- contracts/protocol/bases/OfferBase.sol | 2 +- contracts/protocol/bases/SellerBase.sol | 16 +- .../protocol/facets/FundsHandlerFacet.sol | 74 +- .../ProtocolInitializationHandlerFacet.sol | 2 +- .../protocol/facets/SellerHandlerFacet.sol | 43 +- .../facets/SequentialCommitHandlerFacet.sol | 1 - contracts/protocol/libs/FundsLib.sol | 51 +- contracts/protocol/libs/ProtocolLib.sol | 10 +- scripts/domain/RoyaltyRecipientInfo.js | 203 +++ test/domain/RoyaltyRecipientInfoTest.js | 175 ++ .../royalty-registry/royalty-registry.js | 8 +- test/protocol/ExchangeHandlerTest.js | 18 +- test/protocol/FundsHandlerTest.js | 1587 +++++++++++------ test/protocol/OfferHandlerTest.js | 59 +- test/protocol/OrchestrationHandlerTest.js | 96 +- .../ProtocolInitializationHandlerTest.js | 20 +- test/protocol/SellerHandlerTest.js | 398 +++-- test/protocol/clients/BosonVoucherTest.js | 10 +- test/util/constants.js | 2 - test/util/utils.js | 14 +- 25 files changed, 1904 insertions(+), 934 deletions(-) create mode 100644 scripts/domain/RoyaltyRecipientInfo.js create mode 100644 test/domain/RoyaltyRecipientInfoTest.js diff --git a/contracts/domain/BosonConstants.sol b/contracts/domain/BosonConstants.sol index 00925fd24..0bf43c7dc 100644 --- a/contracts/domain/BosonConstants.sol +++ b/contracts/domain/BosonConstants.sol @@ -21,9 +21,6 @@ uint256 constant ALL_REGIONS_MASK = (1 << (uint256(type(BosonTypes.PausableRegio uint256 constant NOT_ENTERED = 1; uint256 constant ENTERED = 2; -// Seller related -string constant DEFAULT_ROYALTY_RECIPIENT = "Treasury"; - // Twin handler uint256 constant SINGLE_TWIN_RESERVED_GAS = 160000; uint256 constant MINIMAL_RESIDUAL_GAS = 230000; diff --git a/contracts/domain/BosonTypes.sol b/contracts/domain/BosonTypes.sol index 56e7c97c1..bd23ad4b8 100644 --- a/contracts/domain/BosonTypes.sol +++ b/contracts/domain/BosonTypes.sol @@ -114,6 +114,11 @@ contract BosonTypes { bool active; } + struct RoyaltyRecipient { + uint256 id; + address payable wallet; + } + struct DisputeResolver { uint256 id; uint256 escalationResponsePeriod; @@ -330,17 +335,16 @@ contract BosonTypes { Wrapper // Side is not relevant from the protocol perspective } - struct RoyaltyRecipient { - address wallet; - uint256 minRoyaltyPercentage; - string externalId; - } - struct RoyaltyInfo { address payable[] recipients; uint256[] bps; } + struct RoyaltyRecipientInfo { + address payable wallet; + uint256 minRoyaltyPercentage; + } + struct PremintParameters { uint256 reservedRangeLength; address to; diff --git a/contracts/interfaces/events/IBosonAccountEvents.sol b/contracts/interfaces/events/IBosonAccountEvents.sol index 45d6f608b..348eef2aa 100644 --- a/contracts/interfaces/events/IBosonAccountEvents.sol +++ b/contracts/interfaces/events/IBosonAccountEvents.sol @@ -32,7 +32,7 @@ interface IBosonAccountEvents { ); event RoyaltyRecipientsChanged( uint256 indexed sellerId, - BosonTypes.RoyaltyRecipient[] royaltyRecipients, + BosonTypes.RoyaltyRecipientInfo[] royaltyRecipients, address indexed executedBy ); event BuyerCreated(uint256 indexed buyerId, BosonTypes.Buyer buyer, address indexed executedBy); diff --git a/contracts/interfaces/handlers/IBosonAccountHandler.sol b/contracts/interfaces/handlers/IBosonAccountHandler.sol index ec817b02f..1e5d71dac 100644 --- a/contracts/interfaces/handlers/IBosonAccountHandler.sol +++ b/contracts/interfaces/handlers/IBosonAccountHandler.sol @@ -10,7 +10,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: 0x079a9d3b + * The ERC-165 identifier for this interface is: 0x0757010c */ interface IBosonAccountHandler is IBosonAccountEvents, BosonErrors { /** @@ -165,7 +165,7 @@ interface IBosonAccountHandler is IBosonAccountEvents, BosonErrors { */ function addRoyaltyRecipients( uint256 _sellerId, - BosonTypes.RoyaltyRecipient[] calldata _royaltyRecipients + BosonTypes.RoyaltyRecipientInfo[] calldata _royaltyRecipients ) external; /** @@ -191,7 +191,7 @@ interface IBosonAccountHandler is IBosonAccountEvents, BosonErrors { function updateRoyaltyRecipients( uint256 _sellerId, uint256[] calldata _royaltyRecipientIds, - BosonTypes.RoyaltyRecipient[] calldata _royaltyRecipients + BosonTypes.RoyaltyRecipientInfo[] calldata _royaltyRecipients ) external; /** diff --git a/contracts/mock/BuyerContract.sol b/contracts/mock/BuyerContract.sol index 6cb352112..599055dc3 100644 --- a/contracts/mock/BuyerContract.sol +++ b/contracts/mock/BuyerContract.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.22; import { IERC721Receiver } from "../interfaces/IERC721Receiver.sol"; +import { IERC721 } from "../interfaces/IERC721.sol"; /** * @title BuyerContract @@ -32,9 +33,28 @@ contract BuyerContract is IERC721Receiver { address from, uint256 tokenId, bytes calldata data - ) external returns (bytes4) { + ) external virtual returns (bytes4) { if (failType == FailType.Revert) revert("BuyerContract: revert"); if (failType == FailType.ReturnWrongSelector) return 0x12345678; return IERC721Receiver.onERC721Received.selector; } } + +contract BuyerContractMalicious is BuyerContract { + /** + * @dev Return wrong selector to test revert + */ + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes calldata data + ) external override returns (bytes4) { + (address protocolAddress, address bosonVoucher, uint256 tokenIdA) = abi.decode( + data, + (address, address, uint256) + ); + + IERC721(bosonVoucher).safeTransferFrom(protocolAddress, address(this), tokenIdA); + } +} diff --git a/contracts/protocol/bases/OfferBase.sol b/contracts/protocol/bases/OfferBase.sol index 34df901fa..5549ba3b9 100644 --- a/contracts/protocol/bases/OfferBase.sol +++ b/contracts/protocol/bases/OfferBase.sol @@ -392,7 +392,7 @@ contract OfferBase is ProtocolBase, IBosonOfferEvents { ) internal view { if (_royaltyInfo.recipients.length != _royaltyInfo.bps.length) revert ArrayLengthMismatch(); - RoyaltyRecipient[] storage royaltyRecipients = _lookups.royaltyRecipientsBySeller[_sellerId]; + RoyaltyRecipientInfo[] storage royaltyRecipients = _lookups.royaltyRecipientsBySeller[_sellerId]; uint256 totalRoyalties; for (uint256 i = 0; i < _royaltyInfo.recipients.length; ) { diff --git a/contracts/protocol/bases/SellerBase.sol b/contracts/protocol/bases/SellerBase.sol index 810504cc3..cb43f691f 100644 --- a/contracts/protocol/bases/SellerBase.sol +++ b/contracts/protocol/bases/SellerBase.sol @@ -95,10 +95,9 @@ contract SellerBase is ProtocolBase, IBosonAccountEvents { // Set treasury as the default royalty recipient if (_voucherInitValues.royaltyPercentage > protocolLimits().maxRoyaltyPercentage) revert InvalidRoyaltyPercentage(); - RoyaltyRecipient[] storage royaltyRecipients = lookups.royaltyRecipientsBySeller[sellerId]; - RoyaltyRecipient storage defaultRoyaltyRecipient = royaltyRecipients.push(); + RoyaltyRecipientInfo[] storage royaltyRecipients = lookups.royaltyRecipientsBySeller[sellerId]; + RoyaltyRecipientInfo storage defaultRoyaltyRecipient = royaltyRecipients.push(); // We don't store the defaultRoyaltyRecipient.wallet, since it's always the trasury - // We don't store the defaultRoyaltyRecipient.externalId, since the default recipient is always the treasury defaultRoyaltyRecipient.minRoyaltyPercentage = _voucherInitValues.royaltyPercentage; // Calculate seller salt and check that it is unique @@ -243,14 +242,7 @@ contract SellerBase is ProtocolBase, IBosonAccountEvents { */ function fetchRoyaltyRecipients( uint256 _sellerId - ) internal view returns (RoyaltyRecipient[] memory royaltyRecipients) { - royaltyRecipients = protocolLookups().royaltyRecipientsBySeller[_sellerId]; - - // If the seller did not change the default recipient name, return the default name - // royaltyRecipients[0] exists because the default recipient is always present - if (bytes(royaltyRecipients[0].externalId).length == 0) { - royaltyRecipients[0].externalId = DEFAULT_ROYALTY_RECIPIENT; - } - return royaltyRecipients; + ) internal view returns (RoyaltyRecipientInfo[] memory royaltyRecipients) { + return protocolLookups().royaltyRecipientsBySeller[_sellerId]; } } diff --git a/contracts/protocol/facets/FundsHandlerFacet.sol b/contracts/protocol/facets/FundsHandlerFacet.sol index 87e62f2ab..d765202e3 100644 --- a/contracts/protocol/facets/FundsHandlerFacet.sol +++ b/contracts/protocol/facets/FundsHandlerFacet.sol @@ -75,7 +75,7 @@ contract FundsHandlerFacet is IBosonFundsHandler, ProtocolBase { } /** - * @notice Withdraws the specified funds. Can be called for seller, buyer or agent. + * @notice Withdraws the specified funds. Can be called for seller, buyer, agent or royalty recipient. * * Emits FundsWithdrawn event if successful. * @@ -96,34 +96,7 @@ contract FundsHandlerFacet is IBosonFundsHandler, ProtocolBase { address[] calldata _tokenList, uint256[] calldata _tokenAmounts ) external override fundsNotPaused nonReentrant { - address payable sender = payable(msgSender()); - - // Address that will receive the funds - address payable destinationAddress; - - // First check if the caller is a buyer - (bool exists, uint256 callerId) = getBuyerIdByWallet(sender); - if (exists && callerId == _entityId) { - // Caller is a buyer - destinationAddress = sender; - } else { - // Check if the caller is an assistant - (exists, callerId) = getSellerIdByAssistant(sender); - if (exists && callerId == _entityId) { - // Caller is an assistant. In this case funds are transferred to the treasury address - (, Seller storage seller, ) = fetchSeller(callerId); - destinationAddress = seller.treasury; - } else { - (exists, callerId) = getAgentIdByWallet(sender); - if (exists && callerId == _entityId) { - // Caller is an agent - destinationAddress = sender; - } else { - // In this branch, caller is neither buyer, assistant or agent or does not match the _entityId - revert NotAuthorized(); - } - } - } + address payable destinationAddress = getDestinationAddress(_entityId); withdrawFundsInternal(destinationAddress, _entityId, _tokenList, _tokenAmounts); } @@ -324,4 +297,47 @@ contract FundsHandlerFacet is IBosonFundsHandler, ProtocolBase { } } } + + /** + * @notice For a given entity id, it returns the address, where the funds are withdrawn. + * + * Reverts if: + * - Caller is not associated with the entity id + * + * @param _entityId - id of entity for which funds should be withdrawn + * @return destinationAddress - address where the funds are withdrawn + */ + function getDestinationAddress(uint256 _entityId) internal view returns (address payable destinationAddress) { + address payable sender = payable(msgSender()); + + // First check if the caller is a buyer + (bool exists, uint256 callerId) = getBuyerIdByWallet(sender); + if (exists && callerId == _entityId) { + // Caller is a buyer + return sender; + } + + // Check if the caller is an assistant + (exists, callerId) = getSellerIdByAssistant(sender); + if (exists && callerId == _entityId) { + // Caller is an assistant. In this case funds are transferred to the treasury address + (, Seller storage seller, ) = fetchSeller(callerId); + return seller.treasury; + } + + (exists, callerId) = getAgentIdByWallet(sender); + if (exists && callerId == _entityId) { + // Caller is an agent + return sender; + } + + callerId = protocolLookups().royaltyRecipientIdByWallet[sender]; + if (callerId > 0 && callerId == _entityId) { + // Caller is a royalty recipient + return sender; + } + + // In this branch, caller is neither buyer, assistant or agent or does not match the _entityId + revert NotAuthorized(); + } } diff --git a/contracts/protocol/facets/ProtocolInitializationHandlerFacet.sol b/contracts/protocol/facets/ProtocolInitializationHandlerFacet.sol index 50e47f14a..da324f4ab 100644 --- a/contracts/protocol/facets/ProtocolInitializationHandlerFacet.sol +++ b/contracts/protocol/facets/ProtocolInitializationHandlerFacet.sol @@ -217,7 +217,7 @@ contract ProtocolInitializationHandlerFacet is IBosonProtocolInitializationHandl for (uint256 i = 0; i < _royaltyPercentages.length; i++) { // Populate sellers' Royalty Recipients for (uint256 j = 0; j < _sellerIds[i].length; j++) { - RoyaltyRecipient storage defaultRoyaltyRecipient = lookups + RoyaltyRecipientInfo storage defaultRoyaltyRecipient = lookups .royaltyRecipientsBySeller[_sellerIds[i][j]] .push(); defaultRoyaltyRecipient.minRoyaltyPercentage = _royaltyPercentages[i]; diff --git a/contracts/protocol/facets/SellerHandlerFacet.sol b/contracts/protocol/facets/SellerHandlerFacet.sol index c5264daa4..ecb1b9722 100644 --- a/contracts/protocol/facets/SellerHandlerFacet.sol +++ b/contracts/protocol/facets/SellerHandlerFacet.sol @@ -160,7 +160,7 @@ contract SellerHandlerFacet is SellerBase { uint256 royaltyRecipientId = royaltyRecipientIndexBySellerAndRecipient[_seller.treasury]; if (royaltyRecipientId != 0) { - RoyaltyRecipient[] storage royaltyRecipients = lookups.royaltyRecipientsBySeller[_seller.id]; + RoyaltyRecipientInfo[] storage royaltyRecipients = lookups.royaltyRecipientsBySeller[_seller.id]; // If the new treasury is already a royalty recipient, remove it royaltyRecipientId--; // royaltyRecipientId is 1-based, so we need to decrement it to get the index @@ -474,11 +474,11 @@ contract SellerHandlerFacet is SellerBase { * - some royalty percentage is above the limit * * @param _sellerId - seller id - * @param _royaltyRecipients - list of royalty recipients to add + * @param _royaltyRecipients - list of royalty recipients to add, including minimal royalty percentage */ function addRoyaltyRecipients( uint256 _sellerId, - RoyaltyRecipient[] calldata _royaltyRecipients + RoyaltyRecipientInfo[] calldata _royaltyRecipients ) external sellersNotPaused nonReentrant { // Cache protocol lookups and sender for reference ProtocolLib.ProtocolLookups storage lookups = protocolLookups(); @@ -487,26 +487,27 @@ contract SellerHandlerFacet is SellerBase { (Seller storage seller, address sender) = validateAdminStatus(lookups, _sellerId); address treasury = seller.treasury; - RoyaltyRecipient[] storage royaltyRecipients = lookups.royaltyRecipientsBySeller[_sellerId]; + RoyaltyRecipientInfo[] storage royaltyRecipients = lookups.royaltyRecipientsBySeller[_sellerId]; + uint256 maxRoyaltyPercentage = protocolLimits().maxRoyaltyPercentage; uint256 royaltyRecipientsStorageLength = royaltyRecipients.length + 1; for (uint256 i = 0; i < _royaltyRecipients.length; ) { // Cache storage pointer to avoid multiple lookups mapping(address => uint256) storage royaltyRecipientIndexByRecipient = lookups .royaltyRecipientIndexBySellerAndRecipient[_sellerId]; - // No uniqueness check for externalIds since they are not used in the protocol if ( _royaltyRecipients[i].wallet == treasury || _royaltyRecipients[i].wallet == address(0) || royaltyRecipientIndexByRecipient[_royaltyRecipients[i].wallet] != 0 ) revert RecipientNotUnique(); - if (_royaltyRecipients[i].minRoyaltyPercentage > protocolLimits().maxRoyaltyPercentage) - revert InvalidRoyaltyPercentage(); + if (_royaltyRecipients[i].minRoyaltyPercentage > maxRoyaltyPercentage) revert InvalidRoyaltyPercentage(); royaltyRecipients.push(_royaltyRecipients[i]); royaltyRecipientIndexByRecipient[_royaltyRecipients[i].wallet] = royaltyRecipientsStorageLength + i; + createRoyaltyRecipientAccount(_royaltyRecipients[i].wallet); + unchecked { i++; } @@ -515,6 +516,23 @@ contract SellerHandlerFacet is SellerBase { emit RoyaltyRecipientsChanged(_sellerId, fetchRoyaltyRecipients(_sellerId), sender); } + function createRoyaltyRecipientAccount(address payable _royaltyRecipient) internal { + mapping(address => uint256) storage royaltyRecipientIdByWallet = protocolLookups().royaltyRecipientIdByWallet; + // If account exists, do nothing + if (royaltyRecipientIdByWallet[_royaltyRecipient] > 0) { + return; + } + + uint256 royaltyRecipientId = protocolCounters().nextAccountId++; + + protocolEntities().royaltyRecipients[royaltyRecipientId] = RoyaltyRecipient({ + id: royaltyRecipientId, + wallet: _royaltyRecipient + }); + + royaltyRecipientIdByWallet[_royaltyRecipient] = royaltyRecipientId; + } + /** * @notice Updates seller's royalty recipients. * @@ -538,7 +556,7 @@ contract SellerHandlerFacet is SellerBase { function updateRoyaltyRecipients( uint256 _sellerId, uint256[] calldata _royaltyRecipientIds, - RoyaltyRecipient[] calldata _royaltyRecipients + RoyaltyRecipientInfo[] calldata _royaltyRecipients ) external sellersNotPaused nonReentrant { // Cache protocol lookups and sender for reference ProtocolLib.ProtocolLookups storage lookups = protocolLookups(); @@ -553,8 +571,7 @@ contract SellerHandlerFacet is SellerBase { uint256 royaltyRecipientIdsLength = _royaltyRecipientIds.length; if (royaltyRecipientIdsLength != _royaltyRecipients.length) revert ArrayLengthMismatch(); - RoyaltyRecipient[] storage royaltyRecipients = lookups.royaltyRecipientsBySeller[_sellerId]; - + RoyaltyRecipientInfo[] storage royaltyRecipients = lookups.royaltyRecipientsBySeller[_sellerId]; uint256 royaltyRecipientsLength = royaltyRecipients.length; for (uint256 i = 0; i < royaltyRecipientIdsLength; ) { uint256 royaltyRecipientId = _royaltyRecipientIds[i]; @@ -587,6 +604,8 @@ contract SellerHandlerFacet is SellerBase { royaltyRecipients[royaltyRecipientId] = _royaltyRecipients[i]; + createRoyaltyRecipientAccount(_royaltyRecipients[i].wallet); + unchecked { i++; } @@ -622,7 +641,7 @@ contract SellerHandlerFacet is SellerBase { // Make sure admin is the caller and get the sender's address (, address sender) = validateAdminStatus(lookups, _sellerId); - RoyaltyRecipient[] storage royaltyRecipients = lookups.royaltyRecipientsBySeller[_sellerId]; + RoyaltyRecipientInfo[] storage royaltyRecipients = lookups.royaltyRecipientsBySeller[_sellerId]; // We loop from the end of the array to ensure correct ids are removed // _royaltyRecipients must be sorted in ascending order @@ -852,7 +871,7 @@ contract SellerHandlerFacet is SellerBase { */ function getRoyaltyRecipients( uint256 _sellerId - ) external view returns (RoyaltyRecipient[] memory royaltyRecipients) { + ) external view returns (RoyaltyRecipientInfo[] memory royaltyRecipients) { return fetchRoyaltyRecipients(_sellerId); } diff --git a/contracts/protocol/facets/SequentialCommitHandlerFacet.sol b/contracts/protocol/facets/SequentialCommitHandlerFacet.sol index 532b7134d..915f91efd 100644 --- a/contracts/protocol/facets/SequentialCommitHandlerFacet.sol +++ b/contracts/protocol/facets/SequentialCommitHandlerFacet.sol @@ -137,7 +137,6 @@ contract SequentialCommitHandlerFacet is IBosonSequentialCommitHandler, PriceDis // Get the price, originally paid by the reseller uint256 oldPrice; unchecked { - // Get price paid by current buyer uint256 len = exchangeCosts.length; oldPrice = len == 0 ? offer.price : exchangeCosts[len - 1].price; } diff --git a/contracts/protocol/libs/FundsLib.sol b/contracts/protocol/libs/FundsLib.sol index 5fbd7738f..39801b0ab 100644 --- a/contracts/protocol/libs/FundsLib.sol +++ b/contracts/protocol/libs/FundsLib.sol @@ -288,30 +288,28 @@ library FundsLib { address msgSender = EIP712Lib.msgSender(); uint256 len = exchangeCosts.length; for (uint256 i = 0; i < len; ) { - BosonTypes.ExchangeCosts storage secondaryCommit = exchangeCosts[i]; + // Since all elements of exchangeCosts[i] are used it makes sense to copy them to memory + BosonTypes.ExchangeCosts memory secondaryCommit = exchangeCosts[i]; // amount to be released uint256 currentResellerAmount; // inside the scope to avoid stack too deep error { - uint256 price = secondaryCommit.price; - uint256 protocolFeeAmount = secondaryCommit.protocolFeeAmount; - uint256 royaltyAmount = secondaryCommit.royaltyAmount; - if (effectivePriceMultiplier > 0) { - protocolFee += protocolFeeAmount; + protocolFee += secondaryCommit.protocolFeeAmount; sellerRoyalties += distributeRoyalties( + _exchangeId, _offer, - secondaryCommit.royaltyInfoIndex, - price, - royaltyAmount, + secondaryCommit, effectivePriceMultiplier ); } // secondary price without protocol fee and royalties - uint256 reducedSecondaryPrice = price - protocolFeeAmount - royaltyAmount; + uint256 reducedSecondaryPrice = secondaryCommit.price - + secondaryCommit.protocolFeeAmount - + secondaryCommit.royaltyAmount; // current reseller gets the difference between final payout and the immediate payout they received at the time of secondary sale currentResellerAmount = @@ -322,7 +320,7 @@ library FundsLib { ) / HUNDRED_PERCENT; - resellerBuyPrice = price; + resellerBuyPrice = secondaryCommit.price; } if (currentResellerAmount > 0) { @@ -545,33 +543,40 @@ library FundsLib { * @notice Distributes the royalties to external recipients and seller's treasury. * * @param _offer - storage pointer to the offer - * @param _royaltyInfoIndex - index of the royalty info (reffers to offer.royaltyInfo array) - * @param _price - price in the sequential commit - * @param _escrowedRoyaltyAmount - amount of royalties that were escrowed + * @param _secondaryCommit - information about the secondary commit (royaltyInfoIndex, price, escrowedRoyaltyAmount) * @param _effectivePriceMultiplier - multiplier for the price, depending on the state of the exchange */ function distributeRoyalties( + uint256 _exchangeId, BosonTypes.Offer storage _offer, - uint256 _royaltyInfoIndex, - uint256 _price, - uint256 _escrowedRoyaltyAmount, + BosonTypes.ExchangeCosts memory _secondaryCommit, uint256 _effectivePriceMultiplier ) internal returns (uint256 sellerRoyalties) { + address sender = EIP712Lib.msgSender(); address exchangeToken = _offer.exchangeToken; - BosonTypes.RoyaltyInfo storage _royaltyInfo = _offer.royaltyInfo[_royaltyInfoIndex]; + BosonTypes.RoyaltyInfo storage _royaltyInfo = _offer.royaltyInfo[_secondaryCommit.royaltyInfoIndex]; uint256 len = _royaltyInfo.recipients.length; uint256 totalAmount; - uint256 effectivePrice = (_price * _effectivePriceMultiplier) / HUNDRED_PERCENT; + uint256 effectivePrice = (_secondaryCommit.price * _effectivePriceMultiplier) / HUNDRED_PERCENT; + ProtocolLib.ProtocolLookups storage pl = ProtocolLib.protocolLookups(); for (uint256 i = 0; i < len; ) { address payable recipient = _royaltyInfo.recipients[i]; uint256 amount = (_royaltyInfo.bps[i] * effectivePrice) / HUNDRED_PERCENT; totalAmount += amount; if (recipient == address(0)) { // goes to seller's treasury - sellerRoyalties = amount; + sellerRoyalties += amount; } else { - // try to transfer the funds. Or better make it available to withdraw? - FundsLib.transferFundsFromProtocol(exchangeToken, recipient, amount); + // Make funds available to withdraw + if (amount > 0) { + increaseAvailableFundsAndEmitEvent( + _exchangeId, + pl.royaltyRecipientIdByWallet[recipient], + exchangeToken, + amount, + sender + ); + } } unchecked { @@ -580,6 +585,6 @@ library FundsLib { } // if there is a remainder due to rounding, it goes to the seller's treasury - sellerRoyalties += (_effectivePriceMultiplier * _escrowedRoyaltyAmount) / HUNDRED_PERCENT - totalAmount; + sellerRoyalties += (_effectivePriceMultiplier * _secondaryCommit.royaltyAmount) / HUNDRED_PERCENT - totalAmount; } } diff --git a/contracts/protocol/libs/ProtocolLib.sol b/contracts/protocol/libs/ProtocolLib.sol index c2e25c184..be3d604bc 100644 --- a/contracts/protocol/libs/ProtocolLib.sol +++ b/contracts/protocol/libs/ProtocolLib.sol @@ -116,10 +116,12 @@ library ProtocolLib { mapping(uint256 => BosonTypes.Bundle) bundles; // twin id => twin mapping(uint256 => BosonTypes.Twin) twins; - //entity id => auth token + // entity id => auth token mapping(uint256 => BosonTypes.AuthToken) authTokens; // exchange id => sequential commit info mapping(uint256 => BosonTypes.ExchangeCosts[]) exchangeCosts; + // entity id => royalty recipient account + mapping(uint256 => BosonTypes.RoyaltyRecipient) royaltyRecipients; } // Protocol lookups storage @@ -201,10 +203,12 @@ library ProtocolLib { mapping(uint256 => bytes32) sellerSalt; // seller salt => is used mapping(bytes32 => bool) isUsedSellerSalt; - // seller id => royalty recipients - mapping(uint256 => BosonTypes.RoyaltyRecipient[]) royaltyRecipientsBySeller; + // seller id => royalty recipients info + mapping(uint256 => BosonTypes.RoyaltyRecipientInfo[]) royaltyRecipientsBySeller; // seller id => royalty recipient => index of royalty recipient in royaltyRecipientsBySeller mapping(uint256 => mapping(address => uint256)) royaltyRecipientIndexBySellerAndRecipient; + // royalty recipient wallet address => agentId + mapping(address => uint256) royaltyRecipientIdByWallet; } // Incrementing id counters diff --git a/scripts/domain/RoyaltyRecipientInfo.js b/scripts/domain/RoyaltyRecipientInfo.js new file mode 100644 index 000000000..0cc00e7b4 --- /dev/null +++ b/scripts/domain/RoyaltyRecipientInfo.js @@ -0,0 +1,203 @@ +const { bigNumberIsValid, addressIsValid } = require("../util/validations.js"); + +/** + * Boson Protocol Domain Entity: RoyaltyRecipientInfo + * + * See: {BosonTypes.RoyaltyRecipientInfo} + */ +class RoyaltyRecipientInfo { + /* + struct RoyaltyRecipientInfo { + address wallet; + uint256 minRoyaltyPercentage; + } + */ + + constructor(wallet, minRoyaltyPercentage) { + this.wallet = wallet; + this.minRoyaltyPercentage = minRoyaltyPercentage; + } + + /** + * Get a new RoyaltyRecipientInfo instance from a pojo representation + * @param o + * @returns {RoyaltyRecipientInfo} + */ + static fromObject(o) { + const { wallet, minRoyaltyPercentage } = o; + return new RoyaltyRecipientInfo(wallet, minRoyaltyPercentage); + } + + /** + * Get a new RoyaltyRecipientInfo instance from a returned struct representation + * @param struct + * @returns {*} + */ + static fromStruct(struct) { + let wallet, minRoyaltyPercentage; + + // destructure struct + [wallet, minRoyaltyPercentage] = struct; + + return RoyaltyRecipientInfo.fromObject({ + wallet, + minRoyaltyPercentage: minRoyaltyPercentage.toString(), + }); + } + + /** + * Get a database representation of this RoyaltyRecipientInfo instance + * @returns {object} + */ + toObject() { + return JSON.parse(this.toString()); + } + + /** + * Get a string representation of this RoyaltyRecipientInfo instance + * @returns {string} + */ + toString() { + return JSON.stringify(this); + } + + /** + * Get a struct representation of this RoyaltyRecipientInfo instance + * @returns {string} + */ + toStruct() { + return [this.wallet, this.minRoyaltyPercentage]; + } + + /** + * Clone this RoyaltyRecipientInfo + * @returns {RoyaltyRecipientInfo} + */ + clone() { + return RoyaltyRecipientInfo.fromObject(this.toObject()); + } + + /** + * Is this RoyaltyRecipientInfo instance's wallet field valid? + * Must be a eip55 compliant Ethereum address + * @returns {boolean} + */ + walletIsValid() { + return addressIsValid(this.wallet); + } + + /** + * Is this RoyaltyRecipientInfo instance's minRoyaltyPercentage field valid? + * Must be a string representation of a big number + * @returns {boolean} + */ + minRoyaltyPercentageIsValid() { + return bigNumberIsValid(this.minRoyaltyPercentage); + } + + /** + * Is this RoyaltyRecipientInfo instance valid? + * @returns {boolean} + */ + isValid() { + return this.walletIsValid() && this.minRoyaltyPercentageIsValid(); + } +} + +/** + * Boson Protocol Domain Entity: Collection of RoyaltyRecipientInfo + * + * See: {BosonTypes.RoyaltyRecipientInfo} + */ +class RoyaltyRecipientInfoList { + constructor(royaltyRecipientInfos) { + this.royaltyRecipientInfos = royaltyRecipientInfos; + } + + /** + * Get a new RoyaltyRecipientInfoList instance from a pojo representation + * @param o + * @returns {RoyaltyRecipientInfoList} + */ + static fromObject(o) { + const { royaltyRecipientInfos } = o; + return new RoyaltyRecipientInfoList(royaltyRecipientInfos.map((f) => RoyaltyRecipientInfo.fromObject(f))); + } + + /** + * Get a new RoyaltyRecipientInfoList instance from a returned struct representation + * @param struct + * @returns {*} + */ + static fromStruct(struct) { + return RoyaltyRecipientInfoList.fromObject({ + royaltyRecipientInfos: struct.map((royaltyRecipientInfo) => + RoyaltyRecipientInfo.fromStruct(royaltyRecipientInfo) + ), + }); + } + + /** + * Get a database representation of this RoyaltyRecipientInfoList instance + * @returns {object} + */ + toObject() { + return JSON.parse(this.toString()); + } + + /** + * Get a string representation of this RoyaltyRecipientInfoList instance + * @returns {string} + */ + toString() { + return JSON.stringify(this.royaltyRecipientInfos); + } + + /** + * Get a struct representation of this RoyaltyRecipientInfoList instance + * @returns {string} + */ + toStruct() { + return this.royaltyRecipientInfos.map((f) => f.toStruct()); + } + + /** + * Clone this RoyaltyRecipientInfoList + * @returns {RoyaltyRecipientInfoList} + */ + clone() { + return RoyaltyRecipientInfoList.fromObject(this.toObject()); + } + + /** + * Is this RoyaltyRecipientInfoList instance's royaltyRecipientInfo field valid? + * Must be a list of RoyaltyRecipientInfo instances + * @returns {boolean} + */ + royaltyRecipientInfoIsValid() { + let valid = false; + let { royaltyRecipientInfos } = this; + try { + valid = + Array.isArray(royaltyRecipientInfos) && + royaltyRecipientInfos.reduce( + (previousRoyaltyRecipientInfo, currentRoyaltyRecipientInfo) => + previousRoyaltyRecipientInfo && currentRoyaltyRecipientInfo.isValid(), + true + ); + } catch (e) {} + return valid; + } + + /** + * Is this RoyaltyRecipientInfoList instance valid? + * @returns {boolean} + */ + isValid() { + return this.royaltyRecipientInfoIsValid(); + } +} + +// Export +exports.RoyaltyRecipientInfo = RoyaltyRecipientInfo; +exports.RoyaltyRecipientInfoList = RoyaltyRecipientInfoList; diff --git a/test/domain/RoyaltyRecipientInfoTest.js b/test/domain/RoyaltyRecipientInfoTest.js new file mode 100644 index 000000000..88f33102c --- /dev/null +++ b/test/domain/RoyaltyRecipientInfoTest.js @@ -0,0 +1,175 @@ +const { ethers } = require("hardhat"); +const { expect } = require("chai"); +const { RoyaltyRecipientInfo } = require("../../scripts/domain/RoyaltyRecipientInfo"); + +/** + * Test the RoyaltyRecipientInfo domain entity + */ +describe("RoyaltyRecipientInfo", function () { + // Suite-wide scope + let royaltyRecipientInfo, object, promoted, clone, dehydrated, rehydrated, key, value, struct; + let accounts, wallet, minRoyaltyPercentage; + + beforeEach(async function () { + // Get a list of accounts + accounts = await ethers.getSigners(); + wallet = accounts[0].address; + + // Required constructor params + minRoyaltyPercentage = "2000"; + }); + + context("📋 Constructor", async function () { + it("Should allow creation of valid, fully populated RoyaltyRecipientInfo instance", async function () { + // Create a valid royalty recipients + royaltyRecipientInfo = new RoyaltyRecipientInfo(wallet, minRoyaltyPercentage); + expect(royaltyRecipientInfo.walletIsValid()).is.true; + expect(royaltyRecipientInfo.minRoyaltyPercentageIsValid()).is.true; + }); + }); + + context("📋 Field validations", async function () { + beforeEach(async function () { + // Create a valid royalty recipients, then set fields in tests directly + royaltyRecipientInfo = new RoyaltyRecipientInfo(wallet, minRoyaltyPercentage); + expect(royaltyRecipientInfo.isValid()).is.true; + }); + + it("Always present, wallet must be a string representation of an EIP-55 compliant address", async function () { + // Invalid field value + royaltyRecipientInfo.wallet = "0xASFADF"; + expect(royaltyRecipientInfo.walletIsValid()).is.false; + expect(royaltyRecipientInfo.isValid()).is.false; + + // Invalid field value + royaltyRecipientInfo.wallet = "zedzdeadbaby"; + expect(royaltyRecipientInfo.walletIsValid()).is.false; + expect(royaltyRecipientInfo.isValid()).is.false; + + // Valid field value + royaltyRecipientInfo.wallet = accounts[0].address; + expect(royaltyRecipientInfo.walletIsValid()).is.true; + expect(royaltyRecipientInfo.isValid()).is.true; + + // Valid field value + royaltyRecipientInfo.wallet = "0xec2fd5bd6fc7b576dae82c0b9640969d8de501a2"; + expect(royaltyRecipientInfo.walletIsValid()).is.true; + expect(royaltyRecipientInfo.isValid()).is.true; + }); + + it("Always present, minRoyaltyPercentage must be the string representation of a BigNumber", async function () { + // Invalid field value + royaltyRecipientInfo.minRoyaltyPercentage = "zedzdeadbaby"; + expect(royaltyRecipientInfo.minRoyaltyPercentageIsValid()).is.false; + expect(royaltyRecipientInfo.isValid()).is.false; + + // Invalid field value + royaltyRecipientInfo.minRoyaltyPercentage = new Date(); + expect(royaltyRecipientInfo.minRoyaltyPercentageIsValid()).is.false; + expect(royaltyRecipientInfo.isValid()).is.false; + + // Invalid field value + royaltyRecipientInfo.minRoyaltyPercentage = 12; + expect(royaltyRecipientInfo.minRoyaltyPercentageIsValid()).is.false; + expect(royaltyRecipientInfo.isValid()).is.false; + + // Valid field value + royaltyRecipientInfo.minRoyaltyPercentage = "0"; + expect(royaltyRecipientInfo.minRoyaltyPercentageIsValid()).is.true; + expect(royaltyRecipientInfo.isValid()).is.true; + + // Valid field value + royaltyRecipientInfo.minRoyaltyPercentage = "126"; + expect(royaltyRecipientInfo.minRoyaltyPercentageIsValid()).is.true; + expect(royaltyRecipientInfo.isValid()).is.true; + }); + }); + + context("📋 Utility functions", async function () { + beforeEach(async function () { + // Create a valid royalty recipients, then set fields in tests directly + royaltyRecipientInfo = new RoyaltyRecipientInfo(wallet, minRoyaltyPercentage); + expect(royaltyRecipientInfo.isValid()).is.true; + + // Get plain object + object = { + wallet, + minRoyaltyPercentage, + }; + + // Struct representation + struct = [wallet, minRoyaltyPercentage]; + }); + + context("👉 Static", async function () { + it("RoyaltyRecipientInfo.fromObject() should return a RoyaltyRecipientInfo instance with the same values as the given plain object", async function () { + // Promote to instance + promoted = RoyaltyRecipientInfo.fromObject(object); + + // Is a RoyaltyRecipientInfo instance + expect(promoted instanceof RoyaltyRecipientInfo).is.true; + + // Key values all match + for ([key, value] of Object.entries(royaltyRecipientInfo)) { + expect(JSON.stringify(promoted[key]) === JSON.stringify(value)).is.true; + } + }); + + it("RoyaltyRecipientInfo.fromStruct() should return a RoyaltyRecipientInfo instance from a struct representation", async function () { + // Get struct from instance + royaltyRecipientInfo = RoyaltyRecipientInfo.fromStruct(struct); + + // Ensure it is valid + expect(royaltyRecipientInfo.isValid()).to.be.true; + }); + }); + + context("👉 Instance", async function () { + it("instance.toString() should return a JSON string representation of the RoyaltyRecipientInfo instance", async function () { + dehydrated = royaltyRecipientInfo.toString(); + rehydrated = JSON.parse(dehydrated); + + for ([key, value] of Object.entries(royaltyRecipientInfo)) { + expect(JSON.stringify(rehydrated[key]) === JSON.stringify(value)).is.true; + } + }); + + it("instance.clone() should return another RoyaltyRecipientInfo instance with the same property values", async function () { + // Get plain object + clone = royaltyRecipientInfo.clone(); + + // Is a RoyaltyRecipientInfo instance + expect(clone instanceof RoyaltyRecipientInfo).is.true; + + // Key values all match + for ([key, value] of Object.entries(royaltyRecipientInfo)) { + expect(JSON.stringify(clone[key]) === JSON.stringify(value)).is.true; + } + }); + + it("instance.toObject() should return a plain object representation of the RoyaltyRecipientInfo instance", async function () { + // Get plain object + object = royaltyRecipientInfo.toObject(); + + // Not a RoyaltyRecipientInfo instance + expect(object instanceof RoyaltyRecipientInfo).is.false; + + // Key values all match + for ([key, value] of Object.entries(royaltyRecipientInfo)) { + expect(JSON.stringify(object[key]) === JSON.stringify(value)).is.true; + } + }); + + it("instance.toStruct() should return a struct representation of the RoyaltyRecipientInfo instance", async function () { + // Get struct from royalty recipients + struct = royaltyRecipientInfo.toStruct(); + + // Marshal back to a royalty recipients instance + royaltyRecipientInfo = RoyaltyRecipientInfo.fromStruct(struct); + + // Ensure it marshals back to a valid royalty recipients + expect(royaltyRecipientInfo.isValid()).to.be.true; + }); + }); + }); +}); diff --git a/test/integration/royalty-registry/royalty-registry.js b/test/integration/royalty-registry/royalty-registry.js index f325e314c..ffd6d4251 100644 --- a/test/integration/royalty-registry/royalty-registry.js +++ b/test/integration/royalty-registry/royalty-registry.js @@ -22,7 +22,7 @@ const { const { assert } = require("chai"); const { DisputeResolverFee } = require("../../../scripts/domain/DisputeResolverFee.js"); const { RoyaltyInfo } = require("../../../scripts/domain/RoyaltyInfo.js"); -const { RoyaltyRecipient, RoyaltyRecipientList } = require("../../../scripts/domain/RoyaltyRecipient.js"); +const { RoyaltyRecipientInfo, RoyaltyRecipientInfoList } = require("../../../scripts/domain/RoyaltyRecipientInfo.js"); // Requirements to run this test: // - Royalty registry is a submodule. If you didn't clone repository recursively, run `git submodule update --init --recursive` to get it. @@ -67,9 +67,9 @@ describe("[@skip-on-coverage] Royalty registry integration", function () { await accountHandler.connect(assistant).createSeller(seller, emptyAuthToken, voucherInitValues); // Add royalty recipients - const royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(other1.address, "100", "other1"), - new RoyaltyRecipient(other2.address, "200", "other2"), + const royaltyRecipientList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other1.address, "100"), + new RoyaltyRecipientInfo(other2.address, "200"), ]); await accountHandler.connect(assistant).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); diff --git a/test/protocol/ExchangeHandlerTest.js b/test/protocol/ExchangeHandlerTest.js index 595dbba01..4255220da 100644 --- a/test/protocol/ExchangeHandlerTest.js +++ b/test/protocol/ExchangeHandlerTest.js @@ -34,7 +34,7 @@ const GatingType = require("../../scripts/domain/GatingType"); const { DisputeResolverFee } = require("../../scripts/domain/DisputeResolverFee"); const PausableRegion = require("../../scripts/domain/PausableRegion.js"); const { RoyaltyInfo } = require("../../scripts/domain/RoyaltyInfo"); -const { RoyaltyRecipient, RoyaltyRecipientList } = require("../../scripts/domain/RoyaltyRecipient.js"); +const { RoyaltyRecipientInfo, RoyaltyRecipientInfoList } = require("../../scripts/domain/RoyaltyRecipientInfo.js"); const { getInterfaceIds } = require("../../scripts/config/supported-interfaces.js"); const { RevertReasons } = require("../../scripts/config/revert-reasons.js"); const { deployMockTokens } = require("../../scripts/util/deploy-mock-tokens"); @@ -7935,10 +7935,10 @@ describe("IBosonExchangeHandler", function () { let recipients, bps, totalBps; beforeEach(async function () { // Update the offer, so it has multiple recipients in the protocol - const royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(rando.address, "50", "other"), - new RoyaltyRecipient(newOwner.address, "50", "other2"), - new RoyaltyRecipient(treasuryDR.address, "50", "other3"), + const royaltyRecipientList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(rando.address, "50"), + new RoyaltyRecipientInfo(newOwner.address, "50"), + new RoyaltyRecipientInfo(treasuryDR.address, "50"), ]); await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); @@ -8067,10 +8067,10 @@ describe("IBosonExchangeHandler", function () { beforeEach(async function () { // Update the offer, so it has multiple recipients in the protocol - const royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(rando.address, "50", "other"), - new RoyaltyRecipient(newOwner.address, "50", "other2"), - new RoyaltyRecipient(treasuryDR.address, "50", "other3"), + const royaltyRecipientList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(rando.address, "50"), + new RoyaltyRecipientInfo(newOwner.address, "50"), + new RoyaltyRecipientInfo(treasuryDR.address, "50"), ]); await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); diff --git a/test/protocol/FundsHandlerTest.js b/test/protocol/FundsHandlerTest.js index bd0acec6f..174f84dab 100644 --- a/test/protocol/FundsHandlerTest.js +++ b/test/protocol/FundsHandlerTest.js @@ -8,7 +8,7 @@ const PausableRegion = require("../../scripts/domain/PausableRegion.js"); const PriceDiscovery = require("../../scripts/domain/PriceDiscovery"); const Side = require("../../scripts/domain/Side"); const { RoyaltyInfo } = require("../../scripts/domain/RoyaltyInfo"); -const { RoyaltyRecipient, RoyaltyRecipientList } = require("../../scripts/domain/RoyaltyRecipient.js"); +const { RoyaltyRecipientInfo, RoyaltyRecipientInfoList } = require("../../scripts/domain/RoyaltyRecipientInfo.js"); const { getInterfaceIds } = require("../../scripts/config/supported-interfaces.js"); const { RevertReasons } = require("../../scripts/config/revert-reasons.js"); const { deployMockTokens } = require("../../scripts/util/deploy-mock-tokens"); @@ -81,11 +81,11 @@ describe("IBosonFundsHandler", function () { let sellersAvailableFunds, buyerAvailableFunds, protocolAvailableFunds, - externalRoyaltyRecipientsBalance, + royaltyRecipientsAvailableFunds, expectedSellerAvailableFunds, expectedBuyerAvailableFunds, expectedProtocolAvailableFunds, - expectedExternalRoyaltyRecipientsBalance; + expectedRoyaltyRecipientsAvailableFunds; let tokenListSeller, tokenListBuyer, tokenAmountsSeller, tokenAmountsBuyer, tokenList, tokenAmounts; let tx, txReceipt, txCost, event; let disputeResolverFees, disputeResolver, disputeResolverId; @@ -542,510 +542,739 @@ describe("IBosonFundsHandler", function () { }); context("👉 withdrawFunds()", async function () { - beforeEach(async function () { - // cancel the voucher, so both seller and buyer have something to withdraw - await exchangeHandler.connect(buyer).cancelVoucher(exchangeId); // canceling the voucher in tokens - await exchangeHandler.connect(buyer).cancelVoucher(++exchangeId); // canceling the voucher in the native currency + context("singe exchange", async function () { + beforeEach(async function () { + // cancel the voucher, so both seller and buyer have something to withdraw + await exchangeHandler.connect(buyer).cancelVoucher(exchangeId); // canceling the voucher in tokens + await exchangeHandler.connect(buyer).cancelVoucher(++exchangeId); // canceling the voucher in the native currency - // expected payoffs - they are the same for token and native currency - // buyer: price - buyerCancelPenalty - buyerPayoff = BigInt(offerToken.price) - BigInt(offerToken.buyerCancelPenalty); + // expected payoffs - they are the same for token and native currency + // buyer: price - buyerCancelPenalty + buyerPayoff = BigInt(offerToken.price) - BigInt(offerToken.buyerCancelPenalty); - // seller: sellerDeposit + buyerCancelPenalty - sellerPayoff = BigInt(offerToken.sellerDeposit) + BigInt(offerToken.buyerCancelPenalty); - }); + // seller: sellerDeposit + buyerCancelPenalty + sellerPayoff = BigInt(offerToken.sellerDeposit) + BigInt(offerToken.buyerCancelPenalty); + }); - it("should emit a FundsWithdrawn event", async function () { - // Withdraw funds, testing for the event - // Withdraw tokens - tokenListSeller = [await mockToken.getAddress(), ZeroAddress]; - tokenListBuyer = [ZeroAddress, await mockToken.getAddress()]; + it("should emit a FundsWithdrawn event", async function () { + // Withdraw funds, testing for the event + // Withdraw tokens + tokenListSeller = [await mockToken.getAddress(), ZeroAddress]; + tokenListBuyer = [ZeroAddress, await mockToken.getAddress()]; - // Withdraw amounts - tokenAmountsSeller = [sellerPayoff, (BigInt(sellerPayoff) / 2n).toString()]; - tokenAmountsBuyer = [buyerPayoff, (BigInt(buyerPayoff) / 5n).toString()]; + // Withdraw amounts + tokenAmountsSeller = [sellerPayoff, (BigInt(sellerPayoff) / 2n).toString()]; + tokenAmountsBuyer = [buyerPayoff, (BigInt(buyerPayoff) / 5n).toString()]; - // seller withdrawal - const tx = await fundsHandler - .connect(assistant) - .withdrawFunds(seller.id, tokenListSeller, tokenAmountsSeller); - await expect(tx) - .to.emit(fundsHandler, "FundsWithdrawn") - .withArgs( - seller.id, - await treasury.getAddress(), - await mockToken.getAddress(), - sellerPayoff, - await assistant.getAddress() + // seller withdrawal + const tx = await fundsHandler + .connect(assistant) + .withdrawFunds(seller.id, tokenListSeller, tokenAmountsSeller); + await expect(tx) + .to.emit(fundsHandler, "FundsWithdrawn") + .withArgs( + seller.id, + await treasury.getAddress(), + await mockToken.getAddress(), + sellerPayoff, + await assistant.getAddress() + ); + + await expect(tx) + .to.emit(fundsHandler, "FundsWithdrawn") + + .withArgs( + seller.id, + await treasury.getAddress(), + 0n, + BigInt(sellerPayoff) / 2n, + await assistant.getAddress() + ); + + // buyer withdrawal + const tx2 = await fundsHandler.connect(buyer).withdrawFunds(buyerId, tokenListBuyer, tokenAmountsBuyer); + await expect(tx2) + .to.emit(fundsHandler, "FundsWithdrawn", await buyer.getAddress()) + .withArgs( + buyerId, + await buyer.getAddress(), + await mockToken.getAddress(), + BigInt(buyerPayoff) / 5n, + await buyer.getAddress() + ); + + await expect(tx2) + .to.emit(fundsHandler, "FundsWithdrawn") + .withArgs(buyerId, await buyer.getAddress(), 0n, buyerPayoff, await buyer.getAddress()); + }); + + it("should update state", async function () { + // WITHDRAW ONE TOKEN PARTIALLY + + // Read on chain state + sellersAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(seller.id)); + const treasuryBalanceBefore = await provider.getBalance(await treasury.getAddress()); + + // Chain state should match the expected available funds before the withdrawal + expectedSellerAvailableFunds = new FundsList([ + new Funds(await mockToken.getAddress(), "Foreign20", sellerPayoff), + new Funds(ZeroAddress, "Native currency", sellerPayoff), + ]); + expect(sellersAvailableFunds).to.eql( + expectedSellerAvailableFunds, + "Seller available funds mismatch before withdrawal" ); - await expect(tx) - .to.emit(fundsHandler, "FundsWithdrawn") + // withdraw funds + const withdrawAmount = BigInt(sellerPayoff) - parseUnits("0.1", "ether"); + await fundsHandler.connect(assistant).withdrawFunds(seller.id, [ZeroAddress], [withdrawAmount]); - .withArgs( - seller.id, - await treasury.getAddress(), - 0n, - BigInt(sellerPayoff) / 2n, - await assistant.getAddress() + // Read on chain state + sellersAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(seller.id)); + const treasuryBalanceAfter = await provider.getBalance(await treasury.getAddress()); + + // Chain state should match the expected available funds after the withdrawal + // Native currency available funds are reduced for the withdrawal amount + expectedSellerAvailableFunds.funds[1] = new Funds( + ZeroAddress, + "Native currency", + BigInt(sellerPayoff) - BigInt(withdrawAmount) + ); + expect(sellersAvailableFunds).to.eql( + expectedSellerAvailableFunds, + "Seller available funds mismatch after withdrawal" ); - // buyer withdrawal - const tx2 = await fundsHandler.connect(buyer).withdrawFunds(buyerId, tokenListBuyer, tokenAmountsBuyer); - await expect(tx2) - .to.emit(fundsHandler, "FundsWithdrawn", await buyer.getAddress()) - .withArgs( - buyerId, - await buyer.getAddress(), - await mockToken.getAddress(), - BigInt(buyerPayoff) / 5n, - await buyer.getAddress() + // Native currency balance is increased for the withdrawAmount + expect(treasuryBalanceAfter).to.eql( + treasuryBalanceBefore + withdrawAmount, + "Treasury token balance mismatch" ); - await expect(tx2) - .to.emit(fundsHandler, "FundsWithdrawn") - .withArgs(buyerId, await buyer.getAddress(), 0n, buyerPayoff, await buyer.getAddress()); - }); + // WITHDRAW ONE TOKEN FULLY - it("should update state", async function () { - // WITHDRAW ONE TOKEN PARTIALLY + // Read on chain state + buyerAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(buyerId)); + const buyerBalanceBefore = await mockToken.balanceOf(await buyer.getAddress()); - // Read on chain state - sellersAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(seller.id)); - const treasuryBalanceBefore = await provider.getBalance(await treasury.getAddress()); + // Chain state should match the expected available funds before the withdrawal + expectedBuyerAvailableFunds = new FundsList([ + new Funds(await mockToken.getAddress(), "Foreign20", buyerPayoff), + new Funds(ZeroAddress, "Native currency", buyerPayoff), + ]); + expect(buyerAvailableFunds).to.eql( + expectedBuyerAvailableFunds, + "Buyer available funds mismatch before withdrawal" + ); - // Chain state should match the expected available funds before the withdrawal - expectedSellerAvailableFunds = new FundsList([ - new Funds(await mockToken.getAddress(), "Foreign20", sellerPayoff), - new Funds(ZeroAddress, "Native currency", sellerPayoff), - ]); - expect(sellersAvailableFunds).to.eql( - expectedSellerAvailableFunds, - "Seller available funds mismatch before withdrawal" - ); + // withdraw funds + await fundsHandler.connect(buyer).withdrawFunds(buyerId, [await mockToken.getAddress()], [buyerPayoff]); - // withdraw funds - const withdrawAmount = BigInt(sellerPayoff) - parseUnits("0.1", "ether"); - await fundsHandler.connect(assistant).withdrawFunds(seller.id, [ZeroAddress], [withdrawAmount]); + // Read on chain state + buyerAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(buyerId)); + const buyerBalanceAfter = await mockToken.balanceOf(await buyer.getAddress()); - // Read on chain state - sellersAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(seller.id)); - const treasuryBalanceAfter = await provider.getBalance(await treasury.getAddress()); + // Chain state should match the expected available funds after the withdrawal + // Since all tokens are withdrawn, getAvailableFunds should return 0 for token + expectedBuyerAvailableFunds = new FundsList([new Funds(ZeroAddress, "Native currency", buyerPayoff)]); - // Chain state should match the expected available funds after the withdrawal - // Native currency available funds are reduced for the withdrawal amount - expectedSellerAvailableFunds.funds[1] = new Funds( - ZeroAddress, - "Native currency", - BigInt(sellerPayoff) - BigInt(withdrawAmount) - ); - expect(sellersAvailableFunds).to.eql( - expectedSellerAvailableFunds, - "Seller available funds mismatch after withdrawal" - ); + expect(buyerAvailableFunds).to.eql( + expectedBuyerAvailableFunds, + "Buyer available funds mismatch after withdrawal" + ); + // Token balance is increased for the buyer payoff + expect(buyerBalanceAfter).to.eql(buyerBalanceBefore + buyerPayoff, "Buyer token balance mismatch"); + }); - // Native currency balance is increased for the withdrawAmount - expect(treasuryBalanceAfter).to.eql( - treasuryBalanceBefore + withdrawAmount, - "Treasury token balance mismatch" - ); + it("should allow to withdraw all funds at once", async function () { + // Read on chain state + sellersAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(seller.id)); + const treasuryNativeBalanceBefore = await provider.getBalance(await treasury.getAddress()); + const treasuryTokenBalanceBefore = await mockToken.balanceOf(await treasury.getAddress()); - // WITHDRAW ONE TOKEN FULLY + // Chain state should match the expected available funds before the withdrawal + expectedSellerAvailableFunds = new FundsList([ + new Funds(await mockToken.getAddress(), "Foreign20", sellerPayoff), + new Funds(ZeroAddress, "Native currency", sellerPayoff), + ]); + expect(sellersAvailableFunds).to.eql( + expectedSellerAvailableFunds, + "Seller available funds mismatch before withdrawal" + ); - // Read on chain state - buyerAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(buyerId)); - const buyerBalanceBefore = await mockToken.balanceOf(await buyer.getAddress()); + // withdraw all funds + await fundsHandler.connect(assistant).withdrawFunds(seller.id, [], []); - // Chain state should match the expected available funds before the withdrawal - expectedBuyerAvailableFunds = new FundsList([ - new Funds(await mockToken.getAddress(), "Foreign20", buyerPayoff), - new Funds(ZeroAddress, "Native currency", buyerPayoff), - ]); - expect(buyerAvailableFunds).to.eql( - expectedBuyerAvailableFunds, - "Buyer available funds mismatch before withdrawal" - ); + // Read on chain state + sellersAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(seller.id)); + const treasuryNativeBalanceAfter = await provider.getBalance(await treasury.getAddress()); + const treasuryTokenBalanceAfter = await mockToken.balanceOf(await treasury.getAddress()); - // withdraw funds - await fundsHandler.connect(buyer).withdrawFunds(buyerId, [await mockToken.getAddress()], [buyerPayoff]); + // Chain state should match the expected available funds after the withdrawal + // Funds available should be zero + expectedSellerAvailableFunds = new FundsList([]); - // Read on chain state - buyerAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(buyerId)); - const buyerBalanceAfter = await mockToken.balanceOf(await buyer.getAddress()); + expect(sellersAvailableFunds).to.eql( + expectedSellerAvailableFunds, + "Seller available funds mismatch after withdrawal" + ); + // Native currency balance is increased for the withdrawAmount + expect(treasuryNativeBalanceAfter).to.eql( + treasuryNativeBalanceBefore + sellerPayoff, + "Treasury native currency balance mismatch" + ); + expect(treasuryTokenBalanceAfter).to.eql( + treasuryTokenBalanceBefore + sellerPayoff, + "Treasury token balance mismatch" + ); + }); - // Chain state should match the expected available funds after the withdrawal - // Since all tokens are withdrawn, getAvailableFunds should return 0 for token - expectedBuyerAvailableFunds = new FundsList([new Funds(ZeroAddress, "Native currency", buyerPayoff)]); + it("It's possible to withdraw same token twice if in total enough available funds", async function () { + let reduction = parseUnits("0.1", "ether"); + // Withdraw token + tokenListSeller = [await mockToken.getAddress(), await mockToken.getAddress()]; + tokenAmountsSeller = [BigInt(sellerPayoff) - BigInt(reduction), reduction]; - expect(buyerAvailableFunds).to.eql( - expectedBuyerAvailableFunds, - "Buyer available funds mismatch after withdrawal" - ); - // Token balance is increased for the buyer payoff - expect(buyerBalanceAfter).to.eql(buyerBalanceBefore + buyerPayoff, "Buyer token balance mismatch"); - }); + // seller withdrawal + const tx = await fundsHandler + .connect(assistant) + .withdrawFunds(seller.id, tokenListSeller, tokenAmountsSeller); + await expect(tx) + .to.emit(fundsHandler, "FundsWithdrawn") + .withArgs( + seller.id, + await treasury.getAddress(), + await mockToken.getAddress(), + BigInt(sellerPayoff) - BigInt(reduction), + await assistant.getAddress() + ); - it("should allow to withdraw all funds at once", async function () { - // Read on chain state - sellersAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(seller.id)); - const treasuryNativeBalanceBefore = await provider.getBalance(await treasury.getAddress()); - const treasuryTokenBalanceBefore = await mockToken.balanceOf(await treasury.getAddress()); + await expect(tx) + .to.emit(fundsHandler, "FundsWithdrawn") + .withArgs( + seller.id, + await treasury.getAddress(), + await mockToken.getAddress(), + reduction, + await assistant.getAddress() + ); + }); - // Chain state should match the expected available funds before the withdrawal - expectedSellerAvailableFunds = new FundsList([ - new Funds(await mockToken.getAddress(), "Foreign20", sellerPayoff), - new Funds(ZeroAddress, "Native currency", sellerPayoff), - ]); - expect(sellersAvailableFunds).to.eql( - expectedSellerAvailableFunds, - "Seller available funds mismatch before withdrawal" - ); + context("Agent Withdraws funds", async function () { + beforeEach(async function () { + // Create a valid agent, + agentId = "4"; + agent = mockAgent(await other.getAddress()); + agent.id = agentId; + expect(agent.isValid()).is.true; + + // Create an agent + await accountHandler.connect(rando).createAgent(agent); + + // Mock offer + const { offer, offerDates, offerDurations, disputeResolverId } = await mockOffer(); + agentOffer = offer.clone(); + agentOffer.id = "3"; + exchangeId = "3"; + agentOffer.exchangeToken = await mockToken.getAddress(); + + // Create offer with agent + await offerHandler + .connect(assistant) + .createOffer(agentOffer, offerDates, offerDurations, disputeResolverId, agent.id, offerFeeLimit); - // withdraw all funds - await fundsHandler.connect(assistant).withdrawFunds(seller.id, [], []); + // Set used variables + price = agentOffer.price; + sellerDeposit = agentOffer.sellerDeposit; + voucherRedeemableFrom = offerDates.voucherRedeemableFrom; - // Read on chain state - sellersAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(seller.id)); - const treasuryNativeBalanceAfter = await provider.getBalance(await treasury.getAddress()); - const treasuryTokenBalanceAfter = await mockToken.balanceOf(await treasury.getAddress()); + // top up seller's and buyer's account + await mockToken.mint(await assistant.getAddress(), sellerDeposit); + await mockToken.mint(await buyer.getAddress(), price); - // Chain state should match the expected available funds after the withdrawal - // Funds available should be zero - expectedSellerAvailableFunds = new FundsList([]); + // approve protocol to transfer the tokens + await mockToken.connect(assistant).approve(protocolDiamondAddress, sellerDeposit); + await mockToken.connect(buyer).approve(protocolDiamondAddress, price); - expect(sellersAvailableFunds).to.eql( - expectedSellerAvailableFunds, - "Seller available funds mismatch after withdrawal" - ); - // Native currency balance is increased for the withdrawAmount - expect(treasuryNativeBalanceAfter).to.eql( - treasuryNativeBalanceBefore + sellerPayoff, - "Treasury native currency balance mismatch" - ); - expect(treasuryTokenBalanceAfter).to.eql( - treasuryTokenBalanceBefore + sellerPayoff, - "Treasury token balance mismatch" - ); - }); + // deposit to seller's pool + await fundsHandler + .connect(assistant) + .depositFunds(seller.id, await mockToken.getAddress(), sellerDeposit); - it("It's possible to withdraw same toke twice if in total enough available funds", async function () { - let reduction = parseUnits("0.1", "ether"); - // Withdraw token - tokenListSeller = [await mockToken.getAddress(), await mockToken.getAddress()]; - tokenAmountsSeller = [BigInt(sellerPayoff) - BigInt(reduction), reduction]; + // commit to agent offer + await exchangeHandler.connect(buyer).commitToOffer(await buyer.getAddress(), agentOffer.id); - // seller withdrawal - const tx = await fundsHandler - .connect(assistant) - .withdrawFunds(seller.id, tokenListSeller, tokenAmountsSeller); - await expect(tx) - .to.emit(fundsHandler, "FundsWithdrawn") - .withArgs( - seller.id, - await treasury.getAddress(), - await mockToken.getAddress(), - BigInt(sellerPayoff) - BigInt(reduction), - await assistant.getAddress() - ); + // Set time forward to the offer's voucherRedeemableFrom + await setNextBlockTimestamp(Number(voucherRedeemableFrom)); - await expect(tx) - .to.emit(fundsHandler, "FundsWithdrawn") - .withArgs( - seller.id, - await treasury.getAddress(), - await mockToken.getAddress(), - reduction, - await assistant.getAddress() - ); - }); + // succesfully redeem exchange + await exchangeHandler.connect(buyer).redeemVoucher(exchangeId); + }); - context("Agent Withdraws funds", async function () { - beforeEach(async function () { - // Create a valid agent, - agentId = "4"; - agent = mockAgent(await other.getAddress()); - agent.id = agentId; - expect(agent.isValid()).is.true; - - // Create an agent - await accountHandler.connect(rando).createAgent(agent); - - // Mock offer - const { offer, offerDates, offerDurations, disputeResolverId } = await mockOffer(); - agentOffer = offer.clone(); - agentOffer.id = "3"; - exchangeId = "3"; - agentOffer.exchangeToken = await mockToken.getAddress(); - - // Create offer with agent - await offerHandler - .connect(assistant) - .createOffer(agentOffer, offerDates, offerDurations, disputeResolverId, agent.id, offerFeeLimit); + it("Withdraw when exchange is completed, it emits a FundsWithdrawn event", async function () { + // Complete the exchange + await exchangeHandler.connect(buyer).completeExchange(exchangeId); - // Set used variables - price = agentOffer.price; - sellerDeposit = agentOffer.sellerDeposit; - voucherRedeemableFrom = offerDates.voucherRedeemableFrom; + agentPayoff = applyPercentage(agentOffer.price, agent.feePercentage); - // top up seller's and buyer's account - await mockToken.mint(await assistant.getAddress(), sellerDeposit); - await mockToken.mint(await buyer.getAddress(), price); + // Check the balance BEFORE withdrawFunds() + const feeCollectorNativeBalanceBefore = await mockToken.balanceOf(agent.wallet); - // approve protocol to transfer the tokens - await mockToken.connect(assistant).approve(protocolDiamondAddress, sellerDeposit); - await mockToken.connect(buyer).approve(protocolDiamondAddress, price); + await expect( + fundsHandler.connect(other).withdrawFunds(agentId, [await mockToken.getAddress()], [agentPayoff]) + ) + .to.emit(fundsHandler, "FundsWithdrawn") + .withArgs(agentId, agent.wallet, await mockToken.getAddress(), agentPayoff, agent.wallet); - // deposit to seller's pool - await fundsHandler.connect(assistant).depositFunds(seller.id, await mockToken.getAddress(), sellerDeposit); + // Check the balance AFTER withdrawFunds() + const feeCollectorNativeBalanceAfter = await mockToken.balanceOf(agent.wallet); - // commit to agent offer - await exchangeHandler.connect(buyer).commitToOffer(await buyer.getAddress(), agentOffer.id); + // Expected balance + const expectedFeeCollectorNativeBalanceAfter = + BigInt(feeCollectorNativeBalanceBefore) + BigInt(agentPayoff); - // Set time forward to the offer's voucherRedeemableFrom - await setNextBlockTimestamp(Number(voucherRedeemableFrom)); + // Check agent wallet balance and verify the transfer really happened. + expect(feeCollectorNativeBalanceAfter).to.eql( + expectedFeeCollectorNativeBalanceAfter, + "Agent did not receive their fee" + ); + }); - // succesfully redeem exchange - await exchangeHandler.connect(buyer).redeemVoucher(exchangeId); - }); + it("Withdraw when dispute is retracted, it emits a FundsWithdrawn event", async function () { + // raise the dispute + await disputeHandler.connect(buyer).raiseDispute(exchangeId); - it("Withdraw when exchange is completed, it emits a FundsWithdrawn event", async function () { - // Complete the exchange - await exchangeHandler.connect(buyer).completeExchange(exchangeId); + // retract from the dispute + await disputeHandler.connect(buyer).retractDispute(exchangeId); - agentPayoff = applyPercentage(agentOffer.price, agent.feePercentage); + agentPayoff = ((BigInt(agentOffer.price) * BigInt(agent.feePercentage)) / 10000n).toString(); - // Check the balance BEFORE withdrawFunds() - const feeCollectorNativeBalanceBefore = await mockToken.balanceOf(agent.wallet); + // Check the balance BEFORE withdrawFunds() + const feeCollectorNativeBalanceBefore = await mockToken.balanceOf(agent.wallet); - await expect( - fundsHandler.connect(other).withdrawFunds(agentId, [await mockToken.getAddress()], [agentPayoff]) - ) - .to.emit(fundsHandler, "FundsWithdrawn") - .withArgs(agentId, agent.wallet, await mockToken.getAddress(), agentPayoff, agent.wallet); + await expect( + fundsHandler.connect(other).withdrawFunds(agentId, [await mockToken.getAddress()], [agentPayoff]) + ) + .to.emit(fundsHandler, "FundsWithdrawn") + .withArgs(agentId, agent.wallet, await mockToken.getAddress(), agentPayoff, agent.wallet); - // Check the balance AFTER withdrawFunds() - const feeCollectorNativeBalanceAfter = await mockToken.balanceOf(agent.wallet); + // Check the balance AFTER withdrawFunds() + const feeCollectorNativeBalanceAfter = await mockToken.balanceOf(agent.wallet); - // Expected balance - const expectedFeeCollectorNativeBalanceAfter = - BigInt(feeCollectorNativeBalanceBefore) + BigInt(agentPayoff); + // Expected balance + const expectedFeeCollectorNativeBalanceAfter = + BigInt(feeCollectorNativeBalanceBefore) + BigInt(agentPayoff); - // Check agent wallet balance and verify the transfer really happened. - expect(feeCollectorNativeBalanceAfter).to.eql( - expectedFeeCollectorNativeBalanceAfter, - "Agent did not receive their fee" - ); + // Check agent wallet balance and verify the transfer really happened. + expect(feeCollectorNativeBalanceAfter).to.eql( + expectedFeeCollectorNativeBalanceAfter, + "Agent did not receive their fee" + ); + }); }); - it("Withdraw when dispute is retracted, it emits a FundsWithdrawn event", async function () { - // raise the dispute - await disputeHandler.connect(buyer).raiseDispute(exchangeId); + context("💔 Revert Reasons", async function () { + it("The funds region of protocol is paused", async function () { + // Withdraw tokens + tokenListBuyer = [ZeroAddress, await mockToken.getAddress()]; - // retract from the dispute - await disputeHandler.connect(buyer).retractDispute(exchangeId); + // Withdraw amounts + tokenAmountsBuyer = [BigInt(buyerPayoff), BigInt(buyerPayoff) / 5n]; - agentPayoff = ((BigInt(agentOffer.price) * BigInt(agent.feePercentage)) / 10000n).toString(); + // Pause the funds region of the protocol + await pauseHandler.connect(pauser).pause([PausableRegion.Funds]); - // Check the balance BEFORE withdrawFunds() - const feeCollectorNativeBalanceBefore = await mockToken.balanceOf(agent.wallet); + // Attempt to withdraw funds, expecting revert + await expect( + fundsHandler.connect(buyer).withdrawFunds(buyerId, tokenListBuyer, tokenAmountsBuyer) + ).to.revertedWithCustomError(bosonErrors, RevertReasons.REGION_PAUSED); + }); - await expect( - fundsHandler.connect(other).withdrawFunds(agentId, [await mockToken.getAddress()], [agentPayoff]) - ) - .to.emit(fundsHandler, "FundsWithdrawn") - .withArgs(agentId, agent.wallet, await mockToken.getAddress(), agentPayoff, agent.wallet); + it("Caller is not authorized to withdraw", async function () { + // Attempt to withdraw the buyer funds, expecting revert + await expect(fundsHandler.connect(rando).withdrawFunds(buyerId, [], [])).to.revertedWithCustomError( + bosonErrors, + RevertReasons.NOT_AUTHORIZED + ); - // Check the balance AFTER withdrawFunds() - const feeCollectorNativeBalanceAfter = await mockToken.balanceOf(agent.wallet); + // Attempt to withdraw the seller funds, expecting revert + await expect(fundsHandler.connect(rando).withdrawFunds(seller.id, [], [])).to.revertedWithCustomError( + bosonErrors, + RevertReasons.NOT_AUTHORIZED + ); - // Expected balance - const expectedFeeCollectorNativeBalanceAfter = - BigInt(feeCollectorNativeBalanceBefore) + BigInt(agentPayoff); + // Attempt to withdraw the seller funds as treasury, expecting revert + await expect(fundsHandler.connect(treasury).withdrawFunds(seller.id, [], [])).to.revertedWithCustomError( + bosonErrors, + RevertReasons.NOT_AUTHORIZED + ); + }); - // Check agent wallet balance and verify the transfer really happened. - expect(feeCollectorNativeBalanceAfter).to.eql( - expectedFeeCollectorNativeBalanceAfter, - "Agent did not receive their fee" - ); - }); - }); + it("Token list address does not match token amount address", async function () { + // Withdraw token + tokenList = [await mockToken.getAddress(), ZeroAddress]; + tokenAmounts = [sellerPayoff]; - context("💔 Revert Reasons", async function () { - it("The funds region of protocol is paused", async function () { - // Withdraw tokens - tokenListBuyer = [ZeroAddress, await mockToken.getAddress()]; + // Attempt to withdraw the funds, expecting revert + await expect( + fundsHandler.connect(assistant).withdrawFunds(seller.id, tokenList, tokenAmounts) + ).to.revertedWithCustomError(bosonErrors, RevertReasons.TOKEN_AMOUNT_MISMATCH); + }); - // Withdraw amounts - tokenAmountsBuyer = [BigInt(buyerPayoff), BigInt(buyerPayoff) / 5n]; + it("Caller tries to withdraw more than they have in the available funds", async function () { + // Withdraw token + tokenList = [await mockToken.getAddress()]; + tokenAmounts = [BigInt(sellerPayoff) * 2n]; - // Pause the funds region of the protocol - await pauseHandler.connect(pauser).pause([PausableRegion.Funds]); + // Attempt to withdraw the funds, expecting revert + await expect( + fundsHandler.connect(assistant).withdrawFunds(seller.id, tokenList, tokenAmounts) + ).to.revertedWithCustomError(bosonErrors, RevertReasons.INSUFFICIENT_AVAILABLE_FUNDS); + }); - // Attempt to withdraw funds, expecting revert - await expect( - fundsHandler.connect(buyer).withdrawFunds(buyerId, tokenListBuyer, tokenAmountsBuyer) - ).to.revertedWithCustomError(bosonErrors, RevertReasons.REGION_PAUSED); - }); + it("Caller tries to withdraw the same token twice", async function () { + // Withdraw token + tokenList = [await mockToken.getAddress(), await mockToken.getAddress()]; + tokenAmounts = [sellerPayoff, sellerPayoff]; - it("Caller is not authorized to withdraw", async function () { - // Attempt to withdraw the buyer funds, expecting revert - await expect(fundsHandler.connect(rando).withdrawFunds(buyerId, [], [])).to.revertedWithCustomError( - bosonErrors, - RevertReasons.NOT_AUTHORIZED - ); + // Attempt to withdraw the funds, expecting revert + await expect( + fundsHandler.connect(assistant).withdrawFunds(seller.id, tokenList, tokenAmounts) + ).to.revertedWithCustomError(bosonErrors, RevertReasons.INSUFFICIENT_AVAILABLE_FUNDS); + }); - // Attempt to withdraw the seller funds, expecting revert - await expect(fundsHandler.connect(rando).withdrawFunds(seller.id, [], [])).to.revertedWithCustomError( - bosonErrors, - RevertReasons.NOT_AUTHORIZED - ); + it("Nothing to withdraw", async function () { + // Withdraw token + tokenList = [await mockToken.getAddress()]; + tokenAmounts = ["0"]; - // Attempt to withdraw the seller funds as treasury, expecting revert - await expect(fundsHandler.connect(treasury).withdrawFunds(seller.id, [], [])).to.revertedWithCustomError( - bosonErrors, - RevertReasons.NOT_AUTHORIZED - ); - }); + await expect( + fundsHandler.connect(assistant).withdrawFunds(seller.id, tokenList, tokenAmounts) + ).to.revertedWithCustomError(bosonErrors, RevertReasons.NOTHING_TO_WITHDRAW); - it("Token list address does not match token amount address", async function () { - // Withdraw token - tokenList = [await mockToken.getAddress(), ZeroAddress]; - tokenAmounts = [sellerPayoff]; + // first withdraw everything + await fundsHandler.connect(assistant).withdrawFunds(seller.id, [], []); - // Attempt to withdraw the funds, expecting revert - await expect( - fundsHandler.connect(assistant).withdrawFunds(seller.id, tokenList, tokenAmounts) - ).to.revertedWithCustomError(bosonErrors, RevertReasons.TOKEN_AMOUNT_MISMATCH); - }); + // Attempt to withdraw the funds, expecting revert + await expect(fundsHandler.connect(assistant).withdrawFunds(seller.id, [], [])).to.revertedWithCustomError( + bosonErrors, + RevertReasons.NOTHING_TO_WITHDRAW + ); + }); - it("Caller tries to withdraw more than they have in the available funds", async function () { - // Withdraw token - tokenList = [await mockToken.getAddress()]; - tokenAmounts = [BigInt(sellerPayoff) * 2n]; + it("Transfer of funds failed - revert in fallback", async function () { + // deploy a contract that cannot receive funds + const [fallbackErrorContract] = await deployMockTokens(["FallbackError"]); + + // commit to offer on behalf of some contract + tx = await exchangeHandler + .connect(buyer) + .commitToOffer(await fallbackErrorContract.getAddress(), offerNative.id, { value: price }); + txReceipt = await tx.wait(); + event = getEvent(txReceipt, exchangeHandler, "BuyerCommitted"); + exchangeId = event.exchangeId; + const fallbackContractBuyerId = event.buyerId; + + // revoke the voucher so the contract gets credited some funds + await exchangeHandler.connect(assistant).revokeVoucher(exchangeId); + + // we call a fallbackContract which calls fundsHandler.withdraw, which should revert + await expect( + fallbackErrorContract.withdrawFunds( + await fundsHandler.getAddress(), + fallbackContractBuyerId, + [ZeroAddress], + [offerNative.price] + ) + ).to.revertedWithCustomError(bosonErrors, RevertReasons.TOKEN_TRANSFER_FAILED); + }); - // Attempt to withdraw the funds, expecting revert - await expect( - fundsHandler.connect(assistant).withdrawFunds(seller.id, tokenList, tokenAmounts) - ).to.revertedWithCustomError(bosonErrors, RevertReasons.INSUFFICIENT_AVAILABLE_FUNDS); - }); + it("Transfer of funds failed - no payable fallback or receive", async function () { + // deploy a contract that cannot receive funds + const [fallbackErrorContract] = await deployMockTokens(["WithoutFallbackError"]); + + // commit to offer on behalf of some contract + tx = await exchangeHandler + .connect(buyer) + .commitToOffer(await fallbackErrorContract.getAddress(), offerNative.id, { value: price }); + txReceipt = await tx.wait(); + event = getEvent(txReceipt, exchangeHandler, "BuyerCommitted"); + exchangeId = event.exchangeId; + const fallbackContractBuyerId = event.buyerId; + + // revoke the voucher so the contract gets credited some funds + await exchangeHandler.connect(assistant).revokeVoucher(exchangeId); + + // we call a fallbackContract which calls fundsHandler.withdraw, which should revert + await expect( + fallbackErrorContract.withdrawFunds( + await fundsHandler.getAddress(), + fallbackContractBuyerId, + [ZeroAddress], + [offerNative.price] + ) + ).to.revertedWithCustomError(bosonErrors, RevertReasons.TOKEN_TRANSFER_FAILED); + }); - it("Caller tries to withdraw the same token twice", async function () { - // Withdraw token - tokenList = [await mockToken.getAddress(), await mockToken.getAddress()]; - tokenAmounts = [sellerPayoff, sellerPayoff]; + it("Transfer of funds failed - ERC20 token does not exist anymore", async function () { + // destruct mockToken + await mockToken.destruct(); - // Attempt to withdraw the funds, expecting revert - await expect( - fundsHandler.connect(assistant).withdrawFunds(seller.id, tokenList, tokenAmounts) - ).to.revertedWithCustomError(bosonErrors, RevertReasons.INSUFFICIENT_AVAILABLE_FUNDS); + await expect(fundsHandler.connect(assistant).withdrawFunds(seller.id, [], [])).to.revertedWith( + RevertReasons.EOA_FUNCTION_CALL_SAFE_ERC20 + ); + }); + + it("Transfer of funds failed - revert durin ERC20 transfer", async function () { + // pause mockToken + await mockToken.pause(); + + await expect(fundsHandler.connect(assistant).withdrawFunds(seller.id, [], [])).to.revertedWith( + RevertReasons.ERC20_PAUSED + ); + }); + + it("Transfer of funds failed - ERC20 transfer returns false", async function () { + const [foreign20ReturnFalse] = await deployMockTokens(["Foreign20TransferReturnFalse"]); + + await foreign20ReturnFalse.connect(assistant).mint(await assistant.getAddress(), sellerDeposit); + await foreign20ReturnFalse.connect(assistant).approve(protocolDiamondAddress, sellerDeposit); + + await fundsHandler + .connect(assistant) + .depositFunds(seller.id, await foreign20ReturnFalse.getAddress(), sellerDeposit); + + await expect( + fundsHandler + .connect(assistant) + .withdrawFunds(seller.id, [await foreign20ReturnFalse.getAddress()], [sellerDeposit]) + ).to.revertedWith(RevertReasons.SAFE_ERC20_OPERATION_FAILED); + }); }); + }); - it("Nothing to withdraw", async function () { - // Withdraw token - tokenList = [await mockToken.getAddress()]; - tokenAmounts = ["0"]; + context("sequential commit", async function () { + let royaltyRecipientId, royaltyRecipientId2; + let tokenListRoyaltyRecipient, tokenListRoyaltyRecipient2; + let tokenAmountsRoyaltyRecipient, tokenAmountsRoyaltyRecipient2; + beforeEach(async function () { + // Add royalty recipients + const royaltyRecipientList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other.address, "0"), + new RoyaltyRecipientInfo(other2.address, "0"), + ]); + // Royalty recipients increase the accountIds by 2 in the protocol + royaltyRecipientId = accountId.next().value; + royaltyRecipientId2 = accountId.next().value; + + await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); + const royaltySplit = { + seller: 5000, // 50% + other: 3000, // 30% + other2: 2000, // 20% + }; - await expect( - fundsHandler.connect(assistant).withdrawFunds(seller.id, tokenList, tokenAmounts) - ).to.revertedWithCustomError(bosonErrors, RevertReasons.NOTHING_TO_WITHDRAW); + const royalties = 600; + let newRoyaltyInfo = new RoyaltyInfo( + [ZeroAddress, other.address, other2.address], + [ + applyPercentage(royalties, royaltySplit.seller), + applyPercentage(royalties, royaltySplit.other), + applyPercentage(royalties, royaltySplit.other2), + ] + ); - // first withdraw everything - await fundsHandler.connect(assistant).withdrawFunds(seller.id, [], []); + await offerHandler.connect(assistant).updateOfferRoyaltyRecipients(offerToken.id, newRoyaltyInfo); + + price = (BigInt(offerToken.price) * 11n) / 10n; + const expectedCloneAddress = calculateCloneAddress( + await accountHandler.getAddress(), + beaconProxyAddress, + admin.address + ); + const bosonVoucherClone = await ethers.getContractAt("BosonVoucher", expectedCloneAddress); + const tokenId = deriveTokenId(offerToken.id, exchangeId); + let order = { + seller: buyer.address, + buyer: buyer2.address, + voucherContract: expectedCloneAddress, + tokenId: tokenId, + exchangeToken: offerToken.exchangeToken, + price: BigInt(price), + }; + + const priceDiscoveryData = priceDiscoveryContract.interface.encodeFunctionData("fulfilBuyOrder", [order]); - // Attempt to withdraw the funds, expecting revert - await expect(fundsHandler.connect(assistant).withdrawFunds(seller.id, [], [])).to.revertedWithCustomError( - bosonErrors, - RevertReasons.NOTHING_TO_WITHDRAW + const priceDiscovery = new PriceDiscovery( + order.price, + Side.Ask, + await priceDiscoveryContract.getAddress(), + await priceDiscoveryContract.getAddress(), + priceDiscoveryData ); - }); - it("Transfer of funds failed - revert in fallback", async function () { - // deploy a contract that cannot receive funds - const [fallbackErrorContract] = await deployMockTokens(["FallbackError"]); + let royaltyRecipientPayoff = applyPercentage(price, applyPercentage(royalties, royaltySplit.other)); + let royaltyRecipient2Payoff = applyPercentage(price, applyPercentage(royalties, royaltySplit.other2)); - // commit to offer on behalf of some contract - tx = await exchangeHandler - .connect(buyer) - .commitToOffer(await fallbackErrorContract.getAddress(), offerNative.id, { value: price }); - txReceipt = await tx.wait(); - event = getEvent(txReceipt, exchangeHandler, "BuyerCommitted"); - exchangeId = event.exchangeId; - const fallbackContractBuyerId = event.buyerId; + // voucher owner approves protocol to transfer the tokens + await mockToken.mint(buyer.address, order.price); + await mockToken.connect(buyer).approve(protocolDiamondAddress, order.price); - // revoke the voucher so the contract gets credited some funds - await exchangeHandler.connect(assistant).revokeVoucher(exchangeId); + // Voucher owner approves PriceDiscovery contract to transfer the tokens + await bosonVoucherClone.connect(buyer).setApprovalForAll(await priceDiscoveryContract.getAddress(), true); - // we call a fallbackContract which calls fundsHandler.withdraw, which should revert - await expect( - fallbackErrorContract.withdrawFunds( - await fundsHandler.getAddress(), - fallbackContractBuyerId, - [ZeroAddress], - [offerNative.price] - ) - ).to.revertedWithCustomError(bosonErrors, RevertReasons.TOKEN_TRANSFER_FAILED); - }); + // Buyer approves protocol to transfer the tokens + await mockToken.mint(buyer2.address, order.price); + await mockToken.connect(buyer2).approve(protocolDiamondAddress, order.price); - it("Transfer of funds failed - no payable fallback or receive", async function () { - // deploy a contract that cannot receive funds - const [fallbackErrorContract] = await deployMockTokens(["WithoutFallbackError"]); + // commit to offer + await sequentialCommitHandler + .connect(buyer2) + .sequentialCommitToOffer(buyer2.address, tokenId, priceDiscovery, { + gasPrice: 0, + }); - // commit to offer on behalf of some contract - tx = await exchangeHandler - .connect(buyer) - .commitToOffer(await fallbackErrorContract.getAddress(), offerNative.id, { value: price }); - txReceipt = await tx.wait(); - event = getEvent(txReceipt, exchangeHandler, "BuyerCommitted"); - exchangeId = event.exchangeId; - const fallbackContractBuyerId = event.buyerId; + // Finalize the exchange + await setNextBlockTimestamp(Number(voucherRedeemableFrom)); + await exchangeHandler.connect(buyer2).redeemVoucher(exchangeId); + await exchangeHandler.connect(buyer2).completeExchange(exchangeId); - // revoke the voucher so the contract gets credited some funds - await exchangeHandler.connect(assistant).revokeVoucher(exchangeId); + // Withdraw tokens + tokenListRoyaltyRecipient = [await mockToken.getAddress()]; + tokenListRoyaltyRecipient2 = [await mockToken.getAddress()]; + + // Withdraw amounts + tokenAmountsRoyaltyRecipient = [royaltyRecipientPayoff]; + tokenAmountsRoyaltyRecipient2 = [royaltyRecipient2Payoff]; + }); - // we call a fallbackContract which calls fundsHandler.withdraw, which should revert + it("should emit a FundsWithdrawn event", async function () { + // Withdraw funds, testing for the event + // First royalty recipient withdrawal await expect( - fallbackErrorContract.withdrawFunds( - await fundsHandler.getAddress(), - fallbackContractBuyerId, - [ZeroAddress], - [offerNative.price] - ) - ).to.revertedWithCustomError(bosonErrors, RevertReasons.TOKEN_TRANSFER_FAILED); + fundsHandler + .connect(other) + .withdrawFunds(royaltyRecipientId, tokenListRoyaltyRecipient, tokenAmountsRoyaltyRecipient) + ) + .to.emit(fundsHandler, "FundsWithdrawn") + .withArgs( + royaltyRecipientId, + other.address, + await mockToken.getAddress(), + tokenAmountsRoyaltyRecipient[0], + other.address + ); + + // Second royalty recipient withdrawal + await expect( + fundsHandler + .connect(other2) + .withdrawFunds(royaltyRecipientId2, tokenListRoyaltyRecipient2, tokenAmountsRoyaltyRecipient2) + ) + .to.emit(fundsHandler, "FundsWithdrawn") + .withArgs( + royaltyRecipientId2, + other2.address, + await mockToken.getAddress(), + tokenAmountsRoyaltyRecipient2[0], + other2.address + ); }); - it("Transfer of funds failed - ERC20 token does not exist anymore", async function () { - // destruct mockToken - await mockToken.destruct(); + it("should update state", async function () { + // WITHDRAW ONE TOKEN PARTIALLY + let royaltyRecipientPayoff = tokenAmountsRoyaltyRecipient[0]; - await expect(fundsHandler.connect(assistant).withdrawFunds(seller.id, [], [])).to.revertedWith( - RevertReasons.EOA_FUNCTION_CALL_SAFE_ERC20 + // Read on chain state + let royaltyRecipientAvailableFunds = FundsList.fromStruct( + await fundsHandler.getAllAvailableFunds(royaltyRecipientId) ); - }); + const royaltyRecipientBalanceBefore = await mockToken.balanceOf(other.address); - it("Transfer of funds failed - revert durin ERC20 transfer", async function () { - // pause mockToken - await mockToken.pause(); + // Chain state should match the expected available funds before the withdrawal + let expectedRoyaltyRecipientAvailableFunds = new FundsList([ + new Funds(await mockToken.getAddress(), "Foreign20", tokenAmountsRoyaltyRecipient[0]), + ]); + expect(royaltyRecipientAvailableFunds).to.eql( + expectedRoyaltyRecipientAvailableFunds, + "Royalty recipient available funds mismatch before withdrawal" + ); - await expect(fundsHandler.connect(assistant).withdrawFunds(seller.id, [], [])).to.revertedWith( - RevertReasons.ERC20_PAUSED + // withdraw funds + const withdrawAmount = BigInt(royaltyRecipientPayoff) / 2n; + await fundsHandler + .connect(other) + .withdrawFunds(royaltyRecipientId, tokenListRoyaltyRecipient, [withdrawAmount]); + + // Read on chain state + royaltyRecipientAvailableFunds = FundsList.fromStruct( + await fundsHandler.getAllAvailableFunds(royaltyRecipientId) + ); + const royaltyRecipientBalanceAfter = await mockToken.balanceOf(other.address); + + // Chain state should match the expected available funds after the withdrawal + // Token available funds are reduced for the withdrawal amount + expectedRoyaltyRecipientAvailableFunds.funds[0] = new Funds( + await mockToken.getAddress(), + "Foreign20", + BigInt(royaltyRecipientPayoff) - BigInt(withdrawAmount) + ); + expect(royaltyRecipientAvailableFunds).to.eql( + expectedRoyaltyRecipientAvailableFunds, + "Royalty recipient available funds mismatch after withdrawal" ); - }); - it("Transfer of funds failed - ERC20 transfer returns false", async function () { - const [foreign20ReturnFalse] = await deployMockTokens(["Foreign20TransferReturnFalse"]); + // Token balance is increased for the withdrawAmount + expect(royaltyRecipientBalanceAfter).to.eql( + royaltyRecipientBalanceBefore + withdrawAmount, + "Royalty recipient token balance mismatch" + ); + + // WITHDRAW ONE TOKEN FULLY + + // Read on chain state + let royaltyRecipient2Payoff = tokenAmountsRoyaltyRecipient2[0]; - await foreign20ReturnFalse.connect(assistant).mint(await assistant.getAddress(), sellerDeposit); - await foreign20ReturnFalse.connect(assistant).approve(protocolDiamondAddress, sellerDeposit); + // Read on chain state + let royaltyRecipient2AvailableFunds = FundsList.fromStruct( + await fundsHandler.getAllAvailableFunds(royaltyRecipientId2) + ); + const royaltyRecipient2BalanceBefore = await mockToken.balanceOf(other2.address); + + // Chain state should match the expected available funds before the withdrawal + let expectedRoyaltyRecipient2AvailableFunds = new FundsList([ + new Funds(await mockToken.getAddress(), "Foreign20", tokenAmountsRoyaltyRecipient2[0]), + ]); + expect(royaltyRecipient2AvailableFunds).to.eql( + expectedRoyaltyRecipient2AvailableFunds, + "Royalty recipient available funds mismatch before withdrawal" + ); + // withdraw funds + const withdrawAmount2 = BigInt(royaltyRecipient2Payoff); await fundsHandler - .connect(assistant) - .depositFunds(seller.id, await foreign20ReturnFalse.getAddress(), sellerDeposit); + .connect(other2) + .withdrawFunds(royaltyRecipientId2, tokenListRoyaltyRecipient2, [withdrawAmount2]); - await expect( - fundsHandler - .connect(assistant) - .withdrawFunds(seller.id, [await foreign20ReturnFalse.getAddress()], [sellerDeposit]) - ).to.revertedWith(RevertReasons.SAFE_ERC20_OPERATION_FAILED); + // Read on chain state + royaltyRecipient2AvailableFunds = FundsList.fromStruct( + await fundsHandler.getAllAvailableFunds(royaltyRecipientId2) + ); + const royaltyRecipient2BalanceAfter = await mockToken.balanceOf(other2.address); + + // Chain state should match the expected available funds after the withdrawal + // Fund list should be empty + expectedRoyaltyRecipient2AvailableFunds = new FundsList([]); + expect(royaltyRecipient2AvailableFunds).to.eql( + expectedRoyaltyRecipient2AvailableFunds, + "Royalty recipient available funds mismatch after withdrawal" + ); + + // Token balance is increased for the withdrawAmount + expect(royaltyRecipient2BalanceAfter).to.eql( + royaltyRecipient2BalanceBefore + withdrawAmount2, + "Royalty recipient token balance mismatch" + ); }); }); }); @@ -4575,12 +4804,15 @@ describe("IBosonFundsHandler", function () { let mockTokenAddress; context(`Direction: ${direction}`, async function () { + const keyToId = { other: 4, other2: 5 }; + fees.forEach((fee) => { context(`protocol fee: ${fee.protocol / 100}%; royalties: ${fee.royalties / 100}%`, async function () { let voucherOwner, previousPrice; let payoutInformation; let totalRoyalties, totalProtocolFee, totalRoyaltiesSplit; - let royaltySplit; + let royaltySplit, royaltyRecipientsPayoffs; + let royaltiesPerExchange; beforeEach(async function () { payoutInformation = []; @@ -4592,11 +4824,15 @@ describe("IBosonFundsHandler", function () { ); bosonVoucherClone = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); - // Add external royalty recipients - const royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(other.address, "0", "other"), - new RoyaltyRecipient(other2.address, "0", "other2"), + // Add royalty recipients + const royaltyRecipientList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other.address, "0"), + new RoyaltyRecipientInfo(other2.address, "0"), ]); + // Royalty recipients increase the accountIds by 2 in the protocol + accountId.next(); + accountId.next(); + await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); royaltySplit = { seller: 5000, // 50% @@ -4635,15 +4871,15 @@ describe("IBosonFundsHandler", function () { .connect(assistant) .createOffer(offer, offerDates, offerDurations, disputeResolverId, 0, offerFeeLimit); + // Create buyer with price discovery client address to not mess up ids in tests + await accountHandler.createBuyer(mockBuyer(await bpd.getAddress())); + // ids exchangeId = "1"; agentId = "3"; - buyerId = 5; + buyerId = await accountHandler.getNextAccountId(); protocolId = 0; - // Create buyer with protocol address to not mess up ids in tests - await accountHandler.createBuyer(mockBuyer(await bpd.getAddress())); - // commit to offer await exchangeHandler.connect(buyer).commitToOffer(buyer.address, offer.id); @@ -4655,6 +4891,7 @@ describe("IBosonFundsHandler", function () { other: 0n, other2: 0n, }; + royaltiesPerExchange = []; for (const trade of buyerChains[direction]) { // Prepare calldata for PriceDiscovery contract @@ -4714,8 +4951,11 @@ describe("IBosonFundsHandler", function () { // Update royalties split for (const [key, value] of Object.entries(totalRoyaltiesSplit)) { - totalRoyaltiesSplit[key] = - value + BigInt(applyPercentage(order.price, applyPercentage(fee.royalties, royaltySplit[key]))); + const thisTradeRoyalties = BigInt( + applyPercentage(order.price, applyPercentage(fee.royalties, royaltySplit[key])) + ); + totalRoyaltiesSplit[key] = value + thisTradeRoyalties; + royaltiesPerExchange.push({ id: keyToId[key], payoff: thisTradeRoyalties }); } voucherOwner = trade.buyer; // last buyer is voucherOwner in next iteration @@ -4758,6 +4998,15 @@ describe("IBosonFundsHandler", function () { // protocol: protocolFee protocolPayoff = (totalProtocolFee + BigInt(initialFee)).toString(); + + // royalty recipients: royalties + royaltyRecipientsPayoffs = [ + { + id: keyToId["other"], + payoff: totalRoyaltiesSplit.other, + }, + { id: keyToId["other2"], payoff: totalRoyaltiesSplit.other2 }, + ]; }); it("should emit a FundsReleased event", async function () { @@ -4786,6 +5035,22 @@ describe("IBosonFundsHandler", function () { } } + // royalty recipients + for (const royaltyRecipientPayoff of royaltiesPerExchange) { + if (royaltyRecipientPayoff.payoff != 0n) { + expectedEventCount++; + await expect(tx) + .to.emit(exchangeHandler, "FundsReleased") + .withArgs( + exchangeId, + royaltyRecipientPayoff.id, + offer.exchangeToken, + royaltyRecipientPayoff.payoff, + voucherOwner.address + ); + } + } + // Make sure exact number of FundsReleased events was emitted const eventCount = (await tx.wait()).logs.filter((e) => e.eventName == "FundsReleased").length; expect(eventCount).to.equal(expectedEventCount); @@ -4809,9 +5074,9 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); // Chain state should match the expected available funds expectedSellerAvailableFunds = new FundsList([]); @@ -4819,13 +5084,15 @@ describe("IBosonFundsHandler", function () { expectedProtocolAvailableFunds = new FundsList([]); expectedAgentAvailableFunds = new FundsList([]); expectedResellersAvailableFunds = new Array(resellerPayoffs.length).fill(new FundsList([])); - expectedExternalRoyaltyRecipientsBalance = [0n, 0n]; + expectedRoyaltyRecipientsAvailableFunds = new Array(royaltyRecipientsPayoffs.length).fill( + new FundsList([]) + ); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); // Complete the exchange so the funds are released await exchangeHandler.connect(voucherOwner).completeExchange(exchangeId); @@ -4836,7 +5103,7 @@ describe("IBosonFundsHandler", function () { // resellers: difference between the secondary price and immediate payout // protocol: protocolFee // agent: 0 - // external royalty recipients: royalties + // royalty recipients: royalties expectedSellerAvailableFunds.funds.push(new Funds(mockTokenAddress, "Foreign20", sellerPayoff)); if (protocolPayoff != "0") { expectedProtocolAvailableFunds.funds.push(new Funds(mockTokenAddress, "Foreign20", protocolPayoff)); @@ -4844,7 +5111,9 @@ describe("IBosonFundsHandler", function () { expectedResellersAvailableFunds = resellerPayoffs.map((r) => { return new FundsList(r.payoff != "0" ? [new Funds(mockTokenAddress, "Foreign20", r.payoff)] : []); }); - expectedExternalRoyaltyRecipientsBalance = [totalRoyaltiesSplit.other, totalRoyaltiesSplit.other2]; + expectedRoyaltyRecipientsAvailableFunds = royaltyRecipientsPayoffs.map((r) => { + return new FundsList(r.payoff != "0" ? [new Funds(mockTokenAddress, "Foreign20", r.payoff)] : []); + }); sellersAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(seller.id)); buyerAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(buyerId)); @@ -4853,16 +5122,16 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); }); }); @@ -4883,6 +5152,15 @@ describe("IBosonFundsHandler", function () { // protocol: 0 protocolPayoff = 0; + + // royalty recipients: 0 + royaltyRecipientsPayoffs = [ + { + id: keyToId["other"], + payoff: 0n, + }, + { id: keyToId["other2"], payoff: 0n }, + ]; }); it("should emit a FundsReleased event", async function () { @@ -4928,9 +5206,9 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); // Chain state should match the expected available funds expectedSellerAvailableFunds = new FundsList([]); @@ -4938,13 +5216,15 @@ describe("IBosonFundsHandler", function () { expectedProtocolAvailableFunds = new FundsList([]); expectedAgentAvailableFunds = new FundsList([]); expectedResellersAvailableFunds = new Array(resellerPayoffs.length).fill(new FundsList([])); - expectedExternalRoyaltyRecipientsBalance = [0n, 0n]; + expectedRoyaltyRecipientsAvailableFunds = new Array(royaltyRecipientsPayoffs.length).fill( + new FundsList([]) + ); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); // Revoke the voucher so the funds are released await exchangeHandler.connect(assistant).revokeVoucher(exchangeId); @@ -4955,7 +5235,7 @@ describe("IBosonFundsHandler", function () { // resellers: difference between original price and immediate payoff // protocol: 0 // agent: 0 - // external royalty recipients: 0 + // royalty recipients: 0 expectedBuyerAvailableFunds.funds.push(new Funds(mockTokenAddress, "Foreign20", buyerPayoff)); expectedResellersAvailableFunds = resellerPayoffs.map((r) => { return new FundsList(r.payoff != "0" ? [new Funds(mockTokenAddress, "Foreign20", r.payoff)] : []); @@ -4968,16 +5248,16 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); }); }); @@ -4998,6 +5278,15 @@ describe("IBosonFundsHandler", function () { // protocol: 0 protocolPayoff = 0; + + // royalty recipients: 0 + royaltyRecipientsPayoffs = [ + { + id: keyToId["other"], + payoff: 0n, + }, + { id: keyToId["other2"], payoff: 0n }, + ]; }); it("should emit a FundsReleased event", async function () { @@ -5048,9 +5337,9 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); // Chain state should match the expected available funds expectedSellerAvailableFunds = new FundsList([]); @@ -5058,13 +5347,15 @@ describe("IBosonFundsHandler", function () { expectedProtocolAvailableFunds = new FundsList([]); expectedAgentAvailableFunds = new FundsList([]); expectedResellersAvailableFunds = new Array(resellerPayoffs.length).fill(new FundsList([])); - expectedExternalRoyaltyRecipientsBalance = [0n, 0n]; + expectedRoyaltyRecipientsAvailableFunds = new Array(royaltyRecipientsPayoffs.length).fill( + new FundsList([]) + ); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); // Cancel the voucher, so the funds are released await exchangeHandler.connect(voucherOwner).cancelVoucher(exchangeId); @@ -5075,7 +5366,7 @@ describe("IBosonFundsHandler", function () { // resellers: difference between original price and immediate payoff // protocol: 0 // agent: 0 - // external royalty recipients: 0 + // royalty recipients: 0 expectedSellerAvailableFunds.funds[0] = new Funds(mockTokenAddress, "Foreign20", sellerPayoff); expectedBuyerAvailableFunds.funds.push(new Funds(mockTokenAddress, "Foreign20", buyerPayoff)); expectedResellersAvailableFunds = resellerPayoffs.map((r) => { @@ -5089,16 +5380,16 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); }); }); @@ -5146,6 +5437,15 @@ describe("IBosonFundsHandler", function () { // protocol: protocolFee protocolPayoff = (totalProtocolFee + BigInt(initialFee)).toString(); + + // royalty recipients: royalties + royaltyRecipientsPayoffs = [ + { + id: keyToId["other"], + payoff: totalRoyaltiesSplit.other, + }, + { id: keyToId["other2"], payoff: totalRoyaltiesSplit.other2 }, + ]; }); it("should emit a FundsReleased event", async function () { @@ -5174,6 +5474,22 @@ describe("IBosonFundsHandler", function () { } } + // royalty recipients + for (const royaltyRecipientPayoff of royaltiesPerExchange) { + if (royaltyRecipientPayoff.payoff != 0n) { + expectedEventCount++; + await expect(tx) + .to.emit(exchangeHandler, "FundsReleased") + .withArgs( + exchangeId, + royaltyRecipientPayoff.id, + offer.exchangeToken, + royaltyRecipientPayoff.payoff, + voucherOwner.address + ); + } + } + // Make sure exact number of FundsReleased events was emitted const eventCount = (await tx.wait()).logs.filter((e) => e.eventName == "FundsReleased").length; expect(eventCount).to.equal(expectedEventCount); @@ -5197,9 +5513,9 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); // Chain state should match the expected available funds expectedSellerAvailableFunds = new FundsList([]); @@ -5207,13 +5523,15 @@ describe("IBosonFundsHandler", function () { expectedProtocolAvailableFunds = new FundsList([]); expectedAgentAvailableFunds = new FundsList([]); expectedResellersAvailableFunds = new Array(resellerPayoffs.length).fill(new FundsList([])); - expectedExternalRoyaltyRecipientsBalance = [0n, 0n]; + expectedRoyaltyRecipientsAvailableFunds = new Array(royaltyRecipientsPayoffs.length).fill( + new FundsList([]) + ); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); // Retract from the dispute, so the funds are released await disputeHandler.connect(voucherOwner).retractDispute(exchangeId); @@ -5224,7 +5542,7 @@ describe("IBosonFundsHandler", function () { // resellers: difference between the secondary price and immediate payout // protocol: protocolFee // agent: 0 - // external royalty recipients: royalties + // royalty recipients: royalties expectedSellerAvailableFunds.funds.push(new Funds(mockTokenAddress, "Foreign20", sellerPayoff)); if (protocolPayoff != "0") { expectedProtocolAvailableFunds.funds.push( @@ -5234,7 +5552,9 @@ describe("IBosonFundsHandler", function () { expectedResellersAvailableFunds = resellerPayoffs.map((r) => { return new FundsList(r.payoff != "0" ? [new Funds(mockTokenAddress, "Foreign20", r.payoff)] : []); }); - expectedExternalRoyaltyRecipientsBalance = [totalRoyaltiesSplit.other, totalRoyaltiesSplit.other2]; + expectedRoyaltyRecipientsAvailableFunds = royaltyRecipientsPayoffs.map((r) => { + return new FundsList(r.payoff != "0" ? [new Funds(mockTokenAddress, "Foreign20", r.payoff)] : []); + }); sellersAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(seller.id)); buyerAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(buyerId)); @@ -5243,16 +5563,16 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); }); }); @@ -5283,6 +5603,15 @@ describe("IBosonFundsHandler", function () { // protocol: protocolFee protocolPayoff = (totalProtocolFee + BigInt(initialFee)).toString(); + // royalty recipients: royalties + royaltyRecipientsPayoffs = [ + { + id: keyToId["other"], + payoff: totalRoyaltiesSplit.other, + }, + { id: keyToId["other2"], payoff: totalRoyaltiesSplit.other2 }, + ]; + await setNextBlockTimestamp(Number(timeout)); }); @@ -5312,6 +5641,22 @@ describe("IBosonFundsHandler", function () { } } + // royalty recipients + for (const royaltyRecipientPayoff of royaltiesPerExchange) { + if (royaltyRecipientPayoff.payoff != 0n) { + expectedEventCount++; + await expect(tx) + .to.emit(exchangeHandler, "FundsReleased") + .withArgs( + exchangeId, + royaltyRecipientPayoff.id, + offer.exchangeToken, + royaltyRecipientPayoff.payoff, + rando.address + ); + } + } + // Make sure exact number of FundsReleased events was emitted const eventCount = (await tx.wait()).logs.filter((e) => e.eventName == "FundsReleased").length; expect(eventCount).to.equal(expectedEventCount); @@ -5335,9 +5680,9 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); // Chain state should match the expected available funds expectedSellerAvailableFunds = new FundsList([]); @@ -5345,13 +5690,15 @@ describe("IBosonFundsHandler", function () { expectedProtocolAvailableFunds = new FundsList([]); expectedAgentAvailableFunds = new FundsList([]); expectedResellersAvailableFunds = new Array(resellerPayoffs.length).fill(new FundsList([])); - expectedExternalRoyaltyRecipientsBalance = [0n, 0n]; + expectedRoyaltyRecipientsAvailableFunds = new Array(royaltyRecipientsPayoffs.length).fill( + new FundsList([]) + ); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); // Expire the dispute, so the funds are released await disputeHandler.connect(rando).expireDispute(exchangeId); @@ -5362,7 +5709,7 @@ describe("IBosonFundsHandler", function () { // resellers: difference between the secondary price and immediate payout // protocol: protocolFee // agent: 0 - // external royalty recipients: royalties + // royalty recipients: royalties expectedSellerAvailableFunds.funds.push(new Funds(mockTokenAddress, "Foreign20", sellerPayoff)); if (protocolPayoff != "0") { expectedProtocolAvailableFunds.funds.push( @@ -5372,7 +5719,9 @@ describe("IBosonFundsHandler", function () { expectedResellersAvailableFunds = resellerPayoffs.map((r) => { return new FundsList(r.payoff != "0" ? [new Funds(mockTokenAddress, "Foreign20", r.payoff)] : []); }); - expectedExternalRoyaltyRecipientsBalance = [totalRoyaltiesSplit.other, totalRoyaltiesSplit.other2]; + expectedRoyaltyRecipientsAvailableFunds = royaltyRecipientsPayoffs.map((r) => { + return new FundsList(r.payoff != "0" ? [new Funds(mockTokenAddress, "Foreign20", r.payoff)] : []); + }); sellersAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(seller.id)); buyerAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(buyerId)); @@ -5381,16 +5730,16 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); }); }); @@ -5420,6 +5769,7 @@ describe("IBosonFundsHandler", function () { // recalculate the royalties due to rounding errors totalRoyaltiesSplit = { other: 0n, other2: 0n }; totalRoyalties = 0n; + royaltiesPerExchange = []; for (const trade of buyerChains[direction]) { const effectivePrice = applyPercentage(trade.price, sellerPercentBasisPoints); @@ -5427,9 +5777,11 @@ describe("IBosonFundsHandler", function () { totalRoyalties + BigInt(applyPercentage(applyPercentage(trade.price, fee.royalties), sellerPercentBasisPoints)); for (const [key, value] of Object.entries(totalRoyaltiesSplit)) { - totalRoyaltiesSplit[key] = - value + - BigInt(applyPercentage(effectivePrice, applyPercentage(fee.royalties, royaltySplit[key]))); + const thisTradeRoyalties = BigInt( + applyPercentage(effectivePrice, applyPercentage(fee.royalties, royaltySplit[key])) + ); + totalRoyaltiesSplit[key] = value + thisTradeRoyalties; + royaltiesPerExchange.push({ id: keyToId[key], payoff: thisTradeRoyalties }); } } totalRoyaltiesSplit.seller = @@ -5447,6 +5799,15 @@ describe("IBosonFundsHandler", function () { // protocol: protocolFee (only secondary market) protocolPayoff = applyPercentage(totalProtocolFee + BigInt(initialFee), sellerPercentBasisPoints); + // royalty recipients: royalties + royaltyRecipientsPayoffs = [ + { + id: keyToId["other"], + payoff: totalRoyaltiesSplit.other, + }, + { id: keyToId["other2"], payoff: totalRoyaltiesSplit.other2 }, + ]; + // Set the message Type, needed for signature resolutionType = [ { name: "exchangeId", type: "uint256" }, @@ -5505,6 +5866,22 @@ describe("IBosonFundsHandler", function () { } } + // royalty recipients + for (const royaltyRecipientPayoff of royaltiesPerExchange) { + if (royaltyRecipientPayoff.payoff != 0n) { + expectedEventCount++; + await expect(tx) + .to.emit(exchangeHandler, "FundsReleased") + .withArgs( + exchangeId, + royaltyRecipientPayoff.id, + offer.exchangeToken, + royaltyRecipientPayoff.payoff, + assistant.address + ); + } + } + // Make sure exact number of FundsReleased events was emitted const eventCount = (await tx.wait()).logs.filter((e) => e.eventName == "FundsReleased").length; expect(eventCount).to.equal(expectedEventCount); @@ -5528,9 +5905,9 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); // Chain state should match the expected available funds expectedSellerAvailableFunds = new FundsList([]); @@ -5538,13 +5915,15 @@ describe("IBosonFundsHandler", function () { expectedProtocolAvailableFunds = new FundsList([]); expectedAgentAvailableFunds = new FundsList([]); expectedResellersAvailableFunds = new Array(resellerPayoffs.length).fill(new FundsList([])); - expectedExternalRoyaltyRecipientsBalance = [0n, 0n]; + expectedRoyaltyRecipientsAvailableFunds = new Array(royaltyRecipientsPayoffs.length).fill( + new FundsList([]) + ); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); // Resolve the dispute, so the funds are released await disputeHandler @@ -5557,7 +5936,7 @@ describe("IBosonFundsHandler", function () { // resellers: (difference between the secondary price and immediate payout)*(1-buyerPercentage) // protocol: protocolFee (secondary market only) // agent: 0 - // external royalty recipients: royalties*(1-buyerPercentage) + // royalty recipients: royalties*(1-buyerPercentage) expectedSellerAvailableFunds.funds.push(new Funds(mockTokenAddress, "Foreign20", sellerPayoff)); expectedBuyerAvailableFunds = new FundsList([ new Funds(mockTokenAddress, "Foreign20", buyerPayoff), @@ -5570,7 +5949,9 @@ describe("IBosonFundsHandler", function () { expectedResellersAvailableFunds = resellerPayoffs.map((r) => { return new FundsList(r.payoff != "0" ? [new Funds(mockTokenAddress, "Foreign20", r.payoff)] : []); }); - expectedExternalRoyaltyRecipientsBalance = [totalRoyaltiesSplit.other, totalRoyaltiesSplit.other2]; + expectedRoyaltyRecipientsAvailableFunds = royaltyRecipientsPayoffs.map((r) => { + return new FundsList(r.payoff != "0" ? [new Funds(mockTokenAddress, "Foreign20", r.payoff)] : []); + }); sellersAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(seller.id)); buyerAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(buyerId)); @@ -5579,16 +5960,16 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); }); }); @@ -5619,6 +6000,15 @@ describe("IBosonFundsHandler", function () { // protocol: protocolFee protocolPayoff = (totalProtocolFee + BigInt(initialFee)).toString(); + // royalty recipients: royalties + royaltyRecipientsPayoffs = [ + { + id: keyToId["other"], + payoff: totalRoyaltiesSplit.other, + }, + { id: keyToId["other2"], payoff: totalRoyaltiesSplit.other2 }, + ]; + // Escalate the dispute await disputeHandler.connect(voucherOwner).escalateDispute(exchangeId); }); @@ -5649,6 +6039,22 @@ describe("IBosonFundsHandler", function () { } } + // royalty recipients + for (const royaltyRecipientPayoff of royaltiesPerExchange) { + if (royaltyRecipientPayoff.payoff != 0n) { + expectedEventCount++; + await expect(tx) + .to.emit(exchangeHandler, "FundsReleased") + .withArgs( + exchangeId, + royaltyRecipientPayoff.id, + offer.exchangeToken, + royaltyRecipientPayoff.payoff, + voucherOwner.address + ); + } + } + // Make sure exact number of FundsReleased events was emitted const eventCount = (await tx.wait()).logs.filter((e) => e.eventName == "FundsReleased").length; expect(eventCount).to.equal(expectedEventCount); @@ -5672,9 +6078,9 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); // Chain state should match the expected available funds expectedSellerAvailableFunds = new FundsList([]); @@ -5682,13 +6088,15 @@ describe("IBosonFundsHandler", function () { expectedProtocolAvailableFunds = new FundsList([]); expectedAgentAvailableFunds = new FundsList([]); expectedResellersAvailableFunds = new Array(resellerPayoffs.length).fill(new FundsList([])); - expectedExternalRoyaltyRecipientsBalance = [0n, 0n]; + expectedRoyaltyRecipientsAvailableFunds = new Array(royaltyRecipientsPayoffs.length).fill( + new FundsList([]) + ); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); // Retract from the dispute, so the funds are released await disputeHandler.connect(voucherOwner).retractDispute(exchangeId); @@ -5699,7 +6107,7 @@ describe("IBosonFundsHandler", function () { // resellers: difference between the secondary price and immediate payout // protocol: protocolFee // agent: 0 - // external royalty recipients: royalties + // royalty recipients: royalties expectedSellerAvailableFunds.funds.push(new Funds(mockTokenAddress, "Foreign20", sellerPayoff)); if (protocolPayoff != "0") { expectedProtocolAvailableFunds.funds.push( @@ -5709,7 +6117,9 @@ describe("IBosonFundsHandler", function () { expectedResellersAvailableFunds = resellerPayoffs.map((r) => { return new FundsList(r.payoff != "0" ? [new Funds(mockTokenAddress, "Foreign20", r.payoff)] : []); }); - expectedExternalRoyaltyRecipientsBalance = [totalRoyaltiesSplit.other, totalRoyaltiesSplit.other2]; + expectedRoyaltyRecipientsAvailableFunds = royaltyRecipientsPayoffs.map((r) => { + return new FundsList(r.payoff != "0" ? [new Funds(mockTokenAddress, "Foreign20", r.payoff)] : []); + }); sellersAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(seller.id)); buyerAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(buyerId)); @@ -5718,16 +6128,16 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); }); }); @@ -5757,6 +6167,7 @@ describe("IBosonFundsHandler", function () { // recalculate the royalties due to rounding errors totalRoyaltiesSplit = { other: 0n, other2: 0n }; totalRoyalties = 0n; + royaltiesPerExchange = []; for (const trade of buyerChains[direction]) { const effectivePrice = applyPercentage(trade.price, sellerPercentBasisPoints); @@ -5764,9 +6175,12 @@ describe("IBosonFundsHandler", function () { totalRoyalties + BigInt(applyPercentage(applyPercentage(trade.price, fee.royalties), sellerPercentBasisPoints)); for (const [key, value] of Object.entries(totalRoyaltiesSplit)) { - totalRoyaltiesSplit[key] = - value + - BigInt(applyPercentage(effectivePrice, applyPercentage(fee.royalties, royaltySplit[key]))); + const thisTradeRoyalties = BigInt( + applyPercentage(effectivePrice, applyPercentage(fee.royalties, royaltySplit[key])) + ); + + totalRoyaltiesSplit[key] = value + thisTradeRoyalties; + royaltiesPerExchange.push({ id: keyToId[key], payoff: thisTradeRoyalties }); } } totalRoyaltiesSplit.seller = @@ -5784,6 +6198,15 @@ describe("IBosonFundsHandler", function () { // protocol: protocolFee *(1-buyerPercentage) protocolPayoff = applyPercentage(totalProtocolFee + BigInt(initialFee), sellerPercentBasisPoints); + // royalty recipients: royalties + royaltyRecipientsPayoffs = [ + { + id: keyToId["other"], + payoff: totalRoyaltiesSplit.other, + }, + { id: keyToId["other2"], payoff: totalRoyaltiesSplit.other2 }, + ]; + // Set the message Type, needed for signature resolutionType = [ { name: "exchangeId", type: "uint256" }, @@ -5845,6 +6268,22 @@ describe("IBosonFundsHandler", function () { } } + // royalty recipients + for (const royaltyRecipientPayoff of royaltiesPerExchange) { + if (royaltyRecipientPayoff.payoff != 0n) { + expectedEventCount++; + await expect(tx) + .to.emit(exchangeHandler, "FundsReleased") + .withArgs( + exchangeId, + royaltyRecipientPayoff.id, + offer.exchangeToken, + royaltyRecipientPayoff.payoff, + assistant.address + ); + } + } + // Make sure exact number of FundsReleased events was emitted const eventCount = (await tx.wait()).logs.filter((e) => e.eventName == "FundsReleased").length; expect(eventCount).to.equal(expectedEventCount); @@ -5868,9 +6307,9 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); // Chain state should match the expected available funds expectedSellerAvailableFunds = new FundsList([]); @@ -5878,13 +6317,15 @@ describe("IBosonFundsHandler", function () { expectedProtocolAvailableFunds = new FundsList([]); expectedAgentAvailableFunds = new FundsList([]); expectedResellersAvailableFunds = new Array(resellerPayoffs.length).fill(new FundsList([])); - expectedExternalRoyaltyRecipientsBalance = [0n, 0n]; + expectedRoyaltyRecipientsAvailableFunds = new Array(royaltyRecipientsPayoffs.length).fill( + new FundsList([]) + ); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); // Resolve the dispute, so the funds are released await disputeHandler @@ -5897,7 +6338,7 @@ describe("IBosonFundsHandler", function () { // resellers: (difference between the secondary price and immediate payout)*(1-buyerPercentage) // protocol: protocolFee*(1-buyerPercentage) // agent: 0 - // external royalty recipients: royalties*(1-buyerPercentage) + // royalty recipients: royalties*(1-buyerPercentage) expectedSellerAvailableFunds.funds.push(new Funds(mockTokenAddress, "Foreign20", sellerPayoff)); expectedBuyerAvailableFunds = new FundsList([ new Funds(mockTokenAddress, "Foreign20", buyerPayoff), @@ -5910,7 +6351,9 @@ describe("IBosonFundsHandler", function () { expectedResellersAvailableFunds = resellerPayoffs.map((r) => { return new FundsList(r.payoff != "0" ? [new Funds(mockTokenAddress, "Foreign20", r.payoff)] : []); }); - expectedExternalRoyaltyRecipientsBalance = [totalRoyaltiesSplit.other, totalRoyaltiesSplit.other2]; + expectedRoyaltyRecipientsAvailableFunds = royaltyRecipientsPayoffs.map((r) => { + return new FundsList(r.payoff != "0" ? [new Funds(mockTokenAddress, "Foreign20", r.payoff)] : []); + }); sellersAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(seller.id)); buyerAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(buyerId)); @@ -5919,16 +6362,16 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); }); }); @@ -5958,6 +6401,8 @@ describe("IBosonFundsHandler", function () { // recalculate the royalties due to rounding errors totalRoyaltiesSplit = { other: 0n, other2: 0n }; totalRoyalties = 0n; + royaltiesPerExchange = []; + for (const trade of buyerChains[direction]) { const effectivePrice = applyPercentage(trade.price, sellerPercentBasisPoints); @@ -5965,9 +6410,11 @@ describe("IBosonFundsHandler", function () { totalRoyalties + BigInt(applyPercentage(applyPercentage(trade.price, fee.royalties), sellerPercentBasisPoints)); for (const [key, value] of Object.entries(totalRoyaltiesSplit)) { - totalRoyaltiesSplit[key] = - value + - BigInt(applyPercentage(effectivePrice, applyPercentage(fee.royalties, royaltySplit[key]))); + const thisTradeRoyalties = BigInt( + applyPercentage(effectivePrice, applyPercentage(fee.royalties, royaltySplit[key])) + ); + totalRoyaltiesSplit[key] = value + thisTradeRoyalties; + royaltiesPerExchange.push({ id: keyToId[key], payoff: thisTradeRoyalties }); } } totalRoyaltiesSplit.seller = @@ -5985,6 +6432,15 @@ describe("IBosonFundsHandler", function () { // protocol: protocolFee*(1-buyerPercentage) protocolPayoff = applyPercentage(totalProtocolFee + BigInt(initialFee), sellerPercentBasisPoints); + // royalty recipients: royalties + royaltyRecipientsPayoffs = [ + { + id: keyToId["other"], + payoff: totalRoyaltiesSplit.other, + }, + { id: keyToId["other2"], payoff: totalRoyaltiesSplit.other2 }, + ]; + // Escalate the dispute await disputeHandler.connect(voucherOwner).escalateDispute(exchangeId); }); @@ -6022,6 +6478,22 @@ describe("IBosonFundsHandler", function () { } } + // royalty recipients + for (const royaltyRecipientPayoff of royaltiesPerExchange) { + if (royaltyRecipientPayoff.payoff != 0n) { + expectedEventCount++; + await expect(tx) + .to.emit(exchangeHandler, "FundsReleased") + .withArgs( + exchangeId, + royaltyRecipientPayoff.id, + offer.exchangeToken, + royaltyRecipientPayoff.payoff, + assistantDR.address + ); + } + } + // Make sure exact number of FundsReleased events was emitted const eventCount = (await tx.wait()).logs.filter((e) => e.eventName == "FundsReleased").length; expect(eventCount).to.equal(expectedEventCount); @@ -6045,9 +6517,9 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); // Chain state should match the expected available funds expectedSellerAvailableFunds = new FundsList([]); @@ -6055,13 +6527,15 @@ describe("IBosonFundsHandler", function () { expectedProtocolAvailableFunds = new FundsList([]); expectedAgentAvailableFunds = new FundsList([]); expectedResellersAvailableFunds = new Array(resellerPayoffs.length).fill(new FundsList([])); - expectedExternalRoyaltyRecipientsBalance = [0n, 0n]; + expectedRoyaltyRecipientsAvailableFunds = new Array(royaltyRecipientsPayoffs.length).fill( + new FundsList([]) + ); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); // Decide the dispute, so the funds are released await disputeHandler.connect(assistantDR).decideDispute(exchangeId, buyerPercentBasisPoints); @@ -6072,7 +6546,7 @@ describe("IBosonFundsHandler", function () { // resellers: (difference between the secondary price and immediate payout)*(1-buyerPercentage) // protocol: protocolFee*(1-buyerPercentage) // agent: 0 - // external royalty recipients: royalties*(1-buyerPercentage) + // royalty recipients: royalties*(1-buyerPercentage) expectedSellerAvailableFunds.funds.push(new Funds(mockTokenAddress, "Foreign20", sellerPayoff)); expectedBuyerAvailableFunds = new FundsList([ new Funds(mockTokenAddress, "Foreign20", buyerPayoff), @@ -6085,7 +6559,9 @@ describe("IBosonFundsHandler", function () { expectedResellersAvailableFunds = resellerPayoffs.map((r) => { return new FundsList(r.payoff != "0" ? [new Funds(mockTokenAddress, "Foreign20", r.payoff)] : []); }); - expectedExternalRoyaltyRecipientsBalance = [totalRoyaltiesSplit.other, totalRoyaltiesSplit.other2]; + expectedRoyaltyRecipientsAvailableFunds = royaltyRecipientsPayoffs.map((r) => { + return new FundsList(r.payoff != "0" ? [new Funds(mockTokenAddress, "Foreign20", r.payoff)] : []); + }); sellersAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(seller.id)); buyerAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(buyerId)); @@ -6094,16 +6570,16 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); }); }); @@ -6127,6 +6603,15 @@ describe("IBosonFundsHandler", function () { // protocol: 0 protocolPayoff = 0; + // royalty recipients: 0 + royaltyRecipientsPayoffs = [ + { + id: keyToId["other"], + payoff: 0n, + }, + { id: keyToId["other2"], payoff: 0n }, + ]; + // Escalate the dispute tx = await disputeHandler.connect(voucherOwner).escalateDispute(exchangeId); @@ -6190,9 +6675,9 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); // Chain state should match the expected available funds expectedSellerAvailableFunds = new FundsList([]); @@ -6200,13 +6685,15 @@ describe("IBosonFundsHandler", function () { expectedProtocolAvailableFunds = new FundsList([]); expectedAgentAvailableFunds = new FundsList([]); expectedResellersAvailableFunds = new Array(resellerPayoffs.length).fill(new FundsList([])); - expectedExternalRoyaltyRecipientsBalance = [0n, 0n]; + expectedRoyaltyRecipientsAvailableFunds = new Array(royaltyRecipientsPayoffs.length).fill( + new FundsList([]) + ); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); // Expire the escalated dispute, so the funds are released await disputeHandler.connect(rando).expireEscalatedDispute(exchangeId); @@ -6217,7 +6704,7 @@ describe("IBosonFundsHandler", function () { // resellers: difference between the secondary price and immediate payout // protocol: 0 // agent: 0 - // external royalty recipients: 0 + // royalty recipients: 0 expectedBuyerAvailableFunds.funds[0] = new Funds(mockTokenAddress, "Foreign20", buyerPayoff); expectedSellerAvailableFunds.funds.push(new Funds(mockTokenAddress, "Foreign20", sellerPayoff)); expectedResellersAvailableFunds = resellerPayoffs.map((r) => { @@ -6235,16 +6722,16 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); }); } ); @@ -6269,6 +6756,15 @@ describe("IBosonFundsHandler", function () { // protocol: 0 protocolPayoff = 0; + // royalty recipients: 0 + royaltyRecipientsPayoffs = [ + { + id: keyToId["other"], + payoff: 0n, + }, + { id: keyToId["other2"], payoff: 0n }, + ]; + // Escalate the dispute await disputeHandler.connect(voucherOwner).escalateDispute(exchangeId); }); @@ -6323,9 +6819,9 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); - externalRoyaltyRecipientsBalance = await Promise.all( - [other, other2].map((r) => mockToken.balanceOf(r.address)) - ); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); // Chain state should match the expected available funds expectedSellerAvailableFunds = new FundsList([]); @@ -6333,13 +6829,15 @@ describe("IBosonFundsHandler", function () { expectedProtocolAvailableFunds = new FundsList([]); expectedAgentAvailableFunds = new FundsList([]); expectedResellersAvailableFunds = new Array(resellerPayoffs.length).fill(new FundsList([])); - expectedExternalRoyaltyRecipientsBalance = [0n, 0n]; + expectedRoyaltyRecipientsAvailableFunds = new Array(royaltyRecipientsPayoffs.length).fill( + new FundsList([]) + ); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); // Refuse the escalated dispute, so the funds are released await disputeHandler.connect(assistantDR).refuseEscalatedDispute(exchangeId); @@ -6350,7 +6848,7 @@ describe("IBosonFundsHandler", function () { // resellers: difference between the secondary price and immediate payout // protocol: 0 // agent: 0 - // external royalty recipients: 0 + // royalty recipients: 0 expectedBuyerAvailableFunds.funds[0] = new Funds(mockTokenAddress, "Foreign20", buyerPayoff); expectedSellerAvailableFunds.funds.push(new Funds(mockTokenAddress, "Foreign20", sellerPayoff)); expectedResellersAvailableFunds = resellerPayoffs.map((r) => { @@ -6368,13 +6866,16 @@ describe("IBosonFundsHandler", function () { resellersAvailableFunds = ( await Promise.all(resellerPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) ).map((returnedValue) => FundsList.fromStruct(returnedValue)); + royaltyRecipientsAvailableFunds = ( + await Promise.all(royaltyRecipientsPayoffs.map((r) => fundsHandler.getAllAvailableFunds(r.id))) + ).map((returnedValue) => FundsList.fromStruct(returnedValue)); expect(sellersAvailableFunds).to.eql(expectedSellerAvailableFunds); expect(buyerAvailableFunds).to.eql(expectedBuyerAvailableFunds); expect(protocolAvailableFunds).to.eql(expectedProtocolAvailableFunds); expect(agentAvailableFunds).to.eql(expectedAgentAvailableFunds); expect(resellersAvailableFunds).to.eql(expectedResellersAvailableFunds); - expect(externalRoyaltyRecipientsBalance).to.eql(expectedExternalRoyaltyRecipientsBalance); + expect(royaltyRecipientsAvailableFunds).to.eql(expectedRoyaltyRecipientsAvailableFunds); }); } ); @@ -6435,7 +6936,7 @@ describe("IBosonFundsHandler", function () { agentId = "3"; buyerId = 5; - // Create buyer with protocol address to not mess up ids in tests + // Create buyer with price discovery client address to not mess up ids in tests await accountHandler.createBuyer(mockBuyer(await bpd.getAddress())); // commit to offer diff --git a/test/protocol/OfferHandlerTest.js b/test/protocol/OfferHandlerTest.js index 374754033..bfa0a9bff 100644 --- a/test/protocol/OfferHandlerTest.js +++ b/test/protocol/OfferHandlerTest.js @@ -10,7 +10,7 @@ const DisputeResolutionTerms = require("../../scripts/domain/DisputeResolutionTe const OfferFees = require("../../scripts/domain/OfferFees"); const PausableRegion = require("../../scripts/domain/PausableRegion.js"); const Range = require("../../scripts/domain/Range"); -const { RoyaltyRecipient, RoyaltyRecipientList } = require("../../scripts/domain/RoyaltyRecipient.js"); +const { RoyaltyRecipientInfo, RoyaltyRecipientInfoList } = require("../../scripts/domain/RoyaltyRecipientInfo.js"); const { RoyaltyInfo } = require("../../scripts/domain/RoyaltyInfo"); const { getInterfaceIds } = require("../../scripts/config/supported-interfaces.js"); const { RevertReasons } = require("../../scripts/config/revert-reasons.js"); @@ -655,10 +655,13 @@ describe("IBosonOfferHandler", function () { it("Should allow creation of an offer with royalty recipients", async function () { // Add royalty recipients - const royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(other.address, "100", "other"), - new RoyaltyRecipient(other2.address, "200", "other2"), + const royaltyRecipientList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other.address, "100"), + new RoyaltyRecipientInfo(other2.address, "200"), ]); + // royalty recipients increment the account id in the protocol + accountId.next(); + accountId.next(); await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); // Add royalty info to the offer @@ -1033,10 +1036,13 @@ describe("IBosonOfferHandler", function () { context("With royalty info", async function () { beforeEach(async function () { // Add royalty recipients - const royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(other.address, "100", "other"), - new RoyaltyRecipient(other2.address, "200", "other2"), + const royaltyRecipientList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other.address, "100"), + new RoyaltyRecipientInfo(other2.address, "200"), ]); + // royalty recipients increment the account id in the protocol + accountId.next(); + accountId.next(); await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); }); @@ -1558,11 +1564,13 @@ describe("IBosonOfferHandler", function () { .createOffer(offer, offerDates, offerDurations, disputeResolver.id, agentId, offerFeeLimit); // Register royalty recipients - const royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(other.address, "50", "other"), - new RoyaltyRecipient(other2.address, "50", "other2"), - new RoyaltyRecipient(rando.address, "50", "other3"), + const royaltyRecipientList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other.address, "50"), + new RoyaltyRecipientInfo(other2.address, "50"), + new RoyaltyRecipientInfo(rando.address, "50"), ]); + // royalty recipients increment the account id in the protocol + royaltyRecipientList.royaltyRecipientInfos.forEach(() => accountId.next()); await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); const recipients = [other.address, other2.address, ZeroAddress, rando.address]; @@ -2323,10 +2331,14 @@ describe("IBosonOfferHandler", function () { offerFeesStructs[4] = offerFeesList[4].toStruct(); // offer with royalty recipients - const royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(other.address, "50", "other"), - new RoyaltyRecipient(other2.address, "50", "other2"), + const royaltyRecipientList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other.address, "50"), + new RoyaltyRecipientInfo(other2.address, "50"), ]); + // royalty recipients increment the account id in the protocol + accountId.next(); + accountId.next(); + await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); offers[3].royaltyInfo = [new RoyaltyInfo([other.address, ZeroAddress], ["150", "10"])]; offerStructs[3] = offers[3].toStruct(); @@ -2638,7 +2650,7 @@ describe("IBosonOfferHandler", function () { await accountHandler.connect(rando).createSeller(seller, emptyAuthToken, voucherInitValues); - allowedSellersToAdd = ["3"]; + allowedSellersToAdd = [seller.id]; await accountHandler.connect(adminDR).addSellersToAllowList(disputeResolver.id, allowedSellersToAdd); // Attempt to Create an offer, expecting revert @@ -2649,7 +2661,7 @@ describe("IBosonOfferHandler", function () { ).to.revertedWithCustomError(bosonErrors, RevertReasons.SELLER_NOT_APPROVED); // add seller to allow list - allowedSellersToAdd = ["1"]; // existing seller is "1", DR is "2", new seller is "3" + allowedSellersToAdd = ["1"]; await accountHandler.connect(adminDR).addSellersToAllowList(disputeResolver.id, allowedSellersToAdd); // Create an offer, testing for the event @@ -3085,7 +3097,7 @@ describe("IBosonOfferHandler", function () { await accountHandler.connect(rando).createSeller(seller, emptyAuthToken, voucherInitValues); - allowedSellersToAdd = ["3"]; + allowedSellersToAdd = [seller.id]; await accountHandler.connect(adminDR).addSellersToAllowList(disputeResolver.id, allowedSellersToAdd); // Attempt to Create an offer, expecting revert @@ -3404,13 +3416,12 @@ describe("IBosonOfferHandler", function () { context("When offers have non zero agent ids", async function () { beforeEach(async function () { nonZeroAgentIds = []; - agentId = "3"; offerFeesList = []; offerFeesStructs = []; // Create an agent: Required constructor params agent = mockAgent(await other.getAddress()); - agent.id = agentId; + agentId = agent.id; expect(agent.isValid()).is.true; // Create a valid agent await accountHandler.connect(rando).createAgent(agent); @@ -3561,11 +3572,7 @@ describe("IBosonOfferHandler", function () { it("Sum of agent fee amount and protocol fee amount should be <= than the protocol wide offer fee limit", async function () { // Create new agent - let id = "4"; // argument sent to contract for createAgent will be ignored - - // Create a valid agent, then set fields in tests directly agent = mockAgent(await assistant.getAddress()); - agent.id = id; agent.feePercentage = "3000"; // 30% expect(agent.isValid()).is.true; @@ -3575,7 +3582,7 @@ describe("IBosonOfferHandler", function () { //Change protocol fee after creating agent await configHandler.connect(deployer).setProtocolFeePercentage("1100"); //11% - nonZeroAgentIds[1] = id; + nonZeroAgentIds[1] = agent.id; // Attempt to Create an offer, expecting revert await expect( @@ -3926,7 +3933,9 @@ describe("IBosonOfferHandler", function () { .createOfferBatch(offers, offerDatesList, offerDurationsList, disputeResolverIds, agentIds, offerFeeLimits); // Register royalty recipients - const royaltyRecipientList = new RoyaltyRecipientList([new RoyaltyRecipient(rando.address, "50", "other3")]); + const royaltyRecipientList = new RoyaltyRecipientInfoList([new RoyaltyRecipientInfo(rando.address, "50")]); + // royalty recipients increment the account id in the protocol + accountId.next(); await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); offersToUpdate = ["1", "4", "5"]; diff --git a/test/protocol/OrchestrationHandlerTest.js b/test/protocol/OrchestrationHandlerTest.js index df3708ecb..2b76a1a15 100644 --- a/test/protocol/OrchestrationHandlerTest.js +++ b/test/protocol/OrchestrationHandlerTest.js @@ -18,7 +18,7 @@ const TokenType = require("../../scripts/domain/TokenType"); const AuthToken = require("../../scripts/domain/AuthToken"); const AuthTokenType = require("../../scripts/domain/AuthTokenType"); const Range = require("../../scripts/domain/Range"); -const { RoyaltyRecipient, RoyaltyRecipientList } = require("../../scripts/domain/RoyaltyRecipient.js"); +const { RoyaltyRecipientInfo, RoyaltyRecipientInfoList } = require("../../scripts/domain/RoyaltyRecipientInfo.js"); const { RoyaltyInfo } = require("../../scripts/domain/RoyaltyInfo"); const { getInterfaceIds } = require("../../scripts/config/supported-interfaces.js"); const { RevertReasons } = require("../../scripts/config/revert-reasons.js"); @@ -26,7 +26,7 @@ const { getEvent, applyPercentage, compareOfferStructs, - compareRoyaltyRecipientLists, + compareRoyaltyRecipientInfoLists, calculateCloneAddress, calculateBosonProxyAddress, setupTestEnvironment, @@ -34,7 +34,7 @@ const { revertToSnapshot, } = require("../util/utils.js"); const { deployMockTokens } = require("../../scripts/util/deploy-mock-tokens"); -const { oneWeek, oneMonth, VOUCHER_NAME, VOUCHER_SYMBOL, DEFAULT_ROYALTY_RECIPIENT } = require("../util/constants"); +const { oneWeek, oneMonth, VOUCHER_NAME, VOUCHER_SYMBOL } = require("../util/constants"); const { mockTwin, mockOffer, @@ -644,15 +644,15 @@ describe("IBosonOrchestrationHandler", function () { await assistant.getAddress() ); - const expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); await expect(tx) .to.emit(accountHandler, "RoyaltyRecipientsChanged") .withArgs( seller.id, - compareRoyaltyRecipientLists.bind(expectedRoyaltyRecipientList.toStruct()), + compareRoyaltyRecipientInfoLists.bind(expectedRoyaltyRecipientInfoList.toStruct()), assistant.address ); @@ -711,15 +711,15 @@ describe("IBosonOrchestrationHandler", function () { await assistant.getAddress() ); - const expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); await expect(tx) .to.emit(accountHandler, "RoyaltyRecipientsChanged") .withArgs( seller.id, - compareRoyaltyRecipientLists.bind(expectedRoyaltyRecipientList.toStruct()), + compareRoyaltyRecipientInfoLists.bind(expectedRoyaltyRecipientInfoList.toStruct()), assistant.address ); @@ -1385,15 +1385,15 @@ describe("IBosonOrchestrationHandler", function () { await assistant.getAddress() ); - const expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); await expect(tx) .to.emit(accountHandler, "RoyaltyRecipientsChanged") .withArgs( seller.id, - compareRoyaltyRecipientLists.bind(expectedRoyaltyRecipientList.toStruct()), + compareRoyaltyRecipientInfoLists.bind(expectedRoyaltyRecipientInfoList.toStruct()), assistant.address ); @@ -2907,9 +2907,9 @@ describe("IBosonOrchestrationHandler", function () { it("Should allow creation of an offer with royalty recipients", async function () { // Add royalty recipients - const royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(other1.address, "100", "other1"), - new RoyaltyRecipient(other2.address, "200", "other2"), + const royaltyRecipientList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other1.address, "100"), + new RoyaltyRecipientInfo(other2.address, "200"), ]); await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); @@ -3343,9 +3343,9 @@ describe("IBosonOrchestrationHandler", function () { beforeEach(async function () { // Add royalty recipients - const royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(other1.address, "100", "other"), - new RoyaltyRecipient(other2.address, "200", "other2"), + const royaltyRecipientList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other1.address, "100"), + new RoyaltyRecipientInfo(other2.address, "200"), ]); await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); }); @@ -3875,9 +3875,9 @@ describe("IBosonOrchestrationHandler", function () { it("Should allow creation of an offer with royalty recipients", async function () { // Add royalty recipients - const royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(other1.address, "100", "other1"), - new RoyaltyRecipient(other2.address, "200", "other2"), + const royaltyRecipientList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other1.address, "100"), + new RoyaltyRecipientInfo(other2.address, "200"), ]); await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); @@ -4742,9 +4742,9 @@ describe("IBosonOrchestrationHandler", function () { it("Should allow creation of an offer with royalty recipients", async function () { // Add royalty recipients - const royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(other1.address, "100", "other1"), - new RoyaltyRecipient(other2.address, "200", "other2"), + const royaltyRecipientList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other1.address, "100"), + new RoyaltyRecipientInfo(other2.address, "200"), ]); await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); @@ -5792,9 +5792,9 @@ describe("IBosonOrchestrationHandler", function () { it("Should allow creation of an offer with royalty recipients", async function () { // Add royalty recipients - const royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(other1.address, "100", "other1"), - new RoyaltyRecipient(other2.address, "200", "other2"), + const royaltyRecipientList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other1.address, "100"), + new RoyaltyRecipientInfo(other2.address, "200"), ]); await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); @@ -6314,15 +6314,15 @@ describe("IBosonOrchestrationHandler", function () { await assistant.getAddress() ); - const expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); await expect(tx) .to.emit(accountHandler, "RoyaltyRecipientsChanged") .withArgs( seller.id, - compareRoyaltyRecipientLists.bind(expectedRoyaltyRecipientList.toStruct()), + compareRoyaltyRecipientInfoLists.bind(expectedRoyaltyRecipientInfoList.toStruct()), assistant.address ); @@ -6813,15 +6813,15 @@ describe("IBosonOrchestrationHandler", function () { await assistant.getAddress() ); - const expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); await expect(tx) .to.emit(accountHandler, "RoyaltyRecipientsChanged") .withArgs( seller.id, - compareRoyaltyRecipientLists.bind(expectedRoyaltyRecipientList.toStruct()), + compareRoyaltyRecipientInfoLists.bind(expectedRoyaltyRecipientInfoList.toStruct()), assistant.address ); @@ -7161,15 +7161,15 @@ describe("IBosonOrchestrationHandler", function () { await assistant.getAddress() ); - const expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); await expect(tx) .to.emit(accountHandler, "RoyaltyRecipientsChanged") .withArgs( seller.id, - compareRoyaltyRecipientLists.bind(expectedRoyaltyRecipientList.toStruct()), + compareRoyaltyRecipientInfoLists.bind(expectedRoyaltyRecipientInfoList.toStruct()), assistant.address ); @@ -7732,15 +7732,15 @@ describe("IBosonOrchestrationHandler", function () { await assistant.getAddress() ); - const expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); await expect(tx) .to.emit(accountHandler, "RoyaltyRecipientsChanged") .withArgs( seller.id, - compareRoyaltyRecipientLists.bind(expectedRoyaltyRecipientList.toStruct()), + compareRoyaltyRecipientInfoLists.bind(expectedRoyaltyRecipientInfoList.toStruct()), assistant.address ); @@ -8152,15 +8152,15 @@ describe("IBosonOrchestrationHandler", function () { await assistant.getAddress() ); - const expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); await expect(tx) .to.emit(accountHandler, "RoyaltyRecipientsChanged") .withArgs( seller.id, - compareRoyaltyRecipientLists.bind(expectedRoyaltyRecipientList.toStruct()), + compareRoyaltyRecipientInfoLists.bind(expectedRoyaltyRecipientInfoList.toStruct()), assistant.address ); @@ -8651,15 +8651,15 @@ describe("IBosonOrchestrationHandler", function () { await assistant.getAddress() ); - const expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); await expect(tx) .to.emit(accountHandler, "RoyaltyRecipientsChanged") .withArgs( seller.id, - compareRoyaltyRecipientLists.bind(expectedRoyaltyRecipientList.toStruct()), + compareRoyaltyRecipientInfoLists.bind(expectedRoyaltyRecipientInfoList.toStruct()), assistant.address ); @@ -8793,15 +8793,15 @@ describe("IBosonOrchestrationHandler", function () { await assistant.getAddress() ); - const expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); await expect(tx) .to.emit(accountHandler, "RoyaltyRecipientsChanged") .withArgs( seller.id, - compareRoyaltyRecipientLists.bind(expectedRoyaltyRecipientList.toStruct()), + compareRoyaltyRecipientInfoLists.bind(expectedRoyaltyRecipientInfoList.toStruct()), assistant.address ); diff --git a/test/protocol/ProtocolInitializationHandlerTest.js b/test/protocol/ProtocolInitializationHandlerTest.js index cb9392eed..68e045642 100644 --- a/test/protocol/ProtocolInitializationHandlerTest.js +++ b/test/protocol/ProtocolInitializationHandlerTest.js @@ -36,7 +36,7 @@ const { deployProtocolClients } = require("../../scripts/util/deploy-protocol-cl const TokenType = require("../../scripts/domain/TokenType"); const { getStorageAt, setStorageAt } = require("@nomicfoundation/hardhat-network-helpers"); const { RoyaltyInfo } = require("../../scripts/domain/RoyaltyInfo.js"); -const { RoyaltyRecipientList, RoyaltyRecipient } = require("../../scripts/domain/RoyaltyRecipient.js"); +const { RoyaltyRecipientInfoList, RoyaltyRecipientInfo } = require("../../scripts/domain/RoyaltyRecipientInfo.js"); describe("ProtocolInitializationHandler", async function () { // Common vars @@ -1001,7 +1001,7 @@ describe("ProtocolInitializationHandler", async function () { context("Data backfilling", async function () { let accountHandler, offerHandler; - let expectedRoyaltyRecipientLists, expectedRoyaltyInfo; + let expectedRoyaltyRecipientInfoLists, expectedRoyaltyInfo; beforeEach(async function () { const protocolAddress = await diamondCutFacet.getAddress(); @@ -1075,10 +1075,10 @@ describe("ProtocolInitializationHandler", async function () { [royaltyPercentages, sellerIds, offerIds, ZeroAddress] ); - expectedRoyaltyRecipientLists = [ - new RoyaltyRecipientList([new RoyaltyRecipient(ZeroAddress, "0", "Treasury")]), - new RoyaltyRecipientList([new RoyaltyRecipient(ZeroAddress, "1400", "Treasury")]), - new RoyaltyRecipientList([new RoyaltyRecipient(ZeroAddress, "1400", "Treasury")]), + expectedRoyaltyRecipientInfoLists = [ + new RoyaltyRecipientInfoList([new RoyaltyRecipientInfo(ZeroAddress, "0")]), + new RoyaltyRecipientInfoList([new RoyaltyRecipientInfo(ZeroAddress, "1400")]), + new RoyaltyRecipientInfoList([new RoyaltyRecipientInfo(ZeroAddress, "1400")]), ]; expectedRoyaltyInfo = [ @@ -1111,10 +1111,10 @@ describe("ProtocolInitializationHandler", async function () { // Validate that the royalty recipients are set correctly for (let sellerId = 1; sellerId <= 3; sellerId++) { - const returnedRoyaltyRecipientStruct = RoyaltyRecipientList.fromStruct( + const returnedRoyaltyRecipientInfoStruct = RoyaltyRecipientInfoList.fromStruct( await accountHandler.getRoyaltyRecipients(sellerId) ); - expect(returnedRoyaltyRecipientStruct).to.deep.equal(expectedRoyaltyRecipientLists[sellerId - 1]); + expect(returnedRoyaltyRecipientInfoStruct).to.deep.equal(expectedRoyaltyRecipientInfoLists[sellerId - 1]); } // Validate that the royalty info is set correctly @@ -1143,10 +1143,10 @@ describe("ProtocolInitializationHandler", async function () { // Validate that the royalty recipients are set correctly for (let sellerId = 1; sellerId <= 3; sellerId++) { - const returnedRoyaltyRecipientStruct = RoyaltyRecipientList.fromStruct( + const returnedRoyaltyRecipientInfoStruct = RoyaltyRecipientInfoList.fromStruct( await accountHandler.getRoyaltyRecipients(sellerId) ); - expect(returnedRoyaltyRecipientStruct).to.deep.equal(expectedRoyaltyRecipientLists[sellerId - 1]); + expect(returnedRoyaltyRecipientInfoStruct).to.deep.equal(expectedRoyaltyRecipientInfoLists[sellerId - 1]); } // Validate that the royalty info is set correctly diff --git a/test/protocol/SellerHandlerTest.js b/test/protocol/SellerHandlerTest.js index 4ac5fd0d2..6129e7357 100644 --- a/test/protocol/SellerHandlerTest.js +++ b/test/protocol/SellerHandlerTest.js @@ -6,7 +6,7 @@ const AuthToken = require("../../scripts/domain/AuthToken"); const AuthTokenType = require("../../scripts/domain/AuthTokenType"); const SellerUpdateFields = require("../../scripts/domain/SellerUpdateFields"); const PausableRegion = require("../../scripts/domain/PausableRegion.js"); -const { RoyaltyRecipient, RoyaltyRecipientList } = require("../../scripts/domain/RoyaltyRecipient.js"); +const { RoyaltyRecipientInfo, RoyaltyRecipientInfoList } = require("../../scripts/domain/RoyaltyRecipientInfo.js"); const { RevertReasons } = require("../../scripts/config/revert-reasons.js"); const { calculateCloneAddress, @@ -18,9 +18,9 @@ const { paddingType, getSellerSalt, getEvent, - compareRoyaltyRecipientLists, + compareRoyaltyRecipientInfoLists, } = require("../util/utils.js"); -const { VOUCHER_NAME, VOUCHER_SYMBOL, DEFAULT_ROYALTY_RECIPIENT } = require("../util/constants"); +const { VOUCHER_NAME, VOUCHER_SYMBOL } = 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"); @@ -184,15 +184,15 @@ describe("SellerHandler", function () { await expect(tx).to.emit(bosonVoucher, "ContractURIChanged").withArgs(contractURI); - const expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); await expect(tx) .to.emit(accountHandler, "RoyaltyRecipientsChanged") .withArgs( seller.id, - compareRoyaltyRecipientLists.bind(expectedRoyaltyRecipientList.toStruct()), + compareRoyaltyRecipientInfoLists.bind(expectedRoyaltyRecipientInfoList.toStruct()), admin.address ); @@ -220,15 +220,15 @@ describe("SellerHandler", function () { await expect(tx).to.emit(bosonVoucher, "ContractURIChanged").withArgs(contractURI); - const expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); await expect(tx) .to.emit(accountHandler, "RoyaltyRecipientsChanged") .withArgs( seller.id, - compareRoyaltyRecipientLists.bind(expectedRoyaltyRecipientList.toStruct()), + compareRoyaltyRecipientInfoLists.bind(expectedRoyaltyRecipientInfoList.toStruct()), admin.address ); @@ -270,13 +270,16 @@ describe("SellerHandler", function () { expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); // Default royalty recipient is set - const expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); - const royaltyRecipientList = RoyaltyRecipientList.fromStruct( + const royaltyRecipientInfoList = RoyaltyRecipientInfoList.fromStruct( await accountHandler.connect(rando).getRoyaltyRecipients(seller.id) ); - expect(royaltyRecipientList).to.deep.equal(expectedRoyaltyRecipientList, "Default royalty recipient mismatch"); + expect(royaltyRecipientInfoList).to.deep.equal( + expectedRoyaltyRecipientInfoList, + "Default royalty recipient mismatch" + ); // Voucher clone contract bosonVoucher = await getContractAt("OwnableUpgradeable", expectedCloneAddress); @@ -344,13 +347,16 @@ describe("SellerHandler", function () { await accountHandler.connect(admin).createSeller(seller, emptyAuthToken, voucherInitValues); // Default royalty recipient is set - const expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); - const royaltyRecipientList = RoyaltyRecipientList.fromStruct( + const royaltyRecipientInfoList = RoyaltyRecipientInfoList.fromStruct( await accountHandler.connect(rando).getRoyaltyRecipients(seller.id) ); - expect(royaltyRecipientList).to.deep.equal(expectedRoyaltyRecipientList, "Default royalty recipient mismatch"); + expect(royaltyRecipientInfoList).to.deep.equal( + expectedRoyaltyRecipientInfoList, + "Default royalty recipient mismatch" + ); bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); @@ -415,13 +421,16 @@ describe("SellerHandler", function () { expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); // Default royalty recipient is set - const expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); - const royaltyRecipientList = RoyaltyRecipientList.fromStruct( + const royaltyRecipientInfoList = RoyaltyRecipientInfoList.fromStruct( await accountHandler.connect(rando).getRoyaltyRecipients(seller.id) ); - expect(royaltyRecipientList).to.deep.equal(expectedRoyaltyRecipientList, "Default royalty recipient mismatch"); + expect(royaltyRecipientInfoList).to.deep.equal( + expectedRoyaltyRecipientInfoList, + "Default royalty recipient mismatch" + ); // Voucher clone contract bosonVoucher = await getContractAt("OwnableUpgradeable", expectedCloneAddress); @@ -682,8 +691,8 @@ describe("SellerHandler", function () { voucherInitValues.collectionSalt ); seller.id = Number(seller.id) + 1; - const defaultRoyaltyRecipients = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const defaultRoyaltyRecipientInfos = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); const tx = await accountHandler.connect(admin).createSeller(seller, emptyAuthToken, voucherInitValues); @@ -693,7 +702,11 @@ describe("SellerHandler", function () { await expect(tx) .to.emit(accountHandler, "RoyaltyRecipientsChanged") - .withArgs(seller.id, compareRoyaltyRecipientLists.bind(defaultRoyaltyRecipients.toStruct()), admin.address); + .withArgs( + seller.id, + compareRoyaltyRecipientInfoLists.bind(defaultRoyaltyRecipientInfos.toStruct()), + admin.address + ); // Voucher clone contract bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); @@ -1650,13 +1663,16 @@ describe("SellerHandler", function () { } // Default royalty recipient is updated - const expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); - const royaltyRecipientList = RoyaltyRecipientList.fromStruct( + const royaltyRecipientInfoList = RoyaltyRecipientInfoList.fromStruct( await accountHandler.connect(rando).getRoyaltyRecipients(seller.id) ); - expect(royaltyRecipientList).to.deep.equal(expectedRoyaltyRecipientList, "Default royalty recipient mismatch"); + expect(royaltyRecipientInfoList).to.deep.equal( + expectedRoyaltyRecipientInfoList, + "Default royalty recipient mismatch" + ); //Check that old addresses are no longer mapped. We don't map the treasury address. [exists] = await accountHandler.connect(rando).getSellerByAddress(await assistant.getAddress()); @@ -2081,13 +2097,16 @@ describe("SellerHandler", function () { seller2.admin = other1.address; // Default royalty recipient is set - let expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + let expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); - let royaltyRecipientList = RoyaltyRecipientList.fromStruct( + let royaltyRecipientInfoList = RoyaltyRecipientInfoList.fromStruct( await accountHandler.connect(rando).getRoyaltyRecipients(seller.id) ); - expect(royaltyRecipientList).to.deep.equal(expectedRoyaltyRecipientList, "Default royalty recipient mismatch"); + expect(royaltyRecipientInfoList).to.deep.equal( + expectedRoyaltyRecipientInfoList, + "Default royalty recipient mismatch" + ); // Create seller 2 await accountHandler.connect(other1).createSeller(seller2, emptyAuthToken, voucherInitValues); @@ -2102,13 +2121,16 @@ describe("SellerHandler", function () { expect(returnedSeller2.treasury).to.equal(await treasury.getAddress()); // Default royalty recipient is set - expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), + expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), ]); - royaltyRecipientList = RoyaltyRecipientList.fromStruct( + royaltyRecipientInfoList = RoyaltyRecipientInfoList.fromStruct( await accountHandler.connect(rando).getRoyaltyRecipients(seller.id) ); - expect(royaltyRecipientList).to.deep.equal(expectedRoyaltyRecipientList, "Default royalty recipient mismatch"); + expect(royaltyRecipientInfoList).to.deep.equal( + expectedRoyaltyRecipientInfoList, + "Default royalty recipient mismatch" + ); }); it("should be possible to use the same address for assistant, admin and treasury", async function () { @@ -2234,38 +2256,44 @@ describe("SellerHandler", function () { it("update treasury, when it's already one of the royalty recipients", async function () { // add some royalty recipients - const newRoyaltyRecipients = new RoyaltyRecipientList([ - new RoyaltyRecipient(other1.address, "100", "other1"), - new RoyaltyRecipient(other2.address, "200", "other2"), - new RoyaltyRecipient(other3.address, "300", "other3"), + const newRoyaltyRecipientInfos = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other1.address, "100"), + new RoyaltyRecipientInfo(other2.address, "200"), + new RoyaltyRecipientInfo(other3.address, "300"), ]); - await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, newRoyaltyRecipients.toStruct()); + await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, newRoyaltyRecipientInfos.toStruct()); // Default royalty recipient is set - let expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), - ...newRoyaltyRecipients.royaltyRecipients, + let expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), + ...newRoyaltyRecipientInfos.royaltyRecipientInfos, ]); - let royaltyRecipientList = RoyaltyRecipientList.fromStruct( + let royaltyRecipientInfoList = RoyaltyRecipientInfoList.fromStruct( await accountHandler.connect(rando).getRoyaltyRecipients(seller.id) ); - expect(royaltyRecipientList).to.deep.equal(expectedRoyaltyRecipientList, "Royalty recipient list mismatch"); + expect(royaltyRecipientInfoList).to.deep.equal( + expectedRoyaltyRecipientInfoList, + "Royalty recipient list mismatch" + ); // Update seller's treasury seller.treasury = other1.address; await accountHandler.connect(assistant).updateSeller(seller, emptyAuthToken); // Default royalty recipient is set - expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), - new RoyaltyRecipient(other3.address, "300", "other3"), - new RoyaltyRecipient(other2.address, "200", "other2"), + expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), + new RoyaltyRecipientInfo(other3.address, "300"), + new RoyaltyRecipientInfo(other2.address, "200"), ]); - royaltyRecipientList = RoyaltyRecipientList.fromStruct( + royaltyRecipientInfoList = RoyaltyRecipientInfoList.fromStruct( await accountHandler.connect(rando).getRoyaltyRecipients(seller.id) ); - expect(royaltyRecipientList).to.deep.equal(expectedRoyaltyRecipientList, "Royalty recipient list mismatch"); + expect(royaltyRecipientInfoList).to.deep.equal( + expectedRoyaltyRecipientInfoList, + "Royalty recipient list mismatch" + ); }); context("💔 Revert Reasons", async function () { @@ -3840,23 +3868,23 @@ describe("SellerHandler", function () { }); context("Royalty Recipients", async function () { - let royaltyRecipientList, royaltyRecipientListStruct; - let expectedRoyaltyRecipientList; + let royaltyRecipientInfoList, royaltyRecipientInfoListStruct; + let expectedRoyaltyRecipientInfoList; beforeEach(async function () { await accountHandler.connect(admin).createSeller(seller, emptyAuthToken, voucherInitValues); - royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(other1.address, "100", "other1"), - new RoyaltyRecipient(other2.address, "200", "other2"), - new RoyaltyRecipient(other3.address, "300", "other3"), + royaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other1.address, "100"), + new RoyaltyRecipientInfo(other2.address, "200"), + new RoyaltyRecipientInfo(other3.address, "300"), ]); - royaltyRecipientListStruct = royaltyRecipientList.toStruct(); + royaltyRecipientInfoListStruct = royaltyRecipientInfoList.toStruct(); - expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), - ...royaltyRecipientList.royaltyRecipients, + expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), + ...royaltyRecipientInfoList.royaltyRecipientInfos, ]); }); @@ -3865,64 +3893,64 @@ describe("SellerHandler", function () { // Add royalty recipients const tx = await accountHandler .connect(admin) - .addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); + .addRoyaltyRecipients(seller.id, royaltyRecipientInfoList.toStruct()); const event = getEvent(await tx.wait(), accountHandler, "RoyaltyRecipientsChanged"); - const returnedRecipientList = RoyaltyRecipientList.fromStruct(event.royaltyRecipients); + const returnedRecipientList = RoyaltyRecipientInfoList.fromStruct(event.royaltyRecipients); expect(event.sellerId).to.equal(seller.id); expect(event.executedBy).to.equal(admin.address); - expect(returnedRecipientList).to.deep.equal(expectedRoyaltyRecipientList); + expect(returnedRecipientList).to.deep.equal(expectedRoyaltyRecipientInfoList); }); it("should update state", async function () { // Add royalty recipients - await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientListStruct); + await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientInfoListStruct); - const returnedRoyaltyRecipientList = RoyaltyRecipientList.fromStruct( + const returnedRoyaltyRecipientInfoList = RoyaltyRecipientInfoList.fromStruct( await accountHandler.connect(rando).getRoyaltyRecipients(seller.id) ); - expect(returnedRoyaltyRecipientList).to.deep.equal( - expectedRoyaltyRecipientList, + expect(returnedRoyaltyRecipientInfoList).to.deep.equal( + expectedRoyaltyRecipientInfoList, "Royalty recipient mismatch" ); }); it("correctly handle treasury during the seller update", async function () { // Add royalty recipients - await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); + await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientInfoList.toStruct()); // Update the seller, so one of the recipients is removed seller.treasury = other1.address; await accountHandler.connect(admin).updateSeller(seller, emptyAuthToken); // other1 is not a recipient anymore - let returnedRoyaltyRecipientList = RoyaltyRecipientList.fromStruct( + let returnedRoyaltyRecipientInfoList = RoyaltyRecipientInfoList.fromStruct( await accountHandler.connect(rando).getRoyaltyRecipients(seller.id) ); - royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(other3.address, "300", "other3"), - new RoyaltyRecipient(other2.address, "200", "other2"), + royaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other3.address, "300"), + new RoyaltyRecipientInfo(other2.address, "200"), ]); - expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), - ...royaltyRecipientList.royaltyRecipients, + expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), + ...royaltyRecipientInfoList.royaltyRecipientInfos, ]); - expect(returnedRoyaltyRecipientList).to.deep.equal( - expectedRoyaltyRecipientList, + expect(returnedRoyaltyRecipientInfoList).to.deep.equal( + expectedRoyaltyRecipientInfoList, "Royalty recipient mismatch" ); // other 1 now cannot be added as another recipient - royaltyRecipientList = new RoyaltyRecipientList([new RoyaltyRecipient(other1.address, "100", "other1")]); + royaltyRecipientInfoList = new RoyaltyRecipientInfoList([new RoyaltyRecipientInfo(other1.address, "100")]); // Adding other 1 should fail await expect( - accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()) + accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientInfoList.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.RECIPIENT_NOT_UNIQUE); // Update the seller again, so other 1 can later be added as a recipient @@ -3930,26 +3958,26 @@ describe("SellerHandler", function () { await accountHandler.connect(admin).updateSeller(seller, emptyAuthToken); // Now adding should succeed - await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); + await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientInfoList.toStruct()); // other1 is back on the list - returnedRoyaltyRecipientList = RoyaltyRecipientList.fromStruct( + returnedRoyaltyRecipientInfoList = RoyaltyRecipientInfoList.fromStruct( await accountHandler.connect(rando).getRoyaltyRecipients(seller.id) ); - royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(other3.address, "300", "other3"), - new RoyaltyRecipient(other2.address, "200", "other2"), - new RoyaltyRecipient(other1.address, "100", "other1"), + royaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other3.address, "300"), + new RoyaltyRecipientInfo(other2.address, "200"), + new RoyaltyRecipientInfo(other1.address, "100"), ]); - expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), - ...royaltyRecipientList.royaltyRecipients, + expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), + ...royaltyRecipientInfoList.royaltyRecipientInfos, ]); - expect(returnedRoyaltyRecipientList).to.deep.equal( - expectedRoyaltyRecipientList, + expect(returnedRoyaltyRecipientInfoList).to.deep.equal( + expectedRoyaltyRecipientInfoList, "Royalty recipient mismatch" ); }); @@ -3961,7 +3989,7 @@ describe("SellerHandler", function () { // Attempt to add royalty recipients expecting revert await expect( - accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientListStruct) + accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientInfoListStruct) ).to.revertedWithCustomError(bosonErrors, RevertReasons.REGION_PAUSED); }); @@ -3970,14 +3998,14 @@ describe("SellerHandler", function () { // Attempt to add royalty recipients expecting revert await expect( - accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientListStruct) + accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientInfoListStruct) ).to.revertedWithCustomError(bosonErrors, RevertReasons.NO_SUCH_SELLER); }); it("caller is not the seller admin", async function () { // Attempt to add royalty recipients expecting revert await expect( - accountHandler.connect(rando).addRoyaltyRecipients(seller.id, royaltyRecipientListStruct) + accountHandler.connect(rando).addRoyaltyRecipients(seller.id, royaltyRecipientInfoListStruct) ).to.revertedWithCustomError(bosonErrors, RevertReasons.NOT_ADMIN); }); @@ -3989,81 +4017,81 @@ describe("SellerHandler", function () { // Attempt to add royalty recipients expecting revert await expect( - accountHandler.connect(rando).addRoyaltyRecipients(seller.id, royaltyRecipientListStruct) + accountHandler.connect(rando).addRoyaltyRecipients(seller.id, royaltyRecipientInfoListStruct) ).to.revertedWithCustomError(bosonErrors, RevertReasons.NOT_ADMIN); }); it("some recipient is not unique - same tx", async function () { // Make duplicate entry - royaltyRecipientList.royaltyRecipients[2].wallet = other1.address; + royaltyRecipientInfoList.royaltyRecipientInfos[2].wallet = other1.address; // Attempt to add royalty recipients expecting revert await expect( - accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()) + accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientInfoList.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.RECIPIENT_NOT_UNIQUE); }); it("some recipient is not unique - two txs", async function () { // Successfully add recipients - await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientListStruct); + await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientInfoListStruct); // Attempt to add royalty recipients expecting revert await expect( - accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientListStruct) + accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientInfoListStruct) ).to.revertedWithCustomError(bosonErrors, RevertReasons.RECIPIENT_NOT_UNIQUE); }); it("The default recipient cannot be added to the list", async function () { // Make duplicate entry - royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, "100", "duplicateDefault"), - ]); + royaltyRecipientInfoList = new RoyaltyRecipientInfoList([new RoyaltyRecipientInfo(ZeroAddress, "100")]); // Attempt to add royalty recipients expecting revert await expect( - accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()) + accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientInfoList.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.RECIPIENT_NOT_UNIQUE); }); it("the treasury address cannot be added to the list", async function () { // Make duplicate entry - royaltyRecipientList = new RoyaltyRecipientList([new RoyaltyRecipient(treasury, "100", "treasury")]); + royaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(treasury, "100", "treasury"), + ]); // Attempt to add royalty recipients expecting revert await expect( - accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()) + accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientInfoList.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.RECIPIENT_NOT_UNIQUE); }); it("some royalty percentage is above the limit", async function () { - royaltyRecipientList.royaltyRecipients[0].minRoyaltyPercentage = "8000"; + royaltyRecipientInfoList.royaltyRecipientInfos[0].minRoyaltyPercentage = "8000"; // Attempt to add royalty recipients expecting revert await expect( - accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()) + accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientInfoList.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.INVALID_ROYALTY_PERCENTAGE); }); }); }); context("👉 updateRoyaltyRecipients()", async function () { - let royaltyRecipientIds, royaltyRecipientListUpdates; + let royaltyRecipientInfoIds, royaltyRecipientInfoListUpdates; beforeEach(async function () { // add first set of royalty recipients - await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); + await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientInfoList.toStruct()); // update data - royaltyRecipientIds = [1, 0, 3]; - royaltyRecipientListUpdates = new RoyaltyRecipientList([ - new RoyaltyRecipient(other4.address, "400", "other1"), // change address and percentage, keep name - new RoyaltyRecipient(ZeroAddress, "100", "itisme"), // change external id of default recipient - new RoyaltyRecipient(other3.address, "300", "other3"), // change nothing + royaltyRecipientInfoIds = [1, 0, 3]; + royaltyRecipientInfoListUpdates = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other4.address, "400"), // change address and percentage, keep name + new RoyaltyRecipientInfo(ZeroAddress, "150"), // change percentage of default recipient + new RoyaltyRecipientInfo(other3.address, "300"), // change nothing ]); - expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, "100", "itisme"), - new RoyaltyRecipient(other4.address, "400", "other1"), - new RoyaltyRecipient(other2.address, "200", "other2"), - new RoyaltyRecipient(other3.address, "300", "other3"), + expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, "150"), + new RoyaltyRecipientInfo(other4.address, "400"), + new RoyaltyRecipientInfo(other2.address, "200"), + new RoyaltyRecipientInfo(other3.address, "300"), ]); }); @@ -4071,28 +4099,28 @@ describe("SellerHandler", function () { // Update royalty recipients const tx = await accountHandler .connect(admin) - .updateRoyaltyRecipients(seller.id, royaltyRecipientIds, royaltyRecipientListUpdates.toStruct()); + .updateRoyaltyRecipients(seller.id, royaltyRecipientInfoIds, royaltyRecipientInfoListUpdates.toStruct()); const event = getEvent(await tx.wait(), accountHandler, "RoyaltyRecipientsChanged"); - const returnedRecipientList = RoyaltyRecipientList.fromStruct(event.royaltyRecipients); + const returnedRecipientList = RoyaltyRecipientInfoList.fromStruct(event.royaltyRecipients); expect(event.sellerId).to.equal(seller.id); expect(event.executedBy).to.equal(admin.address); - expect(returnedRecipientList).to.deep.equal(expectedRoyaltyRecipientList); + expect(returnedRecipientList).to.deep.equal(expectedRoyaltyRecipientInfoList); }); it("should update state", async function () { // Update royalty recipients await accountHandler .connect(admin) - .updateRoyaltyRecipients(seller.id, royaltyRecipientIds, royaltyRecipientListUpdates.toStruct()); + .updateRoyaltyRecipients(seller.id, royaltyRecipientInfoIds, royaltyRecipientInfoListUpdates.toStruct()); - const returnedRoyaltyRecipientList = RoyaltyRecipientList.fromStruct( + const returnedRoyaltyRecipientInfoList = RoyaltyRecipientInfoList.fromStruct( await accountHandler.connect(rando).getRoyaltyRecipients(seller.id) ); - expect(returnedRoyaltyRecipientList).to.deep.equal( - expectedRoyaltyRecipientList, + expect(returnedRoyaltyRecipientInfoList).to.deep.equal( + expectedRoyaltyRecipientInfoList, "Default royalty recipient mismatch" ); }); @@ -4106,7 +4134,7 @@ describe("SellerHandler", function () { await expect( accountHandler .connect(admin) - .updateRoyaltyRecipients(seller.id, royaltyRecipientIds, royaltyRecipientListUpdates.toStruct()) + .updateRoyaltyRecipients(seller.id, royaltyRecipientInfoIds, royaltyRecipientInfoListUpdates.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.REGION_PAUSED); }); @@ -4117,7 +4145,7 @@ describe("SellerHandler", function () { await expect( accountHandler .connect(admin) - .updateRoyaltyRecipients(seller.id, royaltyRecipientIds, royaltyRecipientListUpdates.toStruct()) + .updateRoyaltyRecipients(seller.id, royaltyRecipientInfoIds, royaltyRecipientInfoListUpdates.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.NO_SUCH_SELLER); }); @@ -4126,7 +4154,7 @@ describe("SellerHandler", function () { await expect( accountHandler .connect(rando) - .updateRoyaltyRecipients(seller.id, royaltyRecipientIds, royaltyRecipientListUpdates.toStruct()) + .updateRoyaltyRecipients(seller.id, royaltyRecipientInfoIds, royaltyRecipientInfoListUpdates.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.NOT_ADMIN); }); @@ -4140,162 +4168,162 @@ describe("SellerHandler", function () { await expect( accountHandler .connect(rando) - .updateRoyaltyRecipients(seller.id, royaltyRecipientIds, royaltyRecipientListUpdates.toStruct()) + .updateRoyaltyRecipients(seller.id, royaltyRecipientInfoIds, royaltyRecipientInfoListUpdates.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.NOT_ADMIN); }); it("length of ids to change does not match length of new values", async function () { - royaltyRecipientIds.push(2); + royaltyRecipientInfoIds.push(2); // Attempt to update royalty recipients expecting revert await expect( accountHandler .connect(admin) - .updateRoyaltyRecipients(seller.id, royaltyRecipientIds, royaltyRecipientListUpdates.toStruct()) + .updateRoyaltyRecipients(seller.id, royaltyRecipientInfoIds, royaltyRecipientInfoListUpdates.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.ARRAY_LENGTH_MISMATCH); }); it("id to update does not exist", async function () { - royaltyRecipientIds[0] = 1234; + royaltyRecipientInfoIds[0] = 1234; // Attempt to update royalty recipients expecting revert await expect( accountHandler .connect(admin) - .updateRoyaltyRecipients(seller.id, royaltyRecipientIds, royaltyRecipientListUpdates.toStruct()) + .updateRoyaltyRecipients(seller.id, royaltyRecipientInfoIds, royaltyRecipientInfoListUpdates.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.INVALID_ROYALTY_RECIPIENT_ID); }); it("seller tries to update the address of default recipient", async function () { - royaltyRecipientIds = [0]; - royaltyRecipientListUpdates = new RoyaltyRecipientList([ - new RoyaltyRecipient(other5.address, "100", "treasury"), // try to change default recipient address + royaltyRecipientInfoIds = [0]; + royaltyRecipientInfoListUpdates = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other5.address, "100"), // try to change default recipient address ]); // Attempt to update royalty recipients expecting revert await expect( accountHandler .connect(admin) - .updateRoyaltyRecipients(seller.id, royaltyRecipientIds, royaltyRecipientListUpdates.toStruct()) + .updateRoyaltyRecipients(seller.id, royaltyRecipientInfoIds, royaltyRecipientInfoListUpdates.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.WRONG_DEFAULT_RECIPIENT); }); it("some recipient is not unique - same tx", async function () { // Make duplicate entry - royaltyRecipientIds = [1, 2]; - royaltyRecipientListUpdates = new RoyaltyRecipientList([ - new RoyaltyRecipient(other5.address, "400", "other1"), - new RoyaltyRecipient(other5.address, "300", "other3"), + royaltyRecipientInfoIds = [1, 2]; + royaltyRecipientInfoListUpdates = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other5.address, "400"), + new RoyaltyRecipientInfo(other5.address, "300"), ]); // Attempt to update royalty recipients expecting revert await expect( accountHandler .connect(admin) - .updateRoyaltyRecipients(seller.id, royaltyRecipientIds, royaltyRecipientListUpdates.toStruct()) + .updateRoyaltyRecipients(seller.id, royaltyRecipientInfoIds, royaltyRecipientInfoListUpdates.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.RECIPIENT_NOT_UNIQUE); }); it("some recipient is not unique - two txs", async function () { - royaltyRecipientListUpdates.royaltyRecipients[0].wallet = other3.address; // address was already added to different recipientid + royaltyRecipientInfoListUpdates.royaltyRecipientInfos[0].wallet = other3.address; // address was already added to different recipientid // Attempt to update royalty recipients expecting revert await expect( accountHandler .connect(admin) - .updateRoyaltyRecipients(seller.id, royaltyRecipientIds, royaltyRecipientListUpdates.toStruct()) + .updateRoyaltyRecipients(seller.id, royaltyRecipientInfoIds, royaltyRecipientInfoListUpdates.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.RECIPIENT_NOT_UNIQUE); }); it("the default recipient cannot be added to the list", async function () { - royaltyRecipientListUpdates = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, "100", "duplicateDefault"), + royaltyRecipientInfoListUpdates = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, "100"), ]); - royaltyRecipientIds = [2]; + royaltyRecipientInfoIds = [2]; // Attempt to update royalty recipients expecting revert await expect( accountHandler .connect(admin) - .updateRoyaltyRecipients(seller.id, royaltyRecipientIds, royaltyRecipientListUpdates.toStruct()) + .updateRoyaltyRecipients(seller.id, royaltyRecipientInfoIds, royaltyRecipientInfoListUpdates.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.RECIPIENT_NOT_UNIQUE); }); it("the treasury address cannot be added to the list", async function () { - royaltyRecipientListUpdates = new RoyaltyRecipientList([ - new RoyaltyRecipient(treasury.address, "100", "treasury"), + royaltyRecipientInfoListUpdates = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(treasury.address, "100"), ]); - royaltyRecipientIds = [1]; + royaltyRecipientInfoIds = [1]; // Attempt to update royalty recipients expecting revert await expect( accountHandler .connect(admin) - .updateRoyaltyRecipients(seller.id, royaltyRecipientIds, royaltyRecipientListUpdates.toStruct()) + .updateRoyaltyRecipients(seller.id, royaltyRecipientInfoIds, royaltyRecipientInfoListUpdates.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.RECIPIENT_NOT_UNIQUE); }); it("some royalty percentage is above the limit", async function () { - royaltyRecipientListUpdates.royaltyRecipients[2].minRoyaltyPercentage = "8000"; + royaltyRecipientInfoListUpdates.royaltyRecipientInfos[2].minRoyaltyPercentage = "8000"; // Attempt to update royalty recipients expecting revert await expect( accountHandler .connect(admin) - .updateRoyaltyRecipients(seller.id, royaltyRecipientIds, royaltyRecipientListUpdates.toStruct()) + .updateRoyaltyRecipients(seller.id, royaltyRecipientInfoIds, royaltyRecipientInfoListUpdates.toStruct()) ).to.revertedWithCustomError(bosonErrors, RevertReasons.INVALID_ROYALTY_PERCENTAGE); }); }); }); context("👉 removeRoyaltyRecipients()", async function () { - let royaltyRecipientIds; + let royaltyRecipientInfoIds; beforeEach(async function () { - royaltyRecipientList = new RoyaltyRecipientList([ - ...royaltyRecipientList.royaltyRecipients, - new RoyaltyRecipient(other4.address, "400", "other4"), - new RoyaltyRecipient(other5.address, "500", "other5"), - new RoyaltyRecipient(other6.address, "600", "other6"), + royaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + ...royaltyRecipientInfoList.royaltyRecipientInfos, + new RoyaltyRecipientInfo(other4.address, "400"), + new RoyaltyRecipientInfo(other5.address, "500"), + new RoyaltyRecipientInfo(other6.address, "600"), ]); // add first set of royalty recipients - await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); + await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientInfoList.toStruct()); // ids to remove - royaltyRecipientIds = [1, 3, 4, 6]; + royaltyRecipientInfoIds = [1, 3, 4, 6]; // Removal process: [0,1,2,3,4,5,6]->[0,1,2,3,4,5]->[0,1,2,3,5]->[0,1,2,5]->[0,5,2] - expectedRoyaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(ZeroAddress, voucherInitValues.royaltyPercentage, DEFAULT_ROYALTY_RECIPIENT), // default - royaltyRecipientList.royaltyRecipients[4], - royaltyRecipientList.royaltyRecipients[1], + expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, voucherInitValues.royaltyPercentage), // default + royaltyRecipientInfoList.royaltyRecipientInfos[4], + royaltyRecipientInfoList.royaltyRecipientInfos[1], ]); }); it("should emit RoyalRecipientsChanged event", async function () { // Remove royalty recipients - const tx = await accountHandler.connect(admin).removeRoyaltyRecipients(seller.id, royaltyRecipientIds); + const tx = await accountHandler.connect(admin).removeRoyaltyRecipients(seller.id, royaltyRecipientInfoIds); const event = getEvent(await tx.wait(), accountHandler, "RoyaltyRecipientsChanged"); - const returnedRecipientList = RoyaltyRecipientList.fromStruct(event.royaltyRecipients); + const returnedRecipientList = RoyaltyRecipientInfoList.fromStruct(event.royaltyRecipients); expect(event.sellerId).to.equal(seller.id); expect(event.executedBy).to.equal(admin.address); - expect(returnedRecipientList).to.deep.equal(expectedRoyaltyRecipientList); + expect(returnedRecipientList).to.deep.equal(expectedRoyaltyRecipientInfoList); }); it("should update state", async function () { // Remove royalty recipients - await accountHandler.connect(admin).removeRoyaltyRecipients(seller.id, royaltyRecipientIds); + await accountHandler.connect(admin).removeRoyaltyRecipients(seller.id, royaltyRecipientInfoIds); - const returnedRoyaltyRecipientList = RoyaltyRecipientList.fromStruct( + const returnedRoyaltyRecipientInfoList = RoyaltyRecipientInfoList.fromStruct( await accountHandler.connect(rando).getRoyaltyRecipients(seller.id) ); - expect(returnedRoyaltyRecipientList).to.deep.equal( - expectedRoyaltyRecipientList, + expect(returnedRoyaltyRecipientInfoList).to.deep.equal( + expectedRoyaltyRecipientInfoList, "Default royalty recipient mismatch" ); }); @@ -4307,7 +4335,7 @@ describe("SellerHandler", function () { // Attempt to remove royalty recipients expecting revert await expect( - accountHandler.connect(admin).removeRoyaltyRecipients(seller.id, royaltyRecipientIds) + accountHandler.connect(admin).removeRoyaltyRecipients(seller.id, royaltyRecipientInfoIds) ).to.revertedWithCustomError(bosonErrors, RevertReasons.REGION_PAUSED); }); @@ -4316,14 +4344,14 @@ describe("SellerHandler", function () { // Attempt to remove royalty recipients expecting revert await expect( - accountHandler.connect(admin).removeRoyaltyRecipients(seller.id, royaltyRecipientIds) + accountHandler.connect(admin).removeRoyaltyRecipients(seller.id, royaltyRecipientInfoIds) ).to.revertedWithCustomError(bosonErrors, RevertReasons.NO_SUCH_SELLER); }); it("caller is not the seller admin", async function () { // Attempt to remove royalty recipients expecting revert await expect( - accountHandler.connect(rando).removeRoyaltyRecipients(seller.id, royaltyRecipientIds) + accountHandler.connect(rando).removeRoyaltyRecipients(seller.id, royaltyRecipientInfoIds) ).to.revertedWithCustomError(bosonErrors, RevertReasons.NOT_ADMIN); }); @@ -4335,34 +4363,34 @@ describe("SellerHandler", function () { // Attempt to remove royalty recipients expecting revert await expect( - accountHandler.connect(rando).removeRoyaltyRecipients(seller.id, royaltyRecipientIds) + accountHandler.connect(rando).removeRoyaltyRecipients(seller.id, royaltyRecipientInfoIds) ).to.revertedWithCustomError(bosonErrors, RevertReasons.NOT_ADMIN); }); it("list of ids to remove is not sorted in ascending order", async function () { - royaltyRecipientIds = [1, 2, 5, 4]; + royaltyRecipientInfoIds = [1, 2, 5, 4]; // Attempt to remove royalty recipients expecting revert await expect( - accountHandler.connect(admin).removeRoyaltyRecipients(seller.id, royaltyRecipientIds) + accountHandler.connect(admin).removeRoyaltyRecipients(seller.id, royaltyRecipientInfoIds) ).to.revertedWithCustomError(bosonErrors, RevertReasons.ROYALTY_RECIPIENT_IDS_NOT_SORTED); }); it("id to remove does not exist", async function () { - royaltyRecipientIds = [1, 2, 1234]; + royaltyRecipientInfoIds = [1, 2, 1234]; // Attempt to remove royalty recipients expecting revert await expect( - accountHandler.connect(admin).removeRoyaltyRecipients(seller.id, royaltyRecipientIds) + accountHandler.connect(admin).removeRoyaltyRecipients(seller.id, royaltyRecipientInfoIds) ).to.revertedWithCustomError(bosonErrors, RevertReasons.ROYALTY_RECIPIENT_IDS_NOT_SORTED); }); it("seller tries to remove the default recipient", async function () { - royaltyRecipientIds = [0, 4, 6]; + royaltyRecipientInfoIds = [0, 4, 6]; // Attempt to remove royalty recipients expecting revert await expect( - accountHandler.connect(admin).removeRoyaltyRecipients(seller.id, royaltyRecipientIds) + accountHandler.connect(admin).removeRoyaltyRecipients(seller.id, royaltyRecipientInfoIds) ).to.revertedWithCustomError(bosonErrors, RevertReasons.CANNOT_REMOVE_DEFAULT_RECIPIENT); }); }); diff --git a/test/protocol/clients/BosonVoucherTest.js b/test/protocol/clients/BosonVoucherTest.js index 868fa49ed..2c16d1607 100644 --- a/test/protocol/clients/BosonVoucherTest.js +++ b/test/protocol/clients/BosonVoucherTest.js @@ -7,7 +7,7 @@ const Role = require("../../../scripts/domain/Role"); const { DisputeResolverFee } = require("../../../scripts/domain/DisputeResolverFee"); const Range = require("../../../scripts/domain/Range"); const { RoyaltyInfo } = require("../../../scripts/domain/RoyaltyInfo"); -const { RoyaltyRecipient, RoyaltyRecipientList } = require("../../../scripts/domain/RoyaltyRecipient.js"); +const { RoyaltyRecipientInfo, RoyaltyRecipientInfoList } = require("../../../scripts/domain/RoyaltyRecipientInfo.js"); const { Funds, FundsList } = require("../../../scripts/domain/Funds"); const { RevertReasons } = require("../../../scripts/config/revert-reasons"); const { @@ -1789,11 +1789,11 @@ describe("IBosonVoucher", function () { await configHandler.connect(deployer).setMaxRoyaltyPercentage("10000"); // Add multiple royalty recipients - const royaltyRecipientList = new RoyaltyRecipientList([ - new RoyaltyRecipient(rando.address, "100", "other1"), - new RoyaltyRecipient(rando2.address, "200", "other2"), + const royaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(rando.address, "100"), + new RoyaltyRecipientInfo(rando2.address, "200"), ]); - await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); + await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientInfoList.toStruct()); // Create an offer with multiple recipients const { offer, offerDates, offerDurations, disputeResolverId } = await mockOffer(); diff --git a/test/util/constants.js b/test/util/constants.js index 214ca9e8c..c6ee8c6e0 100644 --- a/test/util/constants.js +++ b/test/util/constants.js @@ -5,7 +5,6 @@ const oneWeek = oneDay * 7n; // 7 days in seconds const oneMonth = oneDay * 31n; // 31 days in seconds const VOUCHER_NAME = "Boson Voucher (rNFT)"; const VOUCHER_SYMBOL = "BOSON_VOUCHER_RNFT"; -const DEFAULT_ROYALTY_RECIPIENT = "Treasury"; const SEAPORT_ADDRESS = "0x00000000000001ad428e4906aE43D8F9852d0dD6"; // 1.4 const ROYALTY_REGISTRY_ADDRESS = "0xe7c9Cb6D966f76f3B5142167088927Bf34966a1f"; // Polygon address @@ -24,7 +23,6 @@ exports.oneMonth = oneMonth; exports.VOUCHER_NAME = VOUCHER_NAME; exports.VOUCHER_SYMBOL = VOUCHER_SYMBOL; exports.maxPriorityFeePerGas = maxPriorityFeePerGas; -exports.DEFAULT_ROYALTY_RECIPIENT = DEFAULT_ROYALTY_RECIPIENT; exports.ROYALTY_REGISTRY_ADDRESS = ROYALTY_REGISTRY_ADDRESS; exports.ROYALTY_ENGINE_ADDRESS = ROYALTY_ENGINE_ADDRESS; exports.SEAPORT_ADDRESS = SEAPORT_ADDRESS; diff --git a/test/util/utils.js b/test/util/utils.js index ac18a5d9e..b3b631b36 100644 --- a/test/util/utils.js +++ b/test/util/utils.js @@ -22,7 +22,7 @@ const Role = require("../../scripts/domain/Role"); const { toHexString } = require("../../scripts/util/utils.js"); const { expect } = require("chai"); const Offer = require("../../scripts/domain/Offer"); -const { RoyaltyRecipientList } = require("../../scripts/domain/RoyaltyRecipient.js"); +const { RoyaltyRecipientInfoList } = require("../../scripts/domain/RoyaltyRecipientInfo.js"); const { RoyaltyInfo } = require("../../scripts/domain/RoyaltyInfo.js"); function getEvent(receipt, factory, eventName) { @@ -137,7 +137,7 @@ function compareOfferStructs(returnedOffer) { } // ToDo: make a generic predicate for comparing structs -/** Predicate to compare RoyaltyRecipientList in emitted events +/** Predicate to compare RoyaltyRecipientInfoList in emitted events * Bind Royalty Recipient List to this function and pass it to .withArgs() instead of the expected Royalty recipient list * If returned and expected Royalty Recipient Lists are equal, the test will pass, otherwise it raises an error * @@ -145,13 +145,13 @@ function compareOfferStructs(returnedOffer) { * * await expect(tx) .to.emit(accountHandler, "RoyaltyRecipientsChanged") - .withArgs(seller.id, compareRoyaltyRecipientList.bind(expectedRoyaltyRecipientList.toStruct()), admin.address); + .withArgs(seller.id, compareRoyaltyRecipientInfoList.bind(expectedRoyaltyRecipientInfoList.toStruct()), admin.address); * - * @param {*} returnedRoyaltyRecipientList + * @param {*} returnedRoyaltyRecipientInfoList * @returns */ -function compareRoyaltyRecipientLists(returnedRoyaltyRecipientList) { - expect(RoyaltyRecipientList.fromStruct(returnedRoyaltyRecipientList).toStruct()).to.deep.equal(this); +function compareRoyaltyRecipientInfoLists(returnedRoyaltyRecipientInfoList) { + expect(RoyaltyRecipientInfoList.fromStruct(returnedRoyaltyRecipientInfoList).toStruct()).to.deep.equal(this); return true; } @@ -568,7 +568,7 @@ exports.getMappingStoragePosition = getMappingStoragePosition; exports.paddingType = paddingType; exports.getFacetsWithArgs = getFacetsWithArgs; exports.compareOfferStructs = compareOfferStructs; -exports.compareRoyaltyRecipientLists = compareRoyaltyRecipientLists; +exports.compareRoyaltyRecipientInfoLists = compareRoyaltyRecipientInfoLists; exports.objectToArray = objectToArray; exports.deriveTokenId = deriveTokenId; exports.incrementer = incrementer;