diff --git a/.gas-snapshot b/.gas-snapshot new file mode 100644 index 00000000..d3a6b887 --- /dev/null +++ b/.gas-snapshot @@ -0,0 +1,108 @@ +AsksV1_1IntegrationTest:test_ERC20Integration() (gas: 163154) +AsksV1_1IntegrationTest:test_ETHIntegration() (gas: 97044) +AsksV1_1Test:testFail_CannotUpdateCanceledAsk() (gas: 108617) +AsksV1_1Test:testFail_CannotUpdateFilledAsk() (gas: 194330) +AsksV1_1Test:testFail_MustBeTokenOwnerOrOperator() (gas: 6195) +AsksV1_1Test:testGas_CreateAsk() (gas: 99858) +AsksV1_1Test:testRevert_AskMustBeActiveToFill() (gas: 101656) +AsksV1_1Test:testRevert_AskMustExistToCancel() (gas: 4214) +AsksV1_1Test:testRevert_FillAmountMustMatchAsk() (gas: 103127) +AsksV1_1Test:testRevert_FillCurrencyMustMatchAsk() (gas: 103105) +AsksV1_1Test:testRevert_FindersFeeBPSCannotExceed10000() (gas: 7168) +AsksV1_1Test:testRevert_MsgSenderMustBeApprovedToCancelAsk() (gas: 106135) +AsksV1_1Test:testRevert_MustApproveERC721TransferHelper() (gas: 0) +AsksV1_1Test:testRevert_OnlySellerCanSetAskPrice() (gas: 102058) +AsksV1_1Test:testRevert_SellerFundsRecipientCannotBeZeroAddress() (gas: 7130) +AsksV1_1Test:test_CancelAsk() (gas: 32056) +AsksV1_1Test:test_CreateAskAndCancelPreviousOwners() (gas: 188232) +AsksV1_1Test:test_CreateAskFromTokenOperator() (gas: 155484) +AsksV1_1Test:test_CreateAskFromTokenOwner() (gas: 102351) +AsksV1_1Test:test_FillAsk() (gas: 93781) +AsksV1_1Test:test_UpdateAskPrice() (gas: 107953) +CollectionOffersV1IntegrationTest:test_ETHIntegration() (gas: 116439) +CollectionOffersV1IntegrationTest:test_RefundOfferDecreaseToSeller() (gas: 181929) +CollectionOffersV1IntegrationTest:test_WithdrawOfferFromSeller() (gas: 167276) +CollectionOffersV1IntegrationTest:test_WithdrawOfferIncreaseFromSeller() (gas: 179761) +CollectionOffersV1Test:testGas_CreateFirstCollectionOffer() (gas: 166247) +CollectionOffersV1Test:testRevert_CancelOfferMustBeMaker() (gas: 169216) +CollectionOffersV1Test:testRevert_FillMinimumTooHigh() (gas: 347178) +CollectionOffersV1Test:testRevert_FindersFeeCannotExceed10000() (gas: 2557) +CollectionOffersV1Test:testRevert_MustOwnCollectionToken() (gas: 346343) +CollectionOffersV1Test:testRevert_UpdateFindersFeeMustBeRegistrar() (gas: 1903) +CollectionOffersV1Test:testRevert_UpdateOfferMustBeMaker() (gas: 346024) +CollectionOffersV1Test:test_CancelCollectionOffer() (gas: 63366) +CollectionOffersV1Test:test_CreateCeilingOffer() (gas: 234040) +CollectionOffersV1Test:test_CreateCollectionOffer() (gas: 173619) +CollectionOffersV1Test:test_CreateFloorOffer() (gas: 294780) +CollectionOffersV1Test:test_CreateMiddleOffer() (gas: 347766) +CollectionOffersV1Test:test_DecreaseCeilingInPlace() (gas: 360810) +CollectionOffersV1Test:test_DecreaseCeilingToFloor() (gas: 365325) +CollectionOffersV1Test:test_DecreaseCeilingToMiddle() (gas: 368476) +CollectionOffersV1Test:test_DecreaseFloor() (gas: 363994) +CollectionOffersV1Test:test_DecreaseMiddleInPlace() (gas: 360499) +CollectionOffersV1Test:test_DecreaseMiddleToFloor() (gas: 365223) +CollectionOffersV1Test:test_DecreaseMiddleToMiddle() (gas: 365457) +CollectionOffersV1Test:test_FillCollectionOffer() (gas: 371999) +CollectionOffersV1Test:test_IncreaseCeiling() (gas: 363767) +CollectionOffersV1Test:test_IncreaseFloorInPlace() (gas: 359793) +CollectionOffersV1Test:test_IncreaseFloorToMiddle() (gas: 366346) +CollectionOffersV1Test:test_IncreaseFloorToNewCeiling() (gas: 362857) +CollectionOffersV1Test:test_IncreaseMiddleInPlace() (gas: 358259) +CollectionOffersV1Test:test_IncreaseMiddleToCeiling() (gas: 361625) +CollectionOffersV1Test:test_IncreaseMiddleToMiddle() (gas: 364422) +CollectionOffersV1Test:test_UpdateFindersFee() (gas: 5132) +ERC1155TransferHelperTest:testFail_UserMustApproveTransferHelperToTransferBatch() (gas: 43369) +ERC1155TransferHelperTest:testFail_UserMustApproveTransferHelperToTransferSingle() (gas: 37478) +ERC1155TransferHelperTest:testRevert_UserMustApproveModuleToTransferBatch() (gas: 39598) +ERC1155TransferHelperTest:testRevert_UserMustApproveModuleToTransferSingle() (gas: 35446) +ERC1155TransferHelperTest:test_ERC1155TransferBatch() (gas: 85262) +ERC1155TransferHelperTest:test_ERC1155TransferSingle() (gas: 70091) +ERC20TransferHelperTest:testFail_UserMustApproveTransferHelper() (gas: 35193) +ERC20TransferHelperTest:testRevert_UserMustApproveModule() (gas: 32416) +ERC20TransferHelperTest:test_ERC20Transfer() (gas: 86097) +ERC721TransferHelperTest:testFail_UserMustApproveTransferHelper() (gas: 39044) +ERC721TransferHelperTest:testRevert_UserMustApproveModule() (gas: 34878) +ERC721TransferHelperTest:test_ERC721Transfer() (gas: 71495) +OffersV1IntegrationTest:test_ERC20Integration() (gas: 222124) +OffersV1IntegrationTest:test_ETHIntegration() (gas: 156154) +OffersV1Test:testFail_CannotCreateOfferWithInvalidFindersFeeBps() (gas: 2227) +OffersV1Test:testFail_CannotCreateOfferWithoutAttachingFunds() (gas: 2306) +OffersV1Test:testGas_CreateOffer() (gas: 145889) +OffersV1Test:testRevert_AcceptAmountMustMatchOffer() (gas: 151021) +OffersV1Test:testRevert_AcceptCurrencyMustMatchOffer() (gas: 151105) +OffersV1Test:testRevert_CannotCancelInactiveOffer() (gas: 154680) +OffersV1Test:testRevert_CannotFillInactiveOffer() (gas: 155082) +OffersV1Test:testRevert_CannotIncreaseOfferWithoutAttachingFunds() (gas: 149825) +OffersV1Test:testRevert_CannotUpdateInactiveOffer() (gas: 154485) +OffersV1Test:testRevert_CannotUpdateOfferWithPreviousAmount() (gas: 156741) +OffersV1Test:testRevert_OnlySellerCanCancelOffer() (gas: 148964) +OffersV1Test:testRevert_OnlySellerCanUpdateOffer() (gas: 148729) +OffersV1Test:testRevert_OnlyTokenHolderCanFillOffer() (gas: 150339) +OffersV1Test:test_CancelNFTOffer() (gas: 106095) +OffersV1Test:test_CreateOffer() (gas: 148556) +OffersV1Test:test_DecreaseETHOffer() (gas: 164644) +OffersV1Test:test_DecreaseETHOfferWithERC20() (gas: 197810) +OffersV1Test:test_FillNFTOffer() (gas: 154124) +OffersV1Test:test_IncreaseETHOffer() (gas: 162310) +OffersV1Test:test_IncreaseETHOfferWithERC20() (gas: 197765) +ZoraModuleManagerTest:testFail_CannotApproveModuleNotRegistered() (gas: 4441) +ZoraModuleManagerTest:testRevert_CannotSetRegistrarToAddressZero() (gas: 5088) +ZoraModuleManagerTest:testRevert_ModuleAlreadyRegistered() (gas: 78514) +ZoraModuleManagerTest:test_RegisterModule() (gas: 74581) +ZoraModuleManagerTest:test_SetApproval() (gas: 101120) +ZoraModuleManagerTest:test_SetBatchApproval() (gas: 260206) +ZoraModuleManagerTest:test_SetRegistrar() (gas: 4166) +ZoraProtocolFeeSettingsTest:testRevert_AlreadyInitialized() (gas: 49106) +ZoraProtocolFeeSettingsTest:testRevert_InitOnlyOwner() (gas: 2078) +ZoraProtocolFeeSettingsTest:testRevert_OnlyMinter() (gas: 48369) +ZoraProtocolFeeSettingsTest:testRevert_SetMetadataOnlyOwner() (gas: 48084) +ZoraProtocolFeeSettingsTest:testRevert_SetOwnerOnlyOwner() (gas: 48040) +ZoraProtocolFeeSettingsTest:testRevert_SetParamsFeeRecipientMustBeNonZero() (gas: 97695) +ZoraProtocolFeeSettingsTest:testRevert_SetParamsMustBeLessThanHundred() (gas: 97728) +ZoraProtocolFeeSettingsTest:testRevert_SetParamsOnlyOwner() (gas: 97138) +ZoraProtocolFeeSettingsTest:test_Init() (gas: 48621) +ZoraProtocolFeeSettingsTest:test_MintToken() (gas: 97242) +ZoraProtocolFeeSettingsTest:test_ResetParamsToZero() (gas: 122751) +ZoraProtocolFeeSettingsTest:test_SetFeeParams() (gas: 122749) +ZoraProtocolFeeSettingsTest:test_SetMetadata() (gas: 50392) +ZoraProtocolFeeSettingsTest:test_SetOwner() (gas: 50391) diff --git a/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol b/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol new file mode 100644 index 00000000..72854363 --- /dev/null +++ b/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol @@ -0,0 +1,449 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.10; + +/// ------------ IMPORTS ------------ + +/// @title Collection Offer Book V1 +/// @author kulkarohan +/// @notice This module extension manages offers placed on ERC-721 collections +contract CollectionOfferBookV1 { + /// @notice The number of offers placed + uint32 public offerCount; + + /// @notice The metadata of a collection offer + /// @param maker The address of the maker that placed the offer + /// @param id The ID of the offer + /// @param prevId The ID of the previous offer in its collection's offer book + /// @param nextId The ID of the next offer in its collection's offer book + /// @param amount The amount of ETH offered + struct Offer { + address maker; + uint32 id; + uint32 prevId; + uint32 nextId; + uint256 amount; + } + + /// ------------ PUBLIC STORAGE ------------ + + /// @notice The metadata for a given collection offer + /// @dev ERC-721 token address => Offer ID => Offer + mapping(address => mapping(uint256 => Offer)) public offers; + + /// @notice The floor offer ID for a given collection + /// @dev ERC-721 token address => Floor offer ID + mapping(address => uint256) public floorOfferId; + + /// @notice The floor offer amount for a given collection + /// @dev ERC-721 token address => Floor offer amount + mapping(address => uint256) public floorOfferAmount; + + /// @notice The ceiling offer ID for a given collection + /// @dev ERC-721 token address => Ceiling offer ID + mapping(address => uint256) public ceilingOfferId; + + /// @notice The ceiling offer amount for a given collection + /// @dev ERC-721 token address => Ceiling offer amount + mapping(address => uint256) public ceilingOfferAmount; + + /// ------------ INTERNAL FUNCTIONS ------------ + + /// @notice Creates and places a new offer in its collection's offer book + /// @param _amount The amount of ETH offered + /// @param _maker The address of the maker + /// @return The ID of the created collection offer + function _addOffer( + address _collection, + uint256 _amount, + address _maker + ) internal returns (uint256) { + uint256 _offerCount; + unchecked { + _offerCount = ++offerCount; + } + + // If first offer for a collection, mark as both floor and ceiling + if (_isFirstOffer(_collection)) { + offers[_collection][_offerCount] = Offer({maker: _maker, amount: _amount, id: uint32(_offerCount), prevId: 0, nextId: 0}); + + floorOfferId[_collection] = _offerCount; + floorOfferAmount[_collection] = _amount; + + ceilingOfferId[_collection] = _offerCount; + ceilingOfferAmount[_collection] = _amount; + + // Else if offer is greater than current ceiling, mark as new ceiling + } else if (_isNewCeiling(_collection, _amount)) { + uint256 prevCeilingId = ceilingOfferId[_collection]; + + offers[_collection][prevCeilingId].nextId = uint32(_offerCount); + offers[_collection][_offerCount] = Offer({ + maker: _maker, + amount: _amount, + id: uint32(_offerCount), + prevId: uint32(prevCeilingId), + nextId: 0 + }); + + ceilingOfferId[_collection] = _offerCount; + ceilingOfferAmount[_collection] = _amount; + + // Else if offer is less than or equal to current floor, mark as new floor + } else if (_isNewFloor(_collection, _amount)) { + uint256 prevFloorId = floorOfferId[_collection]; + + offers[_collection][prevFloorId].prevId = uint32(_offerCount); + offers[_collection][_offerCount] = Offer({ + maker: _maker, + amount: _amount, + id: uint32(_offerCount), + prevId: 0, + nextId: uint32(prevFloorId) + }); + + floorOfferId[_collection] = _offerCount; + floorOfferAmount[_collection] = _amount; + + // Else offer is between floor and ceiling -- + } else { + // Start at floor + Offer memory offer = offers[_collection][floorOfferId[_collection]]; + + // Traverse towards ceiling; stop when an offer greater than or equal to new offer is reached + while ((offer.amount < _amount) && (offer.nextId != 0)) { + offer = offers[_collection][offer.nextId]; + } + + // Insert new offer before (time priority) + offers[_collection][_offerCount] = Offer({ + maker: _maker, + amount: _amount, + id: uint32(_offerCount), + prevId: offer.prevId, + nextId: offer.id + }); + + // Update neighboring pointers + offers[_collection][offer.id].prevId = uint32(_offerCount); + offers[_collection][offer.prevId].nextId = uint32(_offerCount); + } + + return _offerCount; + } + + /// @notice Updates an offer and (if needed) its location relative to other offers in the collection + /// @param _offer The metadata of the offer to update + /// @param _collection The address of the ERC-721 collection + /// @param _offerId The ID of the offer + /// @param _newAmount The new offer amount + /// @param _increase Whether the update is an amount increase or decrease + function _updateOffer( + Offer storage _offer, + address _collection, + uint256 _offerId, + uint256 _newAmount, + bool _increase + ) internal { + // If offer to update is only offer for its collection -- + if (_isOnlyOffer(_collection, _offerId)) { + // Update offer + _offer.amount = _newAmount; + // Update collection floor + floorOfferAmount[_collection] = _newAmount; + // Update collection ceiling + ceilingOfferAmount[_collection] = _newAmount; + + // Else if offer does not require relocation -- + } else if (_isUpdateInPlace(_collection, _offerId, _newAmount, _increase)) { + if (_isCeilingOffer(_collection, _offerId)) { + // Update offer + _offer.amount = _newAmount; + // Update collection ceiling + ceilingOfferAmount[_collection] = _newAmount; + } else if (_isFloorOffer(_collection, _offerId)) { + // Update offer + _offer.amount = _newAmount; + // Update collection floor + floorOfferAmount[_collection] = _newAmount; + } else { + // Update offer + _offer.amount = _newAmount; + } + + // Else if offer is new ceiling -- + } else if (_isNewCeiling(_collection, _newAmount)) { + // Get previous neighbors + uint256 prevId = _offer.prevId; + uint256 nextId = _offer.nextId; + + // Update previous neighbors + _connectNeighbors(_collection, _offerId, prevId, nextId); + + // Update previous ceiling + uint256 prevCeilingId = ceilingOfferId[_collection]; + offers[_collection][prevCeilingId].nextId = uint32(_offerId); + + // Update offer to be new ceiling + _offer.prevId = uint32(prevCeilingId); + _offer.nextId = 0; + _offer.amount = _newAmount; + + // Update collection ceiling + ceilingOfferId[_collection] = _offerId; + ceilingOfferAmount[_collection] = _newAmount; + + // Else if offer is new floor -- + } else if (_isNewFloor(_collection, _newAmount)) { + // Get previous neighbors + uint256 prevId = _offer.prevId; + uint256 nextId = _offer.nextId; + + // Update previous neighbors + _connectNeighbors(_collection, _offerId, prevId, nextId); + + // Update previous floor + uint256 prevFloorId = floorOfferId[_collection]; + offers[_collection][prevFloorId].prevId = uint32(_offerId); + + // Update offer to be new floor + _offer.nextId = uint32(prevFloorId); + _offer.prevId = 0; + _offer.amount = _newAmount; + + // Update collection floor + floorOfferId[_collection] = _offerId; + floorOfferAmount[_collection] = _newAmount; + + // Else offer requires relocation between floor and ceiling -- + } else { + // Update previous neighbors + _connectNeighbors(_collection, _offerId, _offer.prevId, _offer.nextId); + + // If update is increase -- + if (_increase) { + // Traverse forward until insert location is found + _insertIncreasedOffer(_collection, _offerId, _offer.nextId, _newAmount); + // Else update is decrease -- + } else { + // Traverse backward until insert location is found + _insertDecreasedOffer(_collection, _offerId, _offer.prevId, _newAmount); + } + } + } + + /// @notice Removes an offer from its collection's offer book + /// @param _collection The address of the ERC-721 collection + /// @param _offerId The ID of the offer + function _removeOffer(address _collection, uint256 _offerId) internal { + // If offer is only one for collection, remove all associated data + if (_isOnlyOffer(_collection, _offerId)) { + delete floorOfferId[_collection]; + delete floorOfferAmount[_collection]; + delete ceilingOfferId[_collection]; + delete ceilingOfferAmount[_collection]; + delete offers[_collection][_offerId]; + + // Else if the offer is the current floor, update the collection's floor before removing + } else if (_isFloorOffer(_collection, _offerId)) { + uint256 newFloorId = offers[_collection][_offerId].nextId; + uint256 newFloorAmount = offers[_collection][newFloorId].amount; + + offers[_collection][newFloorId].prevId = 0; + + floorOfferId[_collection] = newFloorId; + floorOfferAmount[_collection] = newFloorAmount; + + delete offers[_collection][_offerId]; + + // Else if the offer is the current ceiling, update the collection's ceiling before removing + } else if (_isCeilingOffer(_collection, _offerId)) { + uint256 newCeilingId = offers[_collection][_offerId].prevId; + uint256 newCeilingAmount = offers[_collection][newCeilingId].amount; + + offers[_collection][newCeilingId].nextId = 0; + + ceilingOfferId[_collection] = newCeilingId; + ceilingOfferAmount[_collection] = newCeilingAmount; + + delete offers[_collection][_offerId]; + + // Else offer is in middle, so update its previous and next neighboring pointers before removing + } else { + Offer memory offer = offers[_collection][_offerId]; + + offers[_collection][offer.nextId].prevId = uint32(offer.prevId); + offers[_collection][offer.prevId].nextId = uint32(offer.nextId); + + delete offers[_collection][_offerId]; + } + } + + /// @notice Finds a collection offer to fill + /// @param _collection The address of the ERC-721 collection + /// @param _minAmount The minimum offer amount valid to match + function _getMatchingOffer(address _collection, uint256 _minAmount) internal view returns (uint256) { + // If current ceiling offer is greater than or equal to maker's minimum, return its id to fill + if (ceilingOfferAmount[_collection] >= _minAmount) { + return ceilingOfferId[_collection]; + // Else return no offer found + } else { + return 0; + } + } + + /// ------------ PRIVATE FUNCTIONS ------------ + + /// @notice Checks whether any offers exist for a collection + /// @param _collection The address of the ERC-721 collection + function _isFirstOffer(address _collection) private view returns (bool) { + return (ceilingOfferId[_collection] == 0) && (floorOfferId[_collection] == 0); + } + + /// @notice Checks whether a given offer is the only one for a collection + /// @param _collection The address of the ERC-721 collection + /// @param _offerId The ID of the offer + function _isOnlyOffer(address _collection, uint256 _offerId) private view returns (bool) { + return (_offerId == floorOfferId[_collection]) && (_offerId == ceilingOfferId[_collection]); + } + + /// @notice Checks whether a given offer is the collection ceiling + /// @param _collection The address of the ERC-721 collection + /// @param _offerId The ID of the offer + function _isCeilingOffer(address _collection, uint256 _offerId) private view returns (bool) { + return (_offerId == ceilingOfferId[_collection]); + } + + /// @notice Checks whether a given offer is the collection floor + /// @param _collection The address of the ERC-721 collection + /// @param _offerId The ID of the offer + function _isFloorOffer(address _collection, uint256 _offerId) private view returns (bool) { + return (_offerId == floorOfferId[_collection]); + } + + /// @notice Checks whether an offer is greater than the collection ceiling + /// @param _collection The address of the ERC-721 collection + /// @param _amount The offer amount + function _isNewCeiling(address _collection, uint256 _amount) private view returns (bool) { + return (_amount > ceilingOfferAmount[_collection]); + } + + /// @notice Checks whether an offer is less than or equal to the collection floor + /// @param _collection The address of the ERC-721 collection + /// @param _amount The offer amount + function _isNewFloor(address _collection, uint256 _amount) private view returns (bool) { + return (_amount <= floorOfferAmount[_collection]); + } + + /// @notice Checks whether an offer can be updated without relocation + /// @param _collection The address of the ERC-721 collection + /// @param _offerId The ID of the offer + /// @param _newAmount The new offer amount + /// @param _increase Whether the update is an amount increase or decrease + function _isUpdateInPlace( + address _collection, + uint256 _offerId, + uint256 _newAmount, + bool _increase + ) private view returns (bool) { + uint256 nextOffer = offers[_collection][_offerId].nextId; + uint256 prevOffer = offers[_collection][_offerId].prevId; + return + ((_increase == true) && (_newAmount <= offers[_collection][nextOffer].amount)) || + ((_increase == false) && (_newAmount > offers[_collection][prevOffer].amount)); + } + + /// @notice Connects the pointers of an offer's neighbors + /// @param _collection The address of the ERC-721 collection + /// @param _offerId The ID of the offer + /// @param _prevId The ID of the offer's previous pointer + /// @param _nextId The ID of the offer's next pointer + function _connectNeighbors( + address _collection, + uint256 _offerId, + uint256 _prevId, + uint256 _nextId + ) private { + // If offer is floor -- + if (_offerId == floorOfferId[_collection]) { + // Mark next as new floor + offers[_collection][_nextId].prevId = 0; + // Update floor data + floorOfferId[_collection] = _nextId; + floorOfferAmount[_collection] = offers[_collection][_nextId].amount; + + // Else if offer is ceiling -- + } else if (_offerId == ceilingOfferId[_collection]) { + // Mark previous as new ceiling + offers[_collection][_prevId].nextId = 0; + // Update ceiling data + ceilingOfferId[_collection] = _prevId; + ceilingOfferAmount[_collection] = offers[_collection][_prevId].amount; + + // Else offer is in middle -- + } else { + // Update neighbor pointers + offers[_collection][_nextId].prevId = uint32(_prevId); + offers[_collection][_prevId].nextId = uint32(_nextId); + } + } + + /// @notice Updates the location of an increased offer + /// @param _collection The address of the ERC-721 collection + /// @param _offerId The ID of the offer to relocate + /// @param _nextId The next ID of the offer to relocate + /// @param _newAmount The new offer amount + function _insertIncreasedOffer( + address _collection, + uint256 _offerId, + uint256 _nextId, + uint256 _newAmount + ) private { + Offer memory offer = offers[_collection][_nextId]; + + // Traverse forward until the apt location is found + while ((offer.amount < _newAmount) && (offer.nextId != 0)) { + offer = offers[_collection][offer.nextId]; + } + + // Update offer pointers + offers[_collection][_offerId].nextId = uint32(offer.id); + offers[_collection][_offerId].prevId = uint32(offer.prevId); + + // Update neighbor pointers + offers[_collection][offer.id].prevId = uint32(_offerId); + offers[_collection][offer.prevId].nextId = uint32(_offerId); + + // Update offer amount + offers[_collection][_offerId].amount = _newAmount; + } + + /// @notice Updates the location of a decreased offer + /// @param _collection The address of the ERC-721 collection + /// @param _offerId The ID of the offer to relocate + /// @param _prevId The previous ID of the offer to relocate + /// @param _newAmount The new offer amount + function _insertDecreasedOffer( + address _collection, + uint256 _offerId, + uint256 _prevId, + uint256 _newAmount + ) private { + Offer memory offer = offers[_collection][_prevId]; + + // Traverse backwards until apt location is found + while ((offer.amount >= _newAmount) && (offer.prevId != 0)) { + offer = offers[_collection][offer.prevId]; + } + + // Update offer pointers + offers[_collection][_offerId].prevId = uint32(offer.id); + offers[_collection][_offerId].nextId = uint32(offer.nextId); + + // Update neighbor pointers + offers[_collection][offer.id].nextId = uint32(_offerId); + offers[_collection][offer.nextId].prevId = uint32(_offerId); + + // Update offer amount + offers[_collection][_offerId].amount = _newAmount; + } +} diff --git a/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol b/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol new file mode 100644 index 00000000..613ecd35 --- /dev/null +++ b/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.10; + +/// ------------ IMPORTS ------------ + +import {ReentrancyGuard} from "@rari-capital/solmate/src/utils/ReentrancyGuard.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ERC721TransferHelper} from "../../../transferHelpers/ERC721TransferHelper.sol"; +import {UniversalExchangeEventV1} from "../../../common/UniversalExchangeEvent/V1/UniversalExchangeEventV1.sol"; +import {IncomingTransferSupportV1} from "../../../common/IncomingTransferSupport/V1/IncomingTransferSupportV1.sol"; +import {OutgoingTransferSupportV1} from "../../../common/OutgoingTransferSupport/V1/OutgoingTransferSupportV1.sol"; +import {FeePayoutSupportV1} from "../../../common/FeePayoutSupport/FeePayoutSupportV1.sol"; +import {ModuleNamingSupportV1} from "../../../common/ModuleNamingSupport/ModuleNamingSupportV1.sol"; +import {CollectionOfferBookV1} from "./CollectionOfferBookV1.sol"; + +/// @title Collection Offers V1 +/// @author kulkarohan +/// @notice This module allows users to offer ETH for any ERC-721 token in a specified collection +contract CollectionOffersV1 is + ReentrancyGuard, + UniversalExchangeEventV1, + IncomingTransferSupportV1, + FeePayoutSupportV1, + ModuleNamingSupportV1, + CollectionOfferBookV1 +{ + /// @notice The finders fee bps configured by the DAO + uint16 public findersFeeBps; + + /// @notice The ZORA ERC-721 Transfer Helper + ERC721TransferHelper public immutable erc721TransferHelper; + + /// ------------ EVENTS ------------ + + /// @notice Emitted when a collection offer is created + /// @param collection The ERC-721 token address of the created offer + /// @param offerId The ID of the created offer + /// @param maker The address of the offer maker + /// @param amount The amount of the created offer + event CollectionOfferCreated(address indexed collection, uint256 indexed offerId, address maker, uint256 amount); + + /// @notice Emitted when a collection offer is updated + /// @param collection The ERC-721 token address of the updated offer + /// @param offerId The ID of the updated offer + /// @param maker The address of the offer maker + /// @param amount The amount of the updated offer + event CollectionOfferUpdated(address indexed collection, uint256 indexed offerId, address maker, uint256 amount); + + /// @notice Emitted when a collection offer is canceled + /// @param collection The ERC-721 token address of the canceled offer + /// @param offerId The ID of the canceled offer + /// @param maker The address of the offer maker + /// @param amount The amount of the canceled offer + event CollectionOfferCanceled(address indexed collection, uint256 indexed offerId, address maker, uint256 amount); + + /// @notice Emitted when a collection offer is filled + /// @param collection The ERC-721 token address of the filled offer + /// @param tokenId The ERC-721 token ID of the filled offer + /// @param offerId The ID of the filled offer + /// @param taker The address of the taker who filled the offer + /// @param finder The address of the finder who referred the sale + event CollectionOfferFilled(address indexed collection, uint256 indexed tokenId, uint256 indexed offerId, address taker, address finder); + + /// @notice Emitted when the finders fee is updated by the DAO + /// @param findersFeeBps The bps of the updated finders fee + event FindersFeeUpdated(uint16 indexed findersFeeBps); + + /// ------------ CONSTRUCTOR ------------ + + /// @param _erc20TransferHelper The ZORA ERC-20 Transfer Helper address + /// @param _erc721TransferHelper The ZORA ERC-721 Transfer Helper address + /// @param _royaltyEngine The Manifold Royalty Engine address + /// @param _weth The WETH token address + constructor( + address _erc20TransferHelper, + address _erc721TransferHelper, + address _royaltyEngine, + address _protocolFeeSettings, + address _weth + ) + IncomingTransferSupportV1(_erc20TransferHelper) + FeePayoutSupportV1(_royaltyEngine, _protocolFeeSettings, _weth, ERC721TransferHelper(_erc721TransferHelper).ZMM().registrar()) + ModuleNamingSupportV1("Collection Offers: v1.0") + { + erc721TransferHelper = ERC721TransferHelper(_erc721TransferHelper); + findersFeeBps = 100; + } + + /// ------------ MAKER FUNCTIONS ------------ + + // ,-. + // `-' + // /|\ + // | ,------------------. ,-------------------. + // / \ |CollectionOffersV1| |ERC20TransferHelper| + // Caller `--------+---------' `---------+---------' + // | createOffer() | | + // | ----------------------> | + // | | | + // | | msg.value | + // | | ---------------------------------> + // | | | + // | | |----. + // | | | | transfer ETH into escrow + // | | |<---' + // | | | + // | |----. | + // | | | _addOffer() | + // | |<---' | + // | | | + // | |----. + // | | | emit CollectionOfferCreated() + // | |<---' + // | | | + // | id | | + // | <---------------------- | + // Caller ,--------+---------. ,---------+---------. + // ,-. |CollectionOffersV1| |ERC20TransferHelper| + // `-' `------------------' `-------------------' + // /|\ + // | + // / \ + /// @notice Creates an offer for any NFT in a collection + /// @param _tokenContract The ERC-721 collection address + /// @return The ID of the created offer + function createOffer(address _tokenContract) external payable nonReentrant returns (uint256) { + // Ensure offer is valid and take custody + _handleIncomingTransfer(msg.value, address(0)); + + // Add to collection's offer book + uint256 offerId = _addOffer(_tokenContract, msg.value, msg.sender); + + emit CollectionOfferCreated(_tokenContract, offerId, msg.sender, msg.value); + + return offerId; + } + + // ,-. + // `-' + // /|\ + // | ,------------------. ,-------------------. + // / \ |CollectionOffersV1| |ERC20TransferHelper| + // Caller `--------+---------' `---------+---------' + // | setOfferAmount() | | + // | ----------------------> | + // | | | + // | | | + // | __________________________________________________________________________ + // | ! ALT / increase offer? | ! + // | !_____/ | | ! + // | ! | transfer msg.value to escrow | ! + // | ! | ---------------------------------> ! + // | !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~! + // | ! [decrease offer] | | ! + // | ! | refund decrease amount | ! + // | ! | ---------------------------------> ! + // | !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~! + // | | | + // | |----. | + // | | | _updateOffer() | + // | |<---' | + // | | | + // | |----. + // | | | emit CollectionOfferUpdated() + // | |<---' + // Caller ,--------+---------. ,---------+---------. + // ,-. |CollectionOffersV1| |ERC20TransferHelper| + // `-' `------------------' `-------------------' + // /|\ + // | + // / \ + /// @notice Updates the amount of a collection offer + /// @param _tokenContract The address of the ERC-721 collection + /// @param _offerId The ID of the collection offer + /// @param _amount The new offer amount + function setOfferAmount( + address _tokenContract, + uint32 _offerId, + uint256 _amount + ) external payable nonReentrant { + Offer storage offer = offers[_tokenContract][_offerId]; + + require(msg.sender == offer.maker, "setOfferAmount must be maker"); + require(_amount > 0 && _amount != offer.amount, "setOfferAmount _amount cannot be 0 or previous offer"); + + uint256 prevAmount = offer.amount; + + if (_amount > prevAmount) { + unchecked { + uint256 increaseAmount = _amount - prevAmount; + _handleIncomingTransfer(increaseAmount, address(0)); + _updateOffer(offer, _tokenContract, _offerId, _amount, true); + } + } else { + unchecked { + uint256 decreaseAmount = prevAmount - _amount; + _handleOutgoingTransfer(msg.sender, decreaseAmount, address(0), 50000); + _updateOffer(offer, _tokenContract, _offerId, _amount, false); + } + } + + emit CollectionOfferUpdated(_tokenContract, _offerId, msg.sender, _amount); + } + + // ,-. + // `-' + // /|\ + // | ,------------------. ,-------------------. + // / \ |CollectionOffersV1| |ERC20TransferHelper| + // Caller `--------+---------' `---------+---------' + // | cancelOffer() | | + // | ----------------------> | + // | | | + // | | call() | + // | | ----------------------------------> + // | | | + // | | |----. + // | | | | refund ETH from escrow + // | | |<---' + // | | | + // | |----. + // | | | emit CollectionOfferCanceled() + // | |<---' + // | | | + // | |----. | + // | | | _removeOffer() | + // | |<---' | + // Caller ,--------+---------. ,---------+---------. + // ,-. |CollectionOffersV1| |ERC20TransferHelper| + // `-' `------------------' `-------------------' + // /|\ + // | + // / \ + /// @notice Cancels and refunds a collection offer + /// @param _tokenContract The address of the ERC-721 collection + /// @param _offerId The ID of the collection offer + function cancelOffer(address _tokenContract, uint32 _offerId) external nonReentrant { + Offer memory offer = offers[_tokenContract][_offerId]; + + require(msg.sender == offer.maker, "cancelOffer must be maker"); + + // Refund offer + _handleOutgoingTransfer(msg.sender, offer.amount, address(0), 50000); + + emit CollectionOfferCanceled(_tokenContract, _offerId, msg.sender, offer.amount); + + _removeOffer(_tokenContract, _offerId); + } + + /// ------------ TAKER FUNCTIONS ------------ + + // ,-. + // `-' + // /|\ + // | ,------------------. ,--------------------. + // / \ |CollectionOffersV1| |ERC721TransferHelper| + // Caller `--------+---------' `---------+----------' + // | fillOffer() | | + // | ----------------------> | + // | | | + // | |----. | + // | | | validate token owner | + // | |<---' | + // | | | + // | |----. | + // | | | _getMatchingOffer() | + // | |<---' | + // | | | + // | | | + // | ________________________________________ | + // | ! ALT / offer exists satisfying minimum? | + // | !_____/ | ! | + // | ! |----. ! | + // | ! | | (continue) ! | + // | ! |<---' ! | + // | !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~! | + // | !~[revert]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~! | + // | | | + // | |----. | + // | | | handle royalty payouts | + // | |<---' | + // | | | + // | |----. | + // | | | handle finders fee payout | + // | |<---' | + // | | | + // | | transferFrom() | + // | | --------------------------------> + // | | | + // | | |----. + // | | | | transfer NFT from taker to maker + // | | |<---' + // | | | + // | |----. | + // | | | emit ExchangeExecuted() | + // | |<---' | + // | | | + // | |----. + // | | | emit CollectionOfferFilled() + // | |<---' + // | | | + // | |----. | + // | | | _removeOffer() | + // | |<---' | + // Caller ,--------+---------. ,---------+----------. + // ,-. |CollectionOffersV1| |ERC721TransferHelper| + // `-' `------------------' `--------------------' + // /|\ + // | + // / \ + /// @notice Fills the highest collection offer above a specified minimum, if exists + /// @param _tokenContract The address of the ERC-721 collection + /// @param _tokenId The ID of the ERC-721 token + /// @param _minAmount The minimum amount willing to accept + /// @param _finder The address of the offer referrer + function fillOffer( + address _tokenContract, + uint256 _tokenId, + uint256 _minAmount, + address _finder + ) external nonReentrant { + require(msg.sender == IERC721(_tokenContract).ownerOf(_tokenId), "fillOffer must own specified token"); + + // Get matching offer (if exists) + uint256 offerId = _getMatchingOffer(_tokenContract, _minAmount); + require(offerId != 0, "fillOffer offer satisfying _minAmount not found"); + + Offer memory offer = offers[_tokenContract][offerId]; + + // Ensure royalties are honored + (uint256 remainingProfit, ) = _handleRoyaltyPayout(_tokenContract, _tokenId, offer.amount, address(0), 300000); + + // Payout optional protocol fee + remainingProfit = _handleProtocolFeePayout(remainingProfit, address(0)); + + // Payout optional finder fee + if (_finder != address(0)) { + uint256 findersFee; + // Calculate payout + findersFee = (remainingProfit * findersFeeBps) / 10000; + // Transfer to finder + _handleOutgoingTransfer(_finder, findersFee, address(0), 50000); + // Update remaining profit + remainingProfit -= findersFee; + } + + // Transfer remaining ETH to taker + _handleOutgoingTransfer(msg.sender, remainingProfit, address(0), 50000); + + // Transfer NFT to maker + erc721TransferHelper.transferFrom(_tokenContract, msg.sender, offer.maker, _tokenId); + + ExchangeDetails memory userAExchangeDetails = ExchangeDetails({tokenContract: address(0), tokenId: 0, amount: offer.amount}); + ExchangeDetails memory userBExchangeDetails = ExchangeDetails({tokenContract: _tokenContract, tokenId: _tokenId, amount: 1}); + + emit ExchangeExecuted(offer.maker, msg.sender, userAExchangeDetails, userBExchangeDetails); + emit CollectionOfferFilled(_tokenContract, _tokenId, offerId, msg.sender, _finder); + + _removeOffer(_tokenContract, offerId); + } + + /// ------------ DAO FUNCTIONS ------------ + + // ,-. + // `-' + // /|\ + // | ,------------------. + // / \ |CollectionOffersV1| + // zoraDAO `--------+---------' + // | setFindersFee() | + // |---------------------->| + // | | + // | |----. + // | | | update finders fee + // | |<---' + // | | + // | |----. + // | | | emit FindersFeeUpdated() + // | |<---' + // zoraDAO ,--------+---------. + // ,-. |CollectionOffersV1| + // `-' `------------------' + // /|\ + // | + // / \ + /// @notice Updates the finders fee for collection offers + /// @param _findersFeeBps The new finders fee bps + function setFindersFee(uint16 _findersFeeBps) external nonReentrant { + require(msg.sender == registrar, "setFindersFee only registrar"); + require(_findersFeeBps <= 10000, "setFindersFee bps must be <= 10000"); + + findersFeeBps = _findersFeeBps; + + emit FindersFeeUpdated(_findersFeeBps); + } +} diff --git a/contracts/test/modules/CollectionOffers/V1/CollectionOffers.integration.t.sol b/contracts/test/modules/CollectionOffers/V1/CollectionOffers.integration.t.sol new file mode 100644 index 00000000..e6f08a3e --- /dev/null +++ b/contracts/test/modules/CollectionOffers/V1/CollectionOffers.integration.t.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.10; + +import {DSTest} from "ds-test/test.sol"; + +import {CollectionOffersV1} from "../../../../modules/CollectionOffers/V1/CollectionOffersV1.sol"; +import {Zorb} from "../../../utils/users/Zorb.sol"; +import {ZoraRegistrar} from "../../../utils/users/ZoraRegistrar.sol"; +import {ZoraModuleManager} from "../../../../ZoraModuleManager.sol"; +import {ZoraProtocolFeeSettings} from "../../../../auxiliary/ZoraProtocolFeeSettings/ZoraProtocolFeeSettings.sol"; +import {ERC20TransferHelper} from "../../../../transferHelpers/ERC20TransferHelper.sol"; +import {ERC721TransferHelper} from "../../../../transferHelpers/ERC721TransferHelper.sol"; +import {RoyaltyEngine} from "../../../utils/modules/RoyaltyEngine.sol"; + +import {TestERC721} from "../../../utils/tokens/TestERC721.sol"; +import {WETH} from "../../../utils/tokens/WETH.sol"; +import {VM} from "../../../utils/VM.sol"; + +/// @title CollectionOffersV1IntegrationTest +/// @notice Integration Tests for CollectionOffersV1 +contract CollectionOffersV1IntegrationTest is DSTest { + VM internal vm; + + ZoraRegistrar internal registrar; + ZoraProtocolFeeSettings internal ZPFS; + ZoraModuleManager internal ZMM; + ERC20TransferHelper internal erc20TransferHelper; + ERC721TransferHelper internal erc721TransferHelper; + RoyaltyEngine internal royaltyEngine; + + CollectionOffersV1 internal offers; + TestERC721 internal token; + WETH internal weth; + + Zorb internal seller; + Zorb internal buyer; + Zorb internal finder; + Zorb internal royaltyRecipient; + Zorb internal protocolFeeRecipient; + + function setUp() public { + // Cheatcodes + vm = VM(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + // Deploy V3 + registrar = new ZoraRegistrar(); + ZPFS = new ZoraProtocolFeeSettings(); + ZMM = new ZoraModuleManager(address(registrar), address(ZPFS)); + protocolFeeRecipient = new Zorb(address(ZMM)); + erc20TransferHelper = new ERC20TransferHelper(address(ZMM)); + erc721TransferHelper = new ERC721TransferHelper(address(ZMM)); + + // Init V3 + registrar.init(ZMM); + ZPFS.init(address(ZMM), address(0)); + + // Create users + seller = new Zorb(address(ZMM)); + buyer = new Zorb(address(ZMM)); + finder = new Zorb(address(ZMM)); + royaltyRecipient = new Zorb(address(ZMM)); + + // Deploy mocks + royaltyEngine = new RoyaltyEngine(address(royaltyRecipient)); + token = new TestERC721(); + weth = new WETH(); + + // Deploy Collection Offers v1.0 + offers = new CollectionOffersV1( + address(erc20TransferHelper), + address(erc721TransferHelper), + address(royaltyEngine), + address(ZPFS), + address(weth) + ); + registrar.registerModule(address(offers)); + + // Set fee parameters for Collection Offers v1.0 + vm.prank(address(registrar)); + ZPFS.setFeeParams(address(offers), address(protocolFeeRecipient), 1); + + // Set seller balance + vm.deal(address(seller), 100 ether); + + // Mint buyer token + token.mint(address(buyer), 0); + + // Users approve Collection Offers module + seller.setApprovalForModule(address(offers), true); + buyer.setApprovalForModule(address(offers), true); + + // Buyer approve ERC721TransferHelper + vm.prank(address(buyer)); + token.setApprovalForAll(address(erc721TransferHelper), true); + } + + /// ------------ ETH COLLECTION OFFER ------------ /// + + function offer() public { + vm.prank(address(seller)); + offers.createOffer{value: 1 ether}(address(token)); + } + + function fill() public { + vm.prank(address(seller)); + offers.createOffer{value: 1 ether}(address(token)); + + vm.prank(address(buyer)); + offers.fillOffer(address(token), 0, 1 ether, address(finder)); + } + + function test_WithdrawOfferFromSeller() public { + uint256 beforeBalance = address(seller).balance; + offer(); + uint256 afterBalance = address(seller).balance; + + require(beforeBalance - afterBalance == 1 ether); + } + + function test_WithdrawOfferIncreaseFromSeller() public { + uint256 beforeBalance = address(seller).balance; + + offer(); + + // Increase initial offer to 2 ETH + vm.prank(address(seller)); + offers.setOfferAmount{value: 1 ether}(address(token), 1, 2 ether); + + uint256 afterBalance = address(seller).balance; + + require(beforeBalance - afterBalance == 2 ether); + } + + function test_RefundOfferDecreaseToSeller() public { + uint256 beforeBalance = address(seller).balance; + + offer(); + + // Decrease initial offer to 0.5 ETH + vm.prank(address(seller)); + offers.setOfferAmount(address(token), 1, 0.5 ether); + + uint256 afterBalance = address(seller).balance; + + require(beforeBalance - afterBalance == 0.5 ether); + } + + function test_ETHIntegration() public { + uint256 beforeSellerBalance = address(seller).balance; + uint256 beforeBuyerBalance = address(buyer).balance; + uint256 beforeRoyaltyRecipientBalance = address(royaltyRecipient).balance; + uint256 beforeFinderBalance = address(finder).balance; + uint256 beforeProtocolFeeRecipient = address(protocolFeeRecipient).balance; + address beforeTokenOwner = token.ownerOf(0); + + fill(); + + uint256 afterBuyerBalance = address(buyer).balance; + uint256 afterSellerBalance = address(seller).balance; + uint256 afterRoyaltyRecipientBalance = address(royaltyRecipient).balance; + uint256 afterFinderBalance = address(finder).balance; + uint256 afterProtocolFeeRecipient = address(protocolFeeRecipient).balance; + address afterTokenOwner = token.ownerOf(0); + + // 1 ETH withdrawn from seller + require((beforeSellerBalance - afterSellerBalance) == 1 ether); + // 0.05 ETH creator royalty + require((afterRoyaltyRecipientBalance - beforeRoyaltyRecipientBalance) == 0.05 ether); + // 1 bps protocol fee (Remaining 0.95 ETH * 0.01% protocol fee = 0.000095 ETH) + require((afterProtocolFeeRecipient - beforeProtocolFeeRecipient) == 0.000095 ether); + // 100 bps finders fee (Remaining 0.949905 ETH * 1% finders fee = 0.00949905 ETH) + require((afterFinderBalance - beforeFinderBalance) == 0.00949905 ether); + // Remaining 0.94040595 ETH paid to buyer + require((afterBuyerBalance - beforeBuyerBalance) == 0.94040595 ether); + // NFT transferred to seller + require((beforeTokenOwner == address(buyer)) && afterTokenOwner == address(seller)); + } +} diff --git a/contracts/test/modules/CollectionOffers/V1/CollectionOffers.t.sol b/contracts/test/modules/CollectionOffers/V1/CollectionOffers.t.sol new file mode 100644 index 00000000..d4565ea4 --- /dev/null +++ b/contracts/test/modules/CollectionOffers/V1/CollectionOffers.t.sol @@ -0,0 +1,551 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.10; + +import {DSTest} from "ds-test/test.sol"; + +import {CollectionOffersV1} from "../../../../modules/CollectionOffers/V1/CollectionOffersV1.sol"; +import {Zorb} from "../../../utils/users/Zorb.sol"; +import {ZoraRegistrar} from "../../../utils/users/ZoraRegistrar.sol"; +import {ZoraModuleManager} from "../../../../ZoraModuleManager.sol"; +import {ZoraProtocolFeeSettings} from "../../../../auxiliary/ZoraProtocolFeeSettings/ZoraProtocolFeeSettings.sol"; +import {ERC20TransferHelper} from "../../../../transferHelpers/ERC20TransferHelper.sol"; +import {ERC721TransferHelper} from "../../../../transferHelpers/ERC721TransferHelper.sol"; +import {RoyaltyEngine} from "../../../utils/modules/RoyaltyEngine.sol"; + +import {TestERC721} from "../../../utils/tokens/TestERC721.sol"; +import {WETH} from "../../../utils/tokens/WETH.sol"; +import {VM} from "../../../utils/VM.sol"; + +/// @title CollectionOffersV1Test +/// @notice Unit Tests for CollectionOffersV1 +contract CollectionOffersV1Test is DSTest { + VM internal vm; + + ZoraRegistrar internal registrar; + ZoraProtocolFeeSettings internal ZPFS; + ZoraModuleManager internal ZMM; + ERC20TransferHelper internal erc20TransferHelper; + ERC721TransferHelper internal erc721TransferHelper; + RoyaltyEngine internal royaltyEngine; + + CollectionOffersV1 internal offers; + TestERC721 internal token; + WETH internal weth; + + Zorb internal seller; + Zorb internal seller2; + Zorb internal seller3; + Zorb internal seller4; + Zorb internal seller5; + Zorb internal buyer; + Zorb internal finder; + Zorb internal royaltyRecipient; + + function setUp() public { + // Cheatcodes + vm = VM(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + // Deploy V3 + registrar = new ZoraRegistrar(); + ZPFS = new ZoraProtocolFeeSettings(); + ZMM = new ZoraModuleManager(address(registrar), address(ZPFS)); + erc20TransferHelper = new ERC20TransferHelper(address(ZMM)); + erc721TransferHelper = new ERC721TransferHelper(address(ZMM)); + + // Init V3 + registrar.init(ZMM); + ZPFS.init(address(ZMM), address(0)); + + // Create users + buyer = new Zorb(address(ZMM)); + finder = new Zorb(address(ZMM)); + seller = new Zorb(address(ZMM)); + seller2 = new Zorb(address(ZMM)); + seller3 = new Zorb(address(ZMM)); + seller4 = new Zorb(address(ZMM)); + seller5 = new Zorb(address(ZMM)); + royaltyRecipient = new Zorb(address(ZMM)); + + // Deploy mocks + royaltyEngine = new RoyaltyEngine(address(royaltyRecipient)); + token = new TestERC721(); + weth = new WETH(); + + // Deploy Collection Offers v1.0 + offers = new CollectionOffersV1( + address(erc20TransferHelper), + address(erc721TransferHelper), + address(royaltyEngine), + address(ZPFS), + address(weth) + ); + registrar.registerModule(address(offers)); + + // Set user balances + vm.deal(address(seller), 100 ether); + vm.deal(address(seller2), 100 ether); + vm.deal(address(seller3), 100 ether); + vm.deal(address(seller4), 100 ether); + vm.deal(address(seller5), 100 ether); + + // Mint buyer token + token.mint(address(buyer), 0); + + // Users approve Collection Offers module + seller.setApprovalForModule(address(offers), true); + seller2.setApprovalForModule(address(offers), true); + seller3.setApprovalForModule(address(offers), true); + seller4.setApprovalForModule(address(offers), true); + seller5.setApprovalForModule(address(offers), true); + buyer.setApprovalForModule(address(offers), true); + + // Buyer approve ERC721TransferHelper + vm.prank(address(buyer)); + token.setApprovalForAll(address(erc721TransferHelper), true); + } + + /// ------------ HELPERS ------------ /// + + function loadOffers() public { + // First offer + vm.prank(address(seller)); + offers.createOffer{value: 1 ether}(address(token)); + // Ceiling offer + vm.prank(address(seller2)); + offers.createOffer{value: 2 ether}(address(token)); + // Floor offer + vm.prank(address(seller3)); + offers.createOffer{value: 0.5 ether}(address(token)); + // Middle offer + vm.prank(address(seller4)); + offers.createOffer{value: 1 ether}(address(token)); + + // Floor to Ceiling order: id3 --> id4 --> id1 --> id2 + } + + /// ------------ CREATE COLLECTION OFFER ------------ /// + + function testGas_CreateFirstCollectionOffer() public { + offers.createOffer{value: 1 ether}(address(token)); + } + + function test_CreateCollectionOffer() public { + vm.prank(address(seller)); + offers.createOffer{value: 1 ether}(address(token)); + + (address offeror, uint32 id, uint32 _prevId, uint32 _nextId, uint256 amount) = offers.offers(address(token), 1); + + require(offeror == address(seller)); + require(id == 1); + require(amount == 1 ether); + require(_prevId == 0); + require(_nextId == 0); + + uint256 floorId = offers.floorOfferId(address(token)); + uint256 floorAmt = offers.floorOfferAmount(address(token)); + uint256 ceilingId = offers.ceilingOfferId(address(token)); + uint256 ceilingAmt = offers.ceilingOfferAmount(address(token)); + + require(floorId == 1); + require(floorAmt == 1 ether); + require(ceilingId == 1); + require(ceilingAmt == 1 ether); + } + + function test_CreateCeilingOffer() public { + // First offer + vm.prank(address(seller)); + offers.createOffer{value: 1 ether}(address(token)); + // Ceiling offer + vm.prank(address(seller2)); + offers.createOffer{value: 2 ether}(address(token)); + + (address offeror1, uint32 id1, uint32 _prevId1, uint32 _nextId1, uint256 amount1) = offers.offers(address(token), 1); + (address offeror2, uint32 id2, uint32 _prevId2, uint32 _nextId2, uint256 amount2) = offers.offers(address(token), 2); + + require(offeror1 == address(seller) && offeror2 == address(seller2)); + require(amount1 == 1 ether && amount2 == 2 ether); + // Ensure floor prevId is 0 and ceiling nextId is 0 + require(_prevId1 == 0 && _nextId2 == 0); + // Ensure floor nextId is ceiling id and ceiling prevId is floor id + require(_nextId1 == 2 && _prevId2 == 1); + + uint256 floorId = offers.floorOfferId(address(token)); + uint256 floorAmt = offers.floorOfferAmount(address(token)); + uint256 ceilingId = offers.ceilingOfferId(address(token)); + uint256 ceilingAmt = offers.ceilingOfferAmount(address(token)); + + require(floorId == id1); + require(floorAmt == amount1); + require(ceilingId == id2); + require(ceilingAmt == amount2); + } + + function test_CreateFloorOffer() public { + // First offer + vm.prank(address(seller)); + offers.createOffer{value: 1 ether}(address(token)); + // Ceiling offer + vm.prank(address(seller2)); + offers.createOffer{value: 2 ether}(address(token)); + // Floor offer + vm.prank(address(seller3)); + offers.createOffer{value: 0.5 ether}(address(token)); + + (address offeror1, uint32 id1, uint32 _prevId1, uint32 _nextId1, uint256 amount1) = offers.offers(address(token), 1); + (address offeror2, uint32 id2, uint32 _prevId2, uint32 _nextId2, uint256 amount2) = offers.offers(address(token), 2); + (address offeror3, uint32 id3, uint32 _prevId3, uint32 _nextId3, uint256 amount3) = offers.offers(address(token), 3); + + // Ensure sellers and amounts are valid + require(offeror1 == address(seller) && offeror2 == address(seller2) && offeror3 == address(seller3)); + require(amount1 == 1 ether && amount2 == 2 ether && amount3 == 0.5 ether); + + // Ensure floor to ceiling order is: id3 --> id1 --> id2 + require(_prevId3 == 0 && _nextId2 == 0); + require(_nextId3 == id1 && _prevId1 == id3); + require(_nextId1 == id2 && _prevId2 == id1); + + uint256 floorId = offers.floorOfferId(address(token)); + uint256 floorAmt = offers.floorOfferAmount(address(token)); + uint256 ceilingId = offers.ceilingOfferId(address(token)); + uint256 ceilingAmt = offers.ceilingOfferAmount(address(token)); + + require(floorId == id3); + require(floorAmt == amount3); + require(ceilingId == id2); + require(ceilingAmt == amount2); + } + + function test_CreateMiddleOffer() public { + // Order: id3 --> **id4** --> id1 --> id2 + loadOffers(); + + (address offeror4, , uint32 _prevId4, uint32 _nextId4, uint256 amount4) = offers.offers(address(token), 4); + + uint256 floorId = offers.floorOfferId(address(token)); + uint256 ceilingId = offers.ceilingOfferId(address(token)); + + require(offeror4 == address(seller4)); + require(amount4 == 1 ether); + + // Ensure placed between id3 and id1 + require(_nextId4 == 1 && _prevId4 == 3); + // Ensure floor and ceiling ids are valid + require(ceilingId == 2 && floorId == 3); + } + + /// ------------ SET COLLECTION OFFER AMOUNT ------------ /// + + function test_IncreaseCeiling() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); + + vm.prank(address(seller2)); + offers.setOfferAmount{value: 3 ether}(address(token), 2, 5 ether); + + (, , uint32 _prevId2, uint32 _nextId2, uint256 amount2) = offers.offers(address(token), 2); + uint256 ceilingId = offers.ceilingOfferId(address(token)); + + require(ceilingId == 2); + require(_prevId2 == 1 && _nextId2 == 0); + require(amount2 == 5 ether); + } + + function test_DecreaseCeilingInPlace() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); + + vm.prank(address(seller2)); + offers.setOfferAmount(address(token), 2, 1.75 ether); + + require(offers.ceilingOfferAmount(address(token)) == 1.75 ether); + require(offers.ceilingOfferId(address(token)) == 2); + } + + function test_DecreaseCeilingToMiddle() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); + + vm.prank(address(seller2)); + offers.setOfferAmount(address(token), 2, 0.95 ether); + + // Updated Order: id3 --> id2 --> id4 --> id1 + (, , uint32 _prevId2, uint32 _nextId2, uint256 amount2) = offers.offers(address(token), 2); + + require(offers.ceilingOfferAmount(address(token)) == 1 ether); + require(offers.ceilingOfferId(address(token)) == 1); + + require(_prevId2 == 3); + require(_nextId2 == 4); + require(amount2 == 0.95 ether); + } + + function test_DecreaseCeilingToFloor() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); + + vm.prank(address(seller2)); + offers.setOfferAmount(address(token), 2, 0.25 ether); + + // Updated Order: id2 --> id3 --> id4 --> id1 + (, , uint32 _prevId2, uint32 _nextId2, uint256 amount2) = offers.offers(address(token), 2); + + require(offers.ceilingOfferAmount(address(token)) == 1 ether); + require(offers.ceilingOfferId(address(token)) == 1); + + require(_prevId2 == 0); + require(_nextId2 == 3); + require(amount2 == 0.25 ether); + } + + function test_IncreaseFloorToNewCeiling() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); + + // Increase floor offer to new ceiling + vm.prank(address(seller3)); + offers.setOfferAmount{value: 4.5 ether}(address(token), 3, 5 ether); + + // Updated Order: id4 --> id1 --> id2 --> id3 + (, , uint32 _prevId3, uint256 _nextId3, uint256 amount3) = offers.offers(address(token), 3); + uint256 floorId = offers.floorOfferId(address(token)); + uint256 ceilingId = offers.ceilingOfferId(address(token)); + + // Ensure book is updated with floor as new ceiling + require(ceilingId == 3 && floorId == 4); + require(_prevId3 == 2 && _nextId3 == 0); + require(amount3 == 5 ether); + } + + function test_IncreaseFloorToMiddle() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); + + // Increase floor offer to equal ceiling + vm.prank(address(seller3)); + offers.setOfferAmount{value: 1.5 ether}(address(token), 3, 2 ether); + + // Updated Order: id4 --> id1 --> id3 --> id2 + (, , uint32 _prevId3, uint32 _nextId3, uint256 amount3) = offers.offers(address(token), 3); + + uint256 floorId = offers.floorOfferId(address(token)); + uint256 ceilingId = offers.ceilingOfferId(address(token)); + + // Ensure book is updated wrt time priority + require(ceilingId == 2 && floorId == 4); + require(_prevId3 == 1 && _nextId3 == 2); + require(amount3 == 2 ether); + } + + function test_IncreaseFloorInPlace() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); + + vm.prank(address(seller3)); + offers.setOfferAmount{value: 0.1 ether}(address(token), 3, 0.6 ether); + + (, , uint32 _prevId3, uint32 _nextId3, uint256 amount3) = offers.offers(address(token), 3); + + require(offers.floorOfferId(address(token)) == 3); + + require(_prevId3 == 0); + require(_nextId3 == 4); + require(amount3 == 0.6 ether); + } + + function test_DecreaseFloor() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); + + vm.prank(address(seller3)); + offers.setOfferAmount(address(token), 3, 0.25 ether); + + (, , uint32 _prevId3, uint32 _nextId3, uint256 amount3) = offers.offers(address(token), 3); + + require(offers.floorOfferId(address(token)) == 3); + + require(_prevId3 == 0); + require(_nextId3 == 4); + require(amount3 == 0.25 ether); + } + + function test_IncreaseMiddleToCeiling() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); + + vm.prank(address(seller4)); + offers.setOfferAmount{value: 5 ether}(address(token), 4, 5 ether); + + // Updated Order: id3 --> id1 --> id2 --> id4 + (, , uint32 _prevId4, uint32 _nextId4, uint256 amount4) = offers.offers(address(token), 4); + + require(offers.ceilingOfferId(address(token)) == 4); + + require(_prevId4 == 2); + require(_nextId4 == 0); + require(amount4 == 5 ether); + } + + function test_IncreaseMiddleToMiddle() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); + + vm.prank(address(seller4)); + offers.setOfferAmount{value: 0.5 ether}(address(token), 4, 1.5 ether); + + // Updated Order: id3 --> id1 --> id4 --> id2 + (, , uint32 _prevId4, uint32 _nextId4, uint256 amount4) = offers.offers(address(token), 4); + + require(offers.ceilingOfferId(address(token)) == 2); + + require(_prevId4 == 1); + require(_nextId4 == 2); + require(amount4 == 1.5 ether); + } + + function test_IncreaseMiddleInPlace() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); + + vm.prank(address(seller)); + offers.setOfferAmount{value: 0.5 ether}(address(token), 1, 1.5 ether); + + (, , uint32 _prevId1, uint32 _nextId1, uint256 amount1) = offers.offers(address(token), 1); + + require(_prevId1 == 4); + require(_nextId1 == 2); + require(amount1 == 1.5 ether); + } + + function test_DecreaseMiddleToFloor() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); + + vm.prank(address(seller)); + offers.setOfferAmount(address(token), 1, 0.25 ether); + + (, , uint32 _prevId1, uint32 _nextId1, uint256 amount1) = offers.offers(address(token), 1); + + require(offers.floorOfferId(address(token)) == 1); + require(offers.floorOfferAmount(address(token)) == 0.25 ether); + + require(_prevId1 == 0); + require(_nextId1 == 3); + require(amount1 == 0.25 ether); + } + + function test_DecreaseMiddleToMiddle() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); + + vm.prank(address(seller)); + offers.setOfferAmount(address(token), 1, 0.75 ether); + + // Updated Order: id3 --> id1 --> id4 --> id2 + (, , uint32 _prevId1, uint32 _nextId1, uint256 amount1) = offers.offers(address(token), 1); + + require(_prevId1 == 3); + require(_nextId1 == 4); + require(amount1 == 0.75 ether); + } + + function test_DecreaseMiddleInPlace() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); + + vm.prank(address(seller4)); + offers.setOfferAmount(address(token), 4, 0.75 ether); + + (, , uint32 _prevId4, uint32 _nextId4, uint256 amount4) = offers.offers(address(token), 4); + + require(_prevId4 == 3); + require(_nextId4 == 1); + require(amount4 == 0.75 ether); + } + + function testRevert_UpdateOfferMustBeMaker() public { + loadOffers(); + + vm.prank(address(seller2)); + vm.expectRevert("setOfferAmount must be maker"); + offers.setOfferAmount(address(token), 1, 0.5 ether); + } + + /// ------------ CANCEL COLLECTION OFFER ------------ /// + + function test_CancelCollectionOffer() public { + vm.prank(address(seller)); + offers.createOffer{value: 1 ether}(address(token)); + + vm.warp(1 hours); + + uint256 beforeSellerBalance = address(seller).balance; + + vm.prank(address(seller)); + offers.cancelOffer(address(token), 1); + + uint256 afterSellerBalance = address(seller).balance; + require(afterSellerBalance - beforeSellerBalance == 1 ether); + } + + function testRevert_CancelOfferMustBeMaker() public { + vm.prank(address(seller)); + offers.createOffer{value: 1 ether}(address(token)); + + vm.expectRevert("cancelOffer must be maker"); + offers.cancelOffer(address(token), 1); + } + + /// ------------ FILL COLLECTION OFFER ------------ /// + + function test_FillCollectionOffer() public { + // Floor to Ceiling order: id3 --> id4 --> id1 --> id2 + loadOffers(); + + vm.prank(address(buyer)); + offers.fillOffer(address(token), 0, 2 ether, address(finder)); + + require(token.ownerOf(0) == address(seller2)); + + // Updated Order: id3 --> id4 --> id1 + require(offers.ceilingOfferId(address(token)) == 1); + require(offers.ceilingOfferAmount(address(token)) == 1 ether); + } + + function testRevert_MustOwnCollectionToken() public { + loadOffers(); + + vm.expectRevert("fillOffer must own specified token"); + offers.fillOffer(address(token), 0, 2 ether, address(finder)); + } + + function testRevert_FillMinimumTooHigh() public { + loadOffers(); + + vm.prank(address(buyer)); + vm.expectRevert("fillOffer offer satisfying _minAmount not found"); + offers.fillOffer(address(token), 0, 5 ether, address(finder)); + } + + /// ------------ SET FINDERS FEE ------------ /// + + function test_UpdateFindersFee() public { + require(offers.findersFeeBps() == 100); + + vm.prank(address(registrar)); + offers.setFindersFee(1000); + + require(offers.findersFeeBps() == 1000); + } + + function testRevert_UpdateFindersFeeMustBeRegistrar() public { + vm.expectRevert("setFindersFee only registrar"); + offers.setFindersFee(1000); + } + + function testRevert_FindersFeeCannotExceed10000() public { + vm.prank(address(registrar)); + vm.expectRevert("setFindersFee bps must be <= 10000"); + offers.setFindersFee(10001); + } +} diff --git a/uml/CollectionOffersV1/cancelOffer.atxt b/uml/CollectionOffersV1/cancelOffer.atxt new file mode 100644 index 00000000..db0cc481 --- /dev/null +++ b/uml/CollectionOffersV1/cancelOffer.atxt @@ -0,0 +1,29 @@ + ,-. + `-' + /|\ + | ,------------------. ,-------------------. + / \ |CollectionOffersV1| |ERC20TransferHelper| + Caller `--------+---------' `---------+---------' + | cancelOffer() | | + | ----------------------> | + | | | + | | call() | + | | ----------------------------------> + | | | + | | |----. + | | | | refund ETH from escrow + | | |<---' + | | | + | |----. + | | | emit CollectionOfferCanceled() + | |<---' + | | | + | |----. | + | | | _removeOffer() | + | |<---' | + Caller ,--------+---------. ,---------+---------. + ,-. |CollectionOffersV1| |ERC20TransferHelper| + `-' `------------------' `-------------------' + /|\ + | + / \ diff --git a/uml/CollectionOffersV1/cancelOffer.txt b/uml/CollectionOffersV1/cancelOffer.txt new file mode 100644 index 00000000..52534bfb --- /dev/null +++ b/uml/CollectionOffersV1/cancelOffer.txt @@ -0,0 +1,12 @@ +@startuml +actor Caller +participant CollectionOffersV1 +participant ERC20TransferHelper + +Caller -> CollectionOffersV1 : cancelOffer() +CollectionOffersV1 -> ERC20TransferHelper : call() +ERC20TransferHelper -> ERC20TransferHelper : refund ETH from escrow +CollectionOffersV1 -> CollectionOffersV1 : emit CollectionOfferCanceled() +CollectionOffersV1 -> CollectionOffersV1 : _removeOffer() + +@enduml \ No newline at end of file diff --git a/uml/CollectionOffersV1/createOffer.atxt b/uml/CollectionOffersV1/createOffer.atxt new file mode 100644 index 00000000..e2f2ae89 --- /dev/null +++ b/uml/CollectionOffersV1/createOffer.atxt @@ -0,0 +1,32 @@ + ,-. + `-' + /|\ + | ,------------------. ,-------------------. + / \ |CollectionOffersV1| |ERC20TransferHelper| + Caller `--------+---------' `---------+---------' + | createOffer() | | + | ----------------------> | + | | | + | | msg.value | + | | ---------------------------------> + | | | + | | |----. + | | | | transfer ETH into escrow + | | |<---' + | | | + | |----. | + | | | _addOffer() | + | |<---' | + | | | + | |----. + | | | emit CollectionOfferCreated() + | |<---' + | | | + | id | | + | <---------------------- | + Caller ,--------+---------. ,---------+---------. + ,-. |CollectionOffersV1| |ERC20TransferHelper| + `-' `------------------' `-------------------' + /|\ + | + / \ diff --git a/uml/CollectionOffersV1/createOffer.txt b/uml/CollectionOffersV1/createOffer.txt new file mode 100644 index 00000000..21d6718e --- /dev/null +++ b/uml/CollectionOffersV1/createOffer.txt @@ -0,0 +1,13 @@ +@startuml +actor Caller +participant CollectionOffersV1 +participant ERC20TransferHelper + +Caller -> CollectionOffersV1 : createOffer() +CollectionOffersV1 -> ERC20TransferHelper : msg.value +ERC20TransferHelper -> ERC20TransferHelper : transfer ETH into escrow +CollectionOffersV1 -> CollectionOffersV1 : _addOffer() +CollectionOffersV1 -> CollectionOffersV1 : emit CollectionOfferCreated() +CollectionOffersV1 -> Caller :id + +@enduml diff --git a/uml/CollectionOffersV1/fillOffer.atxt b/uml/CollectionOffersV1/fillOffer.atxt new file mode 100644 index 00000000..a1a7034d --- /dev/null +++ b/uml/CollectionOffersV1/fillOffer.atxt @@ -0,0 +1,59 @@ + ,-. + `-' + /|\ + | ,------------------. ,--------------------. + / \ |CollectionOffersV1| |ERC721TransferHelper| + Caller `--------+---------' `---------+----------' + | fillOffer() | | + | ----------------------> | + | | | + | |----. | + | | | validate token owner | + | |<---' | + | | | + | |----. | + | | | _getMatchingOffer() | + | |<---' | + | | | + | | | + | ________________________________________ | + | ! ALT / offer exists satisfying minimum? | + | !_____/ | ! | + | ! |----. ! | + | ! | | (continue) ! | + | ! |<---' ! | + | !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~! | + | !~[revert]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~! | + | | | + | |----. | + | | | handle royalty payouts | + | |<---' | + | | | + | |----. | + | | | handle finders fee payout | + | |<---' | + | | | + | | transferFrom() | + | | --------------------------------> + | | | + | | |----. + | | | | transfer NFT from taker to maker + | | |<---' + | | | + | |----. | + | | | emit ExchangeExecuted() | + | |<---' | + | | | + | |----. + | | | emit CollectionOfferFilled() + | |<---' + | | | + | |----. | + | | | _removeOffer() | + | |<---' | + Caller ,--------+---------. ,---------+----------. + ,-. |CollectionOffersV1| |ERC721TransferHelper| + `-' `------------------' `--------------------' + /|\ + | + / \ diff --git a/uml/CollectionOffersV1/fillOffer.txt b/uml/CollectionOffersV1/fillOffer.txt new file mode 100644 index 00000000..d386a540 --- /dev/null +++ b/uml/CollectionOffersV1/fillOffer.txt @@ -0,0 +1,30 @@ +@startuml +actor Caller +participant CollectionOffersV1 +participant ERC721TransferHelper + +Caller -> CollectionOffersV1 : fillOffer() + +CollectionOffersV1 -> CollectionOffersV1 : validate token owner + +CollectionOffersV1 -> CollectionOffersV1 : _getMatchingOffer() + +alt offer exists satisfying minimum? + + CollectionOffersV1 -> CollectionOffersV1 : (continue) + +else revert + +end + +CollectionOffersV1 -> CollectionOffersV1 : handle royalty payouts +CollectionOffersV1 -> CollectionOffersV1 : handle finders fee payout + +CollectionOffersV1 -> ERC721TransferHelper : transferFrom() +ERC721TransferHelper -> ERC721TransferHelper : transfer NFT from taker to maker + +CollectionOffersV1 -> CollectionOffersV1 : emit ExchangeExecuted() +CollectionOffersV1 -> CollectionOffersV1 : emit CollectionOfferFilled() +CollectionOffersV1 -> CollectionOffersV1 : _removeOffer() + +@enduml \ No newline at end of file diff --git a/uml/CollectionOffersV1/setFindersFee.atxt b/uml/CollectionOffersV1/setFindersFee.atxt new file mode 100644 index 00000000..777b9006 --- /dev/null +++ b/uml/CollectionOffersV1/setFindersFee.atxt @@ -0,0 +1,22 @@ + ,-. + `-' + /|\ + | ,------------------. + / \ |CollectionOffersV1| + zoraDAO `--------+---------' + | setFindersFee() | + |---------------------->| + | | + | |----. + | | | update finders fee + | |<---' + | | + | |----. + | | | emit FindersFeeUpdated() + | |<---' + zoraDAO ,--------+---------. + ,-. |CollectionOffersV1| + `-' `------------------' + /|\ + | + / \ diff --git a/uml/CollectionOffersV1/setFindersFee.txt b/uml/CollectionOffersV1/setFindersFee.txt new file mode 100644 index 00000000..fdeefa4c --- /dev/null +++ b/uml/CollectionOffersV1/setFindersFee.txt @@ -0,0 +1,9 @@ +@startuml +actor zoraDAO +participant CollectionOffersV1 + +zoraDAO -> CollectionOffersV1 : setFindersFee() +CollectionOffersV1 -> CollectionOffersV1 : update finders fee +CollectionOffersV1 -> CollectionOffersV1 : emit FindersFeeUpdated() + +@enduml diff --git a/uml/CollectionOffersV1/setOfferAmount.atxt b/uml/CollectionOffersV1/setOfferAmount.atxt new file mode 100644 index 00000000..4492471e --- /dev/null +++ b/uml/CollectionOffersV1/setOfferAmount.atxt @@ -0,0 +1,34 @@ + ,-. + `-' + /|\ + | ,------------------. ,-------------------. + / \ |CollectionOffersV1| |ERC20TransferHelper| + Caller `--------+---------' `---------+---------' + | setOfferAmount() | | + | ----------------------> | + | | | + | | | + | __________________________________________________________________________ + | ! ALT / increase offer? | ! + | !_____/ | | ! + | ! | transfer msg.value to escrow | ! + | ! | ---------------------------------> ! + | !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~! + | ! [decrease offer] | | ! + | ! | refund decrease amount | ! + | ! | ---------------------------------> ! + | !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~! + | | | + | |----. | + | | | _updateOffer() | + | |<---' | + | | | + | |----. + | | | emit CollectionOfferUpdated() + | |<---' + Caller ,--------+---------. ,---------+---------. + ,-. |CollectionOffersV1| |ERC20TransferHelper| + `-' `------------------' `-------------------' + /|\ + | + / \ diff --git a/uml/CollectionOffersV1/setOfferAmount.txt b/uml/CollectionOffersV1/setOfferAmount.txt new file mode 100644 index 00000000..c760a85e --- /dev/null +++ b/uml/CollectionOffersV1/setOfferAmount.txt @@ -0,0 +1,22 @@ +@startuml +actor Caller +participant CollectionOffersV1 +participant ERC20TransferHelper + +Caller -> CollectionOffersV1 : setOfferAmount() + +alt increase offer? + + CollectionOffersV1 -> ERC20TransferHelper : transfer msg.value to escrow + +else decrease offer + + CollectionOffersV1 -> ERC20TransferHelper : refund decrease amount + +end + +CollectionOffersV1 -> CollectionOffersV1 : _updateOffer() + +CollectionOffersV1 -> CollectionOffersV1 : emit CollectionOfferUpdated() + +@enduml \ No newline at end of file