From 469faf1c375c0ea31e62bae0533259b1a05ffcad Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Tue, 25 Jan 2022 17:23:34 -0500 Subject: [PATCH 1/9] [feat] Collection Offers v1.0 --- .../V1/CollectionOfferBookV1.sol | 437 ++++++++++++++++++ .../V1/CollectionOffersV1.sol | 233 ++++++++++ .../V1/CollectionOffers.integration.t.sol | 168 +++++++ .../V1/CollectionOffers.t.sol | 388 ++++++++++++++++ 4 files changed, 1226 insertions(+) create mode 100644 contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol create mode 100644 contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol create mode 100644 contracts/test/modules/CollectionOffers/V1/CollectionOffers.integration.t.sol create mode 100644 contracts/test/modules/CollectionOffers/V1/CollectionOffers.t.sol diff --git a/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol b/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol new file mode 100644 index 00000000..8cfca419 --- /dev/null +++ b/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol @@ -0,0 +1,437 @@ +// 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 + uint256 public offerCount; + + /// @notice The metadata of a collection offer + /// @param seller The address of the seller that placed the offer + /// @param amount The amount of ETH offered + /// @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 + struct Offer { + address seller; + uint256 amount; + uint256 id; + uint256 prevId; + uint256 nextId; + } + + /// ------------ PUBLIC STORAGE ------------ + + /// @notice The metadata for a given collection offer + /// @dev ERC-721 token address => ERC-721 token ID => 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; + + /// @notice The finders fee bps (if overriden) for a given collection offer + /// @notice ERC-721 token address => Offer ID => Finders Fee BPS + mapping(address => mapping(uint256 => uint16)) public findersFeeOverrides; + + /// ------------ INTERNAL FUNCTIONS ------------ + + /// @notice Creates and places a new offer in its collection's offer book + /// @param _offerAmount The amount of ETH offered + /// @param _seller The address of the seller + /// @return The ID of the created collection offer + function _addOffer( + address _collection, + uint256 _offerAmount, + address _seller + ) internal returns (uint256) { + offerCount++; + + // If its the first offer for a collection, mark it as both floor and ceiling + if (_isFirstOffer(_collection)) { + offers[_collection][offerCount] = Offer({seller: _seller, amount: _offerAmount, id: offerCount, prevId: 0, nextId: 0}); + + floorOfferId[_collection] = offerCount; + floorOfferAmount[_collection] = _offerAmount; + + ceilingOfferId[_collection] = offerCount; + ceilingOfferAmount[_collection] = _offerAmount; + + // Else if offer is greater than current ceiling, make it the new ceiling + } else if (_isNewCeiling(_collection, _offerAmount)) { + uint256 prevCeilingId = ceilingOfferId[_collection]; + + offers[_collection][prevCeilingId].nextId = offerCount; + offers[_collection][offerCount] = Offer({seller: _seller, amount: _offerAmount, id: offerCount, prevId: prevCeilingId, nextId: 0}); + + ceilingOfferId[_collection] = offerCount; + ceilingOfferAmount[_collection] = _offerAmount; + + // Else if offer is less than or equal to the current floor, make it the new floor + } else if (_isNewFloor(_collection, _offerAmount)) { + uint256 prevFloorId = floorOfferId[_collection]; + + offers[_collection][prevFloorId].prevId = offerCount; + offers[_collection][offerCount] = Offer({seller: _seller, amount: _offerAmount, id: offerCount, prevId: 0, nextId: prevFloorId}); + + floorOfferId[_collection] = offerCount; + floorOfferAmount[_collection] = _offerAmount; + + // Else offer is between the floor and ceiling -- + } else { + // Start at the floor + Offer memory offer = offers[_collection][floorOfferId[_collection]]; + + // Traverse towards the ceiling, stop when an offer greater than or equal to (time priority) is reached; insert before + while ((offer.amount < _offerAmount) && (offer.nextId != 0)) { + offer = offers[_collection][offer.nextId]; + } + + offers[_collection][offerCount] = Offer({seller: _seller, amount: _offerAmount, id: offerCount, prevId: offer.prevId, nextId: offer.id}); + + // Update neighboring pointers + offers[_collection][offer.id].prevId = offerCount; + offers[_collection][offer.prevId].nextId = offerCount; + } + + return offerCount; + } + + /// @notice Updates an offer and (if needed) its location relative to other offers in the collection + /// @param _collection 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( + address _collection, + uint256 _offerId, + uint256 _newAmount, + bool _increase + ) internal { + // If the offer to update is the only offer in the collection, update the floor and ceiling amounts as well + if (_isOnlyOffer(_collection, _offerId)) { + offers[_collection][_offerId].amount = _newAmount; + floorOfferAmount[_collection] = _newAmount; + ceilingOfferAmount[_collection] = _newAmount; + + // Else if the offer is the ceiling and the update is an increase, just update the ceiling + } else if (_isCeilingIncrease(_collection, _offerId, _increase)) { + offers[_collection][_offerId].amount = _newAmount; + ceilingOfferAmount[_collection] = _newAmount; + + // Else if the offer is the floor and the update is a decrease, just update the floor + } else if (_isFloorDecrease(_collection, _offerId, _increase)) { + offers[_collection][_offerId].amount = _newAmount; + floorOfferAmount[_collection] = _newAmount; + + // Else if the offer is still at the correct location, just update its amount + } else if (_isUpdateInPlace(_collection, _offerId, _newAmount, _increase)) { + offers[_collection][_offerId].amount = _newAmount; + + // Else if the offer is the new ceiling -- + } else if (_isNewCeiling(_collection, _newAmount)) { + uint256 prevId = offers[_collection][_offerId].prevId; + uint256 nextId = offers[_collection][_offerId].nextId; + + // Update previous neighbors + _connectPreviousNeighbors(_collection, _offerId, prevId, nextId); + + // Update previous ceiling + uint256 prevCeilingId = ceilingOfferId[_collection]; + offers[_collection][prevCeilingId].nextId = _offerId; + + // Update offer as new ceiling + offers[_collection][_offerId].prevId = prevCeilingId; + offers[_collection][_offerId].nextId = 0; + offers[_collection][_offerId].amount = _newAmount; + + // Update collection ceiling + ceilingOfferId[_collection] = _offerId; + ceilingOfferAmount[_collection] = _newAmount; + + // Else if the offer is the new floor -- + } else if (_isNewFloor(_collection, _newAmount)) { + uint256 prevId = offers[_collection][_offerId].prevId; + uint256 nextId = offers[_collection][_offerId].nextId; + + // Update previous neighbors + _connectPreviousNeighbors(_collection, _offerId, prevId, nextId); + + // Update previous floor + uint256 prevFloorId = floorOfferId[_collection]; + offers[_collection][prevFloorId].prevId = _offerId; + + // Update offer as new floor + offers[_collection][_offerId].nextId = prevFloorId; + offers[_collection][_offerId].prevId = 0; + offers[_collection][_offerId].amount = _newAmount; + + // Update collection floor + floorOfferId[_collection] = _offerId; + floorOfferAmount[_collection] = _newAmount; + + // Else move the offer to the apt middle location + } else { + Offer memory offer = offers[_collection][_offerId]; + + // Update previous neighbors + _connectPreviousNeighbors(_collection, _offerId, offer.prevId, offer.nextId); + + if (_increase) { + // Traverse forward until the apt location is found + _insertIncreasedOffer(offer, _collection, _offerId, _newAmount); + } else { + // Traverse backward until the apt location is found + _insertDecreasedOffer(offer, _collection, _offerId, _newAmount); + } + } + } + + /// @notice Removes an offer from its collection's offer book + /// @param _collection The ERC-721 collection + /// @param _offerId The ID of the offer + function _removeOffer(address _collection, uint256 _offerId) internal { + // If the offer to remove is the only one for its collection, remove it and reset associated collection data stored + 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 = offer.prevId; + offers[_collection][offer.prevId].nextId = offer.nextId; + + delete offers[_collection][_offerId]; + } + } + + /// @notice Finds a collection offer to fill + /// @param _collection 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 seller's minimum, return its id to fill + if (ceilingOfferAmount[_collection] >= _minAmount) { + return ceilingOfferId[_collection]; + // Else notify seller that no offer fitting their specified minimum exists + } else { + return 0; + } + } + + /// ------------ PRIVATE FUNCTIONS ------------ + + /// @notice Checks whether any offers exist for a collection + /// @param _collection 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 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 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 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 ERC-721 collection + /// @param _offerAmount The offer amount + function _isNewCeiling(address _collection, uint256 _offerAmount) private view returns (bool) { + return (_offerAmount > ceilingOfferAmount[_collection]); + } + + /// @notice Checks whether an offer is less than or equal to the collection floor + /// @param _collection The ERC-721 collection + /// @param _offerAmount The offer amount + function _isNewFloor(address _collection, uint256 _offerAmount) private view returns (bool) { + return (_offerAmount <= floorOfferAmount[_collection]); + } + + /// @notice Checks whether an offer to increase is the collection ceiling + /// @param _collection The ERC-721 collection + /// @param _offerId The ID of the offer + /// @param _increase Whether the update is an amount increase or decrease + function _isCeilingIncrease( + address _collection, + uint256 _offerId, + bool _increase + ) private view returns (bool) { + return (_offerId == ceilingOfferId[_collection]) && (_increase == true); + } + + /// @notice Checks whether an offer to decrease is the collection floor + /// @param _collection The ERC-721 collection + /// @param _offerId The ID of the offer + /// @param _increase Whether the update is an amount increase or decrease + function _isFloorDecrease( + address _collection, + uint256 _offerId, + bool _increase + ) private view returns (bool) { + return (_offerId == floorOfferId[_collection]) && (_increase == false); + } + + /// @notice Checks whether an offer can be updated without relocation + /// @param _collection 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 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 _connectPreviousNeighbors( + address _collection, + uint256 _offerId, + uint256 _prevId, + uint256 _nextId + ) private { + if (_offerId == floorOfferId[_collection]) { + offers[_collection][_nextId].prevId = 0; + + floorOfferId[_collection] = _nextId; + floorOfferAmount[_collection] = offers[_collection][_nextId].amount; + } else if (_offerId == ceilingOfferId[_collection]) { + offers[_collection][_prevId].nextId = 0; + + ceilingOfferId[_collection] = _prevId; + ceilingOfferAmount[_collection] = offers[_collection][_prevId].amount; + } else { + offers[_collection][_nextId].prevId = _prevId; + offers[_collection][_prevId].nextId = _nextId; + } + } + + /// @notice Updates the location of an increased offer + /// @param offer The Offer associated with _offerId + /// @param _collection The ERC-721 collection + /// @param _offerId The ID of the offer + /// @param _newAmount The new offer amount + function _insertIncreasedOffer( + Offer memory offer, + address _collection, + uint256 _offerId, + uint256 _newAmount + ) private { + offer = offers[_collection][offer.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 = offer.id; + offers[_collection][_offerId].prevId = offer.prevId; + + // Update neighbor pointers + offers[_collection][offer.id].prevId = _offerId; + offers[_collection][offer.prevId].nextId = _offerId; + + // Update offer amount + offers[_collection][_offerId].amount = _newAmount; + } + + /// @notice Updates the location of a decreased offer + /// @param offer The Offer associated with _offerId + /// @param _collection The ERC-721 collection + /// @param _offerId The ID of the offer + /// @param _newAmount The new offer amount + function _insertDecreasedOffer( + Offer memory offer, + address _collection, + uint256 _offerId, + uint256 _newAmount + ) private { + offer = offers[_collection][offer.prevId]; + + // Traverse backwards until the apt location is found + while ((offer.amount >= _newAmount) && (offer.prevId != 0)) { + offer = offers[_collection][offer.prevId]; + } + + // Update offer pointers + offers[_collection][_offerId].prevId = offer.id; + offers[_collection][_offerId].nextId = offer.nextId; + + // Update neighbor pointers + offers[_collection][offer.id].nextId = _offerId; + offers[_collection][offer.nextId].prevId = _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..729d58ed --- /dev/null +++ b/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol @@ -0,0 +1,233 @@ +// 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 sell ETH for any ERC-721 token in a specified collection +contract CollectionOffersV1 is + ReentrancyGuard, + UniversalExchangeEventV1, + IncomingTransferSupportV1, + FeePayoutSupportV1, + ModuleNamingSupportV1, + CollectionOfferBookV1 +{ + /// @dev The indicator to denominate all transfers in ETH + address private constant ETH = address(0); + /// @dev The indicator to pass all remaining gas when paying out royalties + uint256 private constant USE_ALL_GAS_FLAG = 0; + + /// @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 id The ID of the created offer + /// @param offer The metadata of the created offer + event CollectionOfferCreated(address indexed collection, uint256 indexed id, Offer offer); + + /// @notice Emitted when a collection offer is updated + /// @param collection The ERC-721 token address of the updated offer + /// @param id The ID of the updated offer + /// @param offer The metadata of the updated offer + event CollectionOfferPriceUpdated(address indexed collection, uint256 indexed id, Offer offer); + + /// @notice Emitted when the finders fee for a collection offer is updated + /// @param collection The ERC-721 token address of the updated offer + /// @param id The ID of the updated offer + /// @param findersFeeBps The bps of the updated finders fee + /// @param offer The metadata of the updated offer + event CollectionOfferFindersFeeUpdated(address indexed collection, uint256 indexed id, uint16 indexed findersFeeBps, Offer offer); + + /// @notice Emitted when a collection offer is canceled + /// @param collection The ERC-721 token address of the canceled offer + /// @param id The ID of the canceled offer + /// @param offer The metadata of the canceled offer + event CollectionOfferCanceled(address indexed collection, uint256 indexed id, Offer offer); + + /// @notice Emitted when a collection offer is filled + /// @param collection The ERC-721 token address of the filled offer + /// @param id The ID of the filled offer + /// @param buyer The address of the buyer who filled the offer + /// @param finder The address of the finder who referred the sale + /// @param offer The metadata of the canceled offer + event CollectionOfferFilled(address indexed collection, uint256 indexed id, address buyer, address finder, Offer offer); + + /// ------------ 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 _wethAddress WETH token address + constructor( + address _erc20TransferHelper, + address _erc721TransferHelper, + address _royaltyEngine, + address _protocolFeeSettings, + address _wethAddress + ) + IncomingTransferSupportV1(_erc20TransferHelper) + FeePayoutSupportV1(_royaltyEngine, _protocolFeeSettings, _wethAddress, ERC721TransferHelper(_erc721TransferHelper).ZMM().registrar()) + ModuleNamingSupportV1("Collection Offers: v1.0") + { + erc721TransferHelper = ERC721TransferHelper(_erc721TransferHelper); + } + + /// ------------ SELLER FUNCTIONS ------------ + + /// @notice Places an offer for any NFT in a collection + /// @param _tokenContract The ERC-721 collection address + /// @return The ID of the created offer + function createCollectionOffer(address _tokenContract) external payable nonReentrant returns (uint256) { + require(msg.value > 0, "createCollectionOffer msg value must be greater than 0"); + + // Ensure offer is valid and take custody + _handleIncomingTransfer(msg.value, ETH); + + uint256 offerId = _addOffer(_tokenContract, msg.value, msg.sender); + + emit CollectionOfferCreated(_tokenContract, offerId, offers[_tokenContract][offerId]); + + return offerId; + } + + /// @notice Updates the price of a collection offer + /// @param _tokenContract The address of the ERC-721 collection + /// @param _offerId The ID of the created offer + /// @param _newAmount The new offer + function setCollectionOfferAmount( + address _tokenContract, + uint256 _offerId, + uint256 _newAmount + ) external payable nonReentrant { + require(msg.sender == offers[_tokenContract][_offerId].seller, "setCollectionOfferAmount offer must be active & msg sender must be seller"); + require( + (_newAmount > 0) && (_newAmount != offers[_tokenContract][_offerId].amount), + "setCollectionOfferAmount _newAmount must be greater than 0 and not equal to previous offer" + ); + uint256 prevAmount = offers[_tokenContract][_offerId].amount; + + if (_newAmount > prevAmount) { + uint256 increaseAmount = _newAmount - prevAmount; + require(msg.value == increaseAmount, "setCollectionOfferAmount must send exact increase amount"); + + _handleIncomingTransfer(increaseAmount, ETH); + _updateOffer(_tokenContract, _offerId, _newAmount, true); + } else if (_newAmount < prevAmount) { + uint256 decreaseAmount = prevAmount - _newAmount; + + _handleOutgoingTransfer(msg.sender, decreaseAmount, ETH, USE_ALL_GAS_FLAG); + _updateOffer(_tokenContract, _offerId, _newAmount, false); + } + + emit CollectionOfferPriceUpdated(_tokenContract, _offerId, offers[_tokenContract][_offerId]); + } + + /// @notice Updates the finders fee of a collection offer + /// @param _tokenContract The address of the ERC-721 collection + /// @param _offerId The ID of the created offer + /// @param _findersFeeBps The new finders fee bps + function setCollectionOfferFindersFee( + address _tokenContract, + uint256 _offerId, + uint16 _findersFeeBps + ) external nonReentrant { + require(msg.sender == offers[_tokenContract][_offerId].seller, "setCollectionOfferFindersFee msg sender must be seller"); + require((_findersFeeBps > 1) && (_findersFeeBps <= 10000), "setCollectionOfferFindersFee must be less than or equal to 10000 bps"); + + findersFeeOverrides[_tokenContract][_offerId] = _findersFeeBps; + + emit CollectionOfferFindersFeeUpdated(_tokenContract, _offerId, _findersFeeBps, offers[_tokenContract][_offerId]); + } + + /// @notice Cancels a collection offer + /// @param _tokenContract The address of the ERC-721 collection + /// @param _offerId The ID of the created offer + function cancelCollectionOffer(address _tokenContract, uint256 _offerId) external nonReentrant { + require(msg.sender == offers[_tokenContract][_offerId].seller, "cancelCollectionOffer offer must be active & msg sender must be seller"); + + // Refund offered amount + _handleOutgoingTransfer(msg.sender, offers[_tokenContract][_offerId].amount, ETH, USE_ALL_GAS_FLAG); + + emit CollectionOfferCanceled(_tokenContract, _offerId, offers[_tokenContract][_offerId]); + + _removeOffer(_tokenContract, _offerId); + } + + /// ------------ BUYER FUNCTIONS ------------ + + /// @notice Fills the highest collection offer available, if above the desired minimum + /// @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 referrer for this sale + function fillCollectionOffer( + address _tokenContract, + uint256 _tokenId, + uint256 _minAmount, + address _finder + ) external nonReentrant { + require(_finder != address(0), "fillCollectionOffer _finder must not be 0 address"); + require(msg.sender == IERC721(_tokenContract).ownerOf(_tokenId), "fillCollectionOffer msg sender must own specified token"); + + // Get matching offer (if exists) + uint256 offerId = _getMatchingOffer(_tokenContract, _minAmount); + require(offerId > 0, "fillCollectionOffer offer satisfying specified _minAmount not found"); + + Offer memory offer = offers[_tokenContract][offerId]; + + // Ensure royalties are honored + (uint256 remainingProfit, ) = _handleRoyaltyPayout(_tokenContract, _tokenId, offer.amount, ETH, USE_ALL_GAS_FLAG); + + // Payout optional protocol fee + remainingProfit = _handleProtocolFeePayout(remainingProfit, ETH); + + // Payout optional finder fee + if (_finder != address(0)) { + uint256 findersFee; + + // If override exists -- + if (findersFeeOverrides[_tokenContract][offerId] != 0) { + // Calculate with override + findersFee = (remainingProfit * findersFeeOverrides[_tokenContract][offerId]) / 10000; + + // Else default 100 bps finders fee + } else { + findersFee = (remainingProfit * 100) / 10000; + } + + _handleOutgoingTransfer(_finder, findersFee, ETH, USE_ALL_GAS_FLAG); + + remainingProfit -= findersFee; + } + + // Transfer remaining ETH to buyer + _handleOutgoingTransfer(msg.sender, remainingProfit, ETH, USE_ALL_GAS_FLAG); + + // Transfer NFT to seller + erc721TransferHelper.transferFrom(_tokenContract, msg.sender, offer.seller, _tokenId); + + ExchangeDetails memory userAExchangeDetails = ExchangeDetails({tokenContract: ETH, tokenId: 0, amount: offer.amount}); + ExchangeDetails memory userBExchangeDetails = ExchangeDetails({tokenContract: _tokenContract, tokenId: _tokenId, amount: 1}); + + emit ExchangeExecuted(offer.seller, msg.sender, userAExchangeDetails, userBExchangeDetails); + emit CollectionOfferFilled(_tokenContract, offerId, msg.sender, _finder, offer); + + _removeOffer(_tokenContract, offerId); + } +} 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..a9086bf6 --- /dev/null +++ b/contracts/test/modules/CollectionOffers/V1/CollectionOffers.integration.t.sol @@ -0,0 +1,168 @@ +// 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; + + 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 + 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 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.createCollectionOffer{value: 1 ether}(address(token)); + } + + function fill() public { + vm.prank(address(seller)); + offers.createCollectionOffer{value: 1 ether}(address(token)); + + vm.prank(address(buyer)); + offers.fillCollectionOffer(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.setCollectionOfferAmount{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.setCollectionOfferAmount(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; + 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; + 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); + // 100 bps finders fee (Remaining 0.95 ETH * 10% finders fee = 0.0095 ETH) + require((afterFinderBalance - beforeFinderBalance) == 0.0095 ether); + // Remaining 0.9405 ETH paid to buyer + require((afterBuyerBalance - beforeBuyerBalance) == 0.9405 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..087d0065 --- /dev/null +++ b/contracts/test/modules/CollectionOffers/V1/CollectionOffers.t.sol @@ -0,0 +1,388 @@ +// 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.createCollectionOffer{value: 1 ether}(address(token)); + // Ceiling offer + vm.prank(address(seller2)); + offers.createCollectionOffer{value: 2 ether}(address(token)); + // Floor offer + vm.prank(address(seller3)); + offers.createCollectionOffer{value: 0.5 ether}(address(token)); + // Middle offer + vm.prank(address(seller4)); + offers.createCollectionOffer{value: 1 ether}(address(token)); + + // Floor to Ceiling order: id3 --> id4 --> id1 --> id2 + } + + /// ------------ CREATE COLLECTION OFFER ------------ /// + + function testGas_CreateFirstCollectionOffer() public { + offers.createCollectionOffer{value: 1 ether}(address(token)); + } + + function test_CreateCollectionOffer() public { + vm.prank(address(seller)); + offers.createCollectionOffer{value: 1 ether}(address(token)); + + (address offeror, uint256 amount, uint256 id, uint256 _prevId, uint256 _nextId) = 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.createCollectionOffer{value: 1 ether}(address(token)); + // Ceiling offer + vm.prank(address(seller2)); + offers.createCollectionOffer{value: 2 ether}(address(token)); + + (address offeror1, uint256 amount1, uint256 id1, uint256 _prevId1, uint256 _nextId1) = offers.offers(address(token), 1); + (address offeror2, uint256 amount2, uint256 id2, uint256 _prevId2, uint256 _nextId2) = 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.createCollectionOffer{value: 1 ether}(address(token)); + // Ceiling offer + vm.prank(address(seller2)); + offers.createCollectionOffer{value: 2 ether}(address(token)); + // Floor offer + vm.prank(address(seller3)); + offers.createCollectionOffer{value: 0.5 ether}(address(token)); + + (address offeror1, uint256 amount1, uint256 id1, uint256 _prevId1, uint256 _nextId1) = offers.offers(address(token), 1); + (address offeror2, uint256 amount2, uint256 id2, uint256 _prevId2, uint256 _nextId2) = offers.offers(address(token), 2); + (address offeror3, uint256 amount3, uint256 id3, uint256 _prevId3, uint256 _nextId3) = 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 { + loadOffers(); + + // Floor to ceiling order: id3 --> id4 --> id1 --> id2 + (address offeror4, uint256 amount4, , uint256 _prevId4, uint256 _nextId4) = offers.offers(address(token), 4); + uint256 floorId = offers.floorOfferId(address(token)); + uint256 ceilingId = offers.ceilingOfferId(address(token)); + + // Ensure seller and amount are valid + require(offeror4 == address(seller4) && amount4 == 1 ether); + + // Ensure placement between offer 3 and 1 + require(_nextId4 == 1 && _prevId4 == 3); + // Ensure floor and ceiling ids are valid + require(ceilingId == 2 && floorId == 3); + } + + function testRevert_MustAttachFundsToCreateOffer() public { + vm.expectRevert("createCollectionOffer msg value must be greater than 0"); + offers.createCollectionOffer(address(token)); + } + + /// ------------ SET COLLECTION OFFER AMOUNT ------------ /// + + function test_IncreaseFloorToNewCeiling() public { + loadOffers(); + + // Increase floor offer to new ceiling + vm.prank(address(seller3)); + offers.setCollectionOfferAmount{value: 4.5 ether}(address(token), 3, 5 ether); + + // Updated order: id4 --> id1 --> id2 --> id3 + (, uint256 amount3, , uint256 _prevId3, uint256 _nextId3) = 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 { + loadOffers(); + + // Increase floor offer to equal ceiling + vm.prank(address(seller3)); + offers.setCollectionOfferAmount{value: 1.5 ether}(address(token), 3, 2 ether); + + // Updated order: id4 --> id1 --> id3 --> id2 + (, uint256 amount3, , uint256 _prevId3, uint256 _nextId3) = 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_IncreaseCeilingInPlace() public { + loadOffers(); + + vm.prank(address(seller2)); + offers.setCollectionOfferAmount{value: 3 ether}(address(token), 2, 5 ether); + + (, uint256 amount2, , uint256 _prevId2, uint256 _nextId2) = offers.offers(address(token), 2); + uint256 ceilingId = offers.ceilingOfferId(address(token)); + + require(ceilingId == 2); + require(_prevId2 == 1 && _nextId2 == 0); + require(amount2 == 5 ether); + } + + function testRevert_UpdateOfferMustBeSeller() public { + loadOffers(); + + vm.prank(address(seller2)); + vm.expectRevert("setCollectionOfferAmount offer must be active & msg sender must be seller"); + offers.setCollectionOfferAmount(address(token), 1, 0.5 ether); + } + + /// ------------ SET COLLECTION OFFER FINDERS FEE ------------ /// + + function test_UpdateFindersFee() public { + vm.startPrank(address(seller)); + offers.createCollectionOffer{value: 1 ether}(address(token)); + + vm.warp(1 hours); + + offers.setCollectionOfferFindersFee(address(token), 1, 1000); + + vm.stopPrank(); + + require(offers.findersFeeOverrides(address(token), 1) == 1000); + } + + function testRevert_UpdateFindersFeeMustBeSeller() public { + vm.prank(address(seller)); + offers.createCollectionOffer{value: 1 ether}(address(token)); + + vm.warp(1 hours); + + vm.expectRevert("setCollectionOfferFindersFee msg sender must be seller"); + offers.setCollectionOfferFindersFee(address(token), 1, 1000); + } + + function testRevert_UpdateFindersFeeMustBeValidBps() public { + vm.startPrank(address(seller)); + offers.createCollectionOffer{value: 1 ether}(address(token)); + + vm.warp(1 hours); + + vm.expectRevert("setCollectionOfferFindersFee must be less than or equal to 10000 bps"); + offers.setCollectionOfferFindersFee(address(token), 1, 10001); + + vm.stopPrank(); + } + + /// ------------ CANCEL COLLECTION OFFER ------------ /// + + function test_CancelCollectionOffer() public { + vm.prank(address(seller)); + offers.createCollectionOffer{value: 1 ether}(address(token)); + + vm.warp(1 hours); + + uint256 beforeSellerBalance = address(seller).balance; + + vm.prank(address(seller)); + offers.cancelCollectionOffer(address(token), 1); + + uint256 afterSellerBalance = address(seller).balance; + require(afterSellerBalance - beforeSellerBalance == 1 ether); + } + + function testRevert_CancelOfferMustBeSeller() public { + vm.prank(address(seller)); + offers.createCollectionOffer{value: 1 ether}(address(token)); + + vm.expectRevert("cancelCollectionOffer offer must be active & msg sender must be seller"); + offers.cancelCollectionOffer(address(token), 1); + } + + /// ------------ FILL COLLECTION OFFER ------------ /// + + function test_FillCollectionOffer() public { + loadOffers(); + + vm.prank(address(buyer)); + offers.fillCollectionOffer(address(token), 0, 2 ether, address(finder)); + + require(token.ownerOf(0) == address(seller2)); + } + + function testRevert_FillMinimumTooHigh() public { + loadOffers(); + + vm.prank(address(buyer)); + vm.expectRevert("fillCollectionOffer offer satisfying specified _minAmount not found"); + offers.fillCollectionOffer(address(token), 0, 5 ether, address(finder)); + } + + function testRevert_MustOwnCollectionToken() public { + loadOffers(); + + vm.expectRevert("fillCollectionOffer msg sender must own specified token"); + offers.fillCollectionOffer(address(token), 0, 2 ether, address(finder)); + } +} From f943878dceb9ab4df02d6fcafaa3d2a5b0d7d078 Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Wed, 9 Feb 2022 15:15:30 -0500 Subject: [PATCH 2/9] [feat] update for community review --- .../V1/CollectionOfferBookV1.sol | 299 +++++++------- .../V1/CollectionOffersV1.sol | 370 +++++++++++++----- .../V1/CollectionOffers.integration.t.sol | 10 +- .../V1/CollectionOffers.t.sol | 341 +++++++++++----- uml/CollectionOffersV1/cancelOffer.atxt | 29 ++ uml/CollectionOffersV1/cancelOffer.txt | 12 + uml/CollectionOffersV1/createOffer.atxt | 32 ++ uml/CollectionOffersV1/createOffer.txt | 13 + uml/CollectionOffersV1/fillOffer.atxt | 59 +++ uml/CollectionOffersV1/fillOffer.txt | 30 ++ uml/CollectionOffersV1/setFindersFee.atxt | 22 ++ uml/CollectionOffersV1/setFindersFee.txt | 9 + uml/CollectionOffersV1/setOfferAmount.atxt | 34 ++ uml/CollectionOffersV1/setOfferAmount.txt | 22 ++ 14 files changed, 934 insertions(+), 348 deletions(-) create mode 100644 uml/CollectionOffersV1/cancelOffer.atxt create mode 100644 uml/CollectionOffersV1/cancelOffer.txt create mode 100644 uml/CollectionOffersV1/createOffer.atxt create mode 100644 uml/CollectionOffersV1/createOffer.txt create mode 100644 uml/CollectionOffersV1/fillOffer.atxt create mode 100644 uml/CollectionOffersV1/fillOffer.txt create mode 100644 uml/CollectionOffersV1/setFindersFee.atxt create mode 100644 uml/CollectionOffersV1/setFindersFee.txt create mode 100644 uml/CollectionOffersV1/setOfferAmount.atxt create mode 100644 uml/CollectionOffersV1/setOfferAmount.txt diff --git a/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol b/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol index 8cfca419..91a4ae76 100644 --- a/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol +++ b/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol @@ -8,31 +8,31 @@ pragma solidity 0.8.10; /// @notice This module extension manages offers placed on ERC-721 collections contract CollectionOfferBookV1 { /// @notice The number of offers placed - uint256 public offerCount; + uint32 public offerCount; /// @notice The metadata of a collection offer - /// @param seller The address of the seller that placed the offer - /// @param amount The amount of ETH offered + /// @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 seller; + address maker; + uint32 id; + uint32 prevId; + uint32 nextId; uint256 amount; - uint256 id; - uint256 prevId; - uint256 nextId; } /// ------------ PUBLIC STORAGE ------------ /// @notice The metadata for a given collection offer - /// @dev ERC-721 token address => ERC-721 token ID => Offer ID => 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; + mapping(address => uint32) public floorOfferId; /// @notice The floor offer amount for a given collection /// @dev ERC-721 token address => Floor offer amount @@ -40,70 +40,69 @@ contract CollectionOfferBookV1 { /// @notice The ceiling offer ID for a given collection /// @dev ERC-721 token address => Ceiling offer ID - mapping(address => uint256) public ceilingOfferId; + mapping(address => uint32) public ceilingOfferId; /// @notice The ceiling offer amount for a given collection /// @dev ERC-721 token address => Ceiling offer amount mapping(address => uint256) public ceilingOfferAmount; - /// @notice The finders fee bps (if overriden) for a given collection offer - /// @notice ERC-721 token address => Offer ID => Finders Fee BPS - mapping(address => mapping(uint256 => uint16)) public findersFeeOverrides; - /// ------------ INTERNAL FUNCTIONS ------------ /// @notice Creates and places a new offer in its collection's offer book - /// @param _offerAmount The amount of ETH offered - /// @param _seller The address of the seller + /// @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 _offerAmount, - address _seller - ) internal returns (uint256) { - offerCount++; + uint256 _amount, + address _maker + ) internal returns (uint32) { + unchecked { + ++offerCount; + } - // If its the first offer for a collection, mark it as both floor and ceiling + // If first offer for a collection, mark as both floor and ceiling if (_isFirstOffer(_collection)) { - offers[_collection][offerCount] = Offer({seller: _seller, amount: _offerAmount, id: offerCount, prevId: 0, nextId: 0}); + offers[_collection][offerCount] = Offer({maker: _maker, amount: _amount, id: offerCount, prevId: 0, nextId: 0}); floorOfferId[_collection] = offerCount; - floorOfferAmount[_collection] = _offerAmount; + floorOfferAmount[_collection] = _amount; ceilingOfferId[_collection] = offerCount; - ceilingOfferAmount[_collection] = _offerAmount; + ceilingOfferAmount[_collection] = _amount; - // Else if offer is greater than current ceiling, make it the new ceiling - } else if (_isNewCeiling(_collection, _offerAmount)) { - uint256 prevCeilingId = ceilingOfferId[_collection]; + // Else if offer is greater than current ceiling, mark as new ceiling + } else if (_isNewCeiling(_collection, _amount)) { + uint32 prevCeilingId = ceilingOfferId[_collection]; offers[_collection][prevCeilingId].nextId = offerCount; - offers[_collection][offerCount] = Offer({seller: _seller, amount: _offerAmount, id: offerCount, prevId: prevCeilingId, nextId: 0}); + offers[_collection][offerCount] = Offer({maker: _maker, amount: _amount, id: offerCount, prevId: prevCeilingId, nextId: 0}); ceilingOfferId[_collection] = offerCount; - ceilingOfferAmount[_collection] = _offerAmount; + ceilingOfferAmount[_collection] = _amount; - // Else if offer is less than or equal to the current floor, make it the new floor - } else if (_isNewFloor(_collection, _offerAmount)) { - uint256 prevFloorId = floorOfferId[_collection]; + // Else if offer is less than or equal to current floor, mark as new floor + } else if (_isNewFloor(_collection, _amount)) { + uint32 prevFloorId = floorOfferId[_collection]; offers[_collection][prevFloorId].prevId = offerCount; - offers[_collection][offerCount] = Offer({seller: _seller, amount: _offerAmount, id: offerCount, prevId: 0, nextId: prevFloorId}); + offers[_collection][offerCount] = Offer({maker: _maker, amount: _amount, id: offerCount, prevId: 0, nextId: prevFloorId}); floorOfferId[_collection] = offerCount; - floorOfferAmount[_collection] = _offerAmount; + floorOfferAmount[_collection] = _amount; - // Else offer is between the floor and ceiling -- + // Else offer is between floor and ceiling -- } else { - // Start at the floor + // Start at floor Offer memory offer = offers[_collection][floorOfferId[_collection]]; - // Traverse towards the ceiling, stop when an offer greater than or equal to (time priority) is reached; insert before - while ((offer.amount < _offerAmount) && (offer.nextId != 0)) { + // 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]; } - offers[_collection][offerCount] = Offer({seller: _seller, amount: _offerAmount, id: offerCount, prevId: offer.prevId, nextId: offer.id}); + // Insert new offer before (time priority) + offers[_collection][offerCount] = Offer({maker: _maker, amount: _amount, id: offerCount, prevId: offer.prevId, nextId: offer.id}); // Update neighboring pointers offers[_collection][offer.id].prevId = offerCount; @@ -114,100 +113,110 @@ contract CollectionOfferBookV1 { } /// @notice Updates an offer and (if needed) its location relative to other offers in the collection - /// @param _collection The ERC-721 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, + uint32 _offerId, uint256 _newAmount, bool _increase ) internal { - // If the offer to update is the only offer in the collection, update the floor and ceiling amounts as well + // If offer to update is only offer for its collection -- if (_isOnlyOffer(_collection, _offerId)) { - offers[_collection][_offerId].amount = _newAmount; + // Update offer + _offer.amount = _newAmount; + // Update collection floor floorOfferAmount[_collection] = _newAmount; + // Update collection ceiling ceilingOfferAmount[_collection] = _newAmount; - // Else if the offer is the ceiling and the update is an increase, just update the ceiling - } else if (_isCeilingIncrease(_collection, _offerId, _increase)) { - offers[_collection][_offerId].amount = _newAmount; - ceilingOfferAmount[_collection] = _newAmount; - - // Else if the offer is the floor and the update is a decrease, just update the floor - } else if (_isFloorDecrease(_collection, _offerId, _increase)) { - offers[_collection][_offerId].amount = _newAmount; - floorOfferAmount[_collection] = _newAmount; - - // Else if the offer is still at the correct location, just update its amount + // Else if offer does not require relocation -- } else if (_isUpdateInPlace(_collection, _offerId, _newAmount, _increase)) { - offers[_collection][_offerId].amount = _newAmount; + 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 the offer is the new ceiling -- + // Else if offer is new ceiling -- } else if (_isNewCeiling(_collection, _newAmount)) { - uint256 prevId = offers[_collection][_offerId].prevId; - uint256 nextId = offers[_collection][_offerId].nextId; + // Get previous neighbors + uint32 prevId = _offer.prevId; + uint32 nextId = _offer.nextId; // Update previous neighbors - _connectPreviousNeighbors(_collection, _offerId, prevId, nextId); + _connectNeighbors(_collection, _offerId, prevId, nextId); // Update previous ceiling - uint256 prevCeilingId = ceilingOfferId[_collection]; + uint32 prevCeilingId = ceilingOfferId[_collection]; offers[_collection][prevCeilingId].nextId = _offerId; - // Update offer as new ceiling - offers[_collection][_offerId].prevId = prevCeilingId; - offers[_collection][_offerId].nextId = 0; - offers[_collection][_offerId].amount = _newAmount; + // Update offer to be new ceiling + _offer.prevId = prevCeilingId; + _offer.nextId = 0; + _offer.amount = _newAmount; // Update collection ceiling ceilingOfferId[_collection] = _offerId; ceilingOfferAmount[_collection] = _newAmount; - // Else if the offer is the new floor -- + // Else if offer is new floor -- } else if (_isNewFloor(_collection, _newAmount)) { - uint256 prevId = offers[_collection][_offerId].prevId; - uint256 nextId = offers[_collection][_offerId].nextId; + // Get previous neighbors + uint32 prevId = _offer.prevId; + uint32 nextId = _offer.nextId; // Update previous neighbors - _connectPreviousNeighbors(_collection, _offerId, prevId, nextId); + _connectNeighbors(_collection, _offerId, prevId, nextId); // Update previous floor - uint256 prevFloorId = floorOfferId[_collection]; + uint32 prevFloorId = floorOfferId[_collection]; offers[_collection][prevFloorId].prevId = _offerId; - // Update offer as new floor - offers[_collection][_offerId].nextId = prevFloorId; - offers[_collection][_offerId].prevId = 0; - offers[_collection][_offerId].amount = _newAmount; + // Update offer to be new floor + _offer.nextId = prevFloorId; + _offer.prevId = 0; + _offer.amount = _newAmount; // Update collection floor floorOfferId[_collection] = _offerId; floorOfferAmount[_collection] = _newAmount; - // Else move the offer to the apt middle location + // Else offer requires relocation between floor and ceiling -- } else { - Offer memory offer = offers[_collection][_offerId]; - // Update previous neighbors - _connectPreviousNeighbors(_collection, _offerId, offer.prevId, offer.nextId); + _connectNeighbors(_collection, _offerId, _offer.prevId, _offer.nextId); + // If update is increase -- if (_increase) { - // Traverse forward until the apt location is found - _insertIncreasedOffer(offer, _collection, _offerId, _newAmount); + // Traverse forward until insert location is found + _insertIncreasedOffer(_collection, _offerId, _offer.nextId, _newAmount); + // Else update is decrease -- } else { - // Traverse backward until the apt location is found - _insertDecreasedOffer(offer, _collection, _offerId, _newAmount); + // 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 ERC-721 collection + /// @param _collection The address of the ERC-721 collection /// @param _offerId The ID of the offer - function _removeOffer(address _collection, uint256 _offerId) internal { - // If the offer to remove is the only one for its collection, remove it and reset associated collection data stored + function _removeOffer(address _collection, uint32 _offerId) internal { + // If offer is only one for collection, remove all associated data if (_isOnlyOffer(_collection, _offerId)) { delete floorOfferId[_collection]; delete floorOfferAmount[_collection]; @@ -217,7 +226,7 @@ contract CollectionOfferBookV1 { // 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; + uint32 newFloorId = offers[_collection][_offerId].nextId; uint256 newFloorAmount = offers[_collection][newFloorId].amount; offers[_collection][newFloorId].prevId = 0; @@ -229,7 +238,7 @@ contract CollectionOfferBookV1 { // 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; + uint32 newCeilingId = offers[_collection][_offerId].prevId; uint256 newCeilingAmount = offers[_collection][newCeilingId].amount; offers[_collection][newCeilingId].nextId = 0; @@ -251,13 +260,13 @@ contract CollectionOfferBookV1 { } /// @notice Finds a collection offer to fill - /// @param _collection The ERC-721 collection + /// @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 seller's minimum, return its id to fill + function _getMatchingOffer(address _collection, uint256 _minAmount) internal view returns (uint32) { + // 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 notify seller that no offer fitting their specified minimum exists + // Else return no offer found } else { return 0; } @@ -266,127 +275,111 @@ contract CollectionOfferBookV1 { /// ------------ PRIVATE FUNCTIONS ------------ /// @notice Checks whether any offers exist for a collection - /// @param _collection The ERC-721 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 ERC-721 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) { + function _isOnlyOffer(address _collection, uint32 _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 ERC-721 collection + /// @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) { + function _isCeilingOffer(address _collection, uint32 _offerId) private view returns (bool) { return (_offerId == ceilingOfferId[_collection]); } /// @notice Checks whether a given offer is the collection floor - /// @param _collection The ERC-721 collection + /// @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) { + function _isFloorOffer(address _collection, uint32 _offerId) private view returns (bool) { return (_offerId == floorOfferId[_collection]); } /// @notice Checks whether an offer is greater than the collection ceiling - /// @param _collection The ERC-721 collection - /// @param _offerAmount The offer amount - function _isNewCeiling(address _collection, uint256 _offerAmount) private view returns (bool) { - return (_offerAmount > ceilingOfferAmount[_collection]); + /// @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 ERC-721 collection - /// @param _offerAmount The offer amount - function _isNewFloor(address _collection, uint256 _offerAmount) private view returns (bool) { - return (_offerAmount <= floorOfferAmount[_collection]); - } - - /// @notice Checks whether an offer to increase is the collection ceiling - /// @param _collection The ERC-721 collection - /// @param _offerId The ID of the offer - /// @param _increase Whether the update is an amount increase or decrease - function _isCeilingIncrease( - address _collection, - uint256 _offerId, - bool _increase - ) private view returns (bool) { - return (_offerId == ceilingOfferId[_collection]) && (_increase == true); - } - - /// @notice Checks whether an offer to decrease is the collection floor - /// @param _collection The ERC-721 collection - /// @param _offerId The ID of the offer - /// @param _increase Whether the update is an amount increase or decrease - function _isFloorDecrease( - address _collection, - uint256 _offerId, - bool _increase - ) private view returns (bool) { - return (_offerId == floorOfferId[_collection]) && (_increase == false); + /// @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 ERC-721 collection + /// @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, + uint32 _offerId, uint256 _newAmount, bool _increase ) private view returns (bool) { - uint256 nextOffer = offers[_collection][_offerId].nextId; - uint256 prevOffer = offers[_collection][_offerId].prevId; + uint32 nextOffer = offers[_collection][_offerId].nextId; + uint32 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 ERC-721 collection + /// @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 _connectPreviousNeighbors( + function _connectNeighbors( address _collection, - uint256 _offerId, - uint256 _prevId, - uint256 _nextId + uint32 _offerId, + uint32 _prevId, + uint32 _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 = _prevId; offers[_collection][_prevId].nextId = _nextId; } } /// @notice Updates the location of an increased offer - /// @param offer The Offer associated with _offerId - /// @param _collection The ERC-721 collection - /// @param _offerId The ID of the 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( - Offer memory offer, address _collection, - uint256 _offerId, + uint32 _offerId, + uint32 _nextId, uint256 _newAmount ) private { - offer = offers[_collection][offer.nextId]; + Offer memory offer = offers[_collection][_nextId]; // Traverse forward until the apt location is found while ((offer.amount < _newAmount) && (offer.nextId != 0)) { @@ -406,19 +399,19 @@ contract CollectionOfferBookV1 { } /// @notice Updates the location of a decreased offer - /// @param offer The Offer associated with _offerId - /// @param _collection The ERC-721 collection - /// @param _offerId The ID of the 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( - Offer memory offer, address _collection, - uint256 _offerId, + uint32 _offerId, + uint32 _prevId, uint256 _newAmount ) private { - offer = offers[_collection][offer.prevId]; + Offer memory offer = offers[_collection][_prevId]; - // Traverse backwards until the apt location is found + // Traverse backwards until apt location is found while ((offer.amount >= _newAmount) && (offer.prevId != 0)) { offer = offers[_collection][offer.prevId]; } diff --git a/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol b/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol index 729d58ed..f37921b5 100644 --- a/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol +++ b/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol @@ -16,7 +16,7 @@ import {CollectionOfferBookV1} from "./CollectionOfferBookV1.sol"; /// @title Collection Offers V1 /// @author kulkarohan -/// @notice This module allows users to sell ETH for any ERC-721 token in a specified collection +/// @notice This module allows users to offer ETH for any ERC-721 token in a specified collection contract CollectionOffersV1 is ReentrancyGuard, UniversalExchangeEventV1, @@ -29,6 +29,8 @@ contract CollectionOffersV1 is address private constant ETH = address(0); /// @dev The indicator to pass all remaining gas when paying out royalties uint256 private constant USE_ALL_GAS_FLAG = 0; + /// @notice The finders fee bps configured by the DAO + uint16 public findersFeeBps; /// @notice The ZORA ERC-721 Transfer Helper ERC721TransferHelper public immutable erc721TransferHelper; @@ -38,156 +40,295 @@ contract CollectionOffersV1 is /// @notice Emitted when a collection offer is created /// @param collection The ERC-721 token address of the created offer /// @param id The ID of the created offer - /// @param offer The metadata of the created offer - event CollectionOfferCreated(address indexed collection, uint256 indexed id, Offer offer); + /// @param maker The address of the offer maker + /// @param amount The amount of the created offer + event CollectionOfferCreated(address indexed collection, uint256 indexed id, address maker, uint256 amount); /// @notice Emitted when a collection offer is updated /// @param collection The ERC-721 token address of the updated offer /// @param id The ID of the updated offer - /// @param offer The metadata of the updated offer - event CollectionOfferPriceUpdated(address indexed collection, uint256 indexed id, Offer offer); - - /// @notice Emitted when the finders fee for a collection offer is updated - /// @param collection The ERC-721 token address of the updated offer - /// @param id The ID of the updated offer - /// @param findersFeeBps The bps of the updated finders fee - /// @param offer The metadata of the updated offer - event CollectionOfferFindersFeeUpdated(address indexed collection, uint256 indexed id, uint16 indexed findersFeeBps, Offer offer); + /// @param maker The address of the offer maker + /// @param amount The amount of the updated offer + event CollectionOfferUpdated(address indexed collection, uint256 indexed id, address maker, uint256 amount); /// @notice Emitted when a collection offer is canceled /// @param collection The ERC-721 token address of the canceled offer /// @param id The ID of the canceled offer - /// @param offer The metadata of the canceled offer - event CollectionOfferCanceled(address indexed collection, uint256 indexed id, Offer offer); + /// @param maker The address of the offer maker + /// @param amount The amount of the canceled offer + event CollectionOfferCanceled(address indexed collection, uint256 indexed id, 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 id The ID of the filled offer - /// @param buyer The address of the buyer who filled the offer + /// @param taker The address of the taker who filled the offer /// @param finder The address of the finder who referred the sale - /// @param offer The metadata of the canceled offer - event CollectionOfferFilled(address indexed collection, uint256 indexed id, address buyer, address finder, Offer offer); + event CollectionOfferFilled(address indexed collection, uint256 indexed tokenId, uint256 indexed id, 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 _wethAddress WETH token address + /// @param _weth The WETH token address constructor( address _erc20TransferHelper, address _erc721TransferHelper, address _royaltyEngine, address _protocolFeeSettings, - address _wethAddress + address _weth ) IncomingTransferSupportV1(_erc20TransferHelper) - FeePayoutSupportV1(_royaltyEngine, _protocolFeeSettings, _wethAddress, ERC721TransferHelper(_erc721TransferHelper).ZMM().registrar()) + FeePayoutSupportV1(_royaltyEngine, _protocolFeeSettings, _weth, ERC721TransferHelper(_erc721TransferHelper).ZMM().registrar()) ModuleNamingSupportV1("Collection Offers: v1.0") { erc721TransferHelper = ERC721TransferHelper(_erc721TransferHelper); + findersFeeBps = 100; } - /// ------------ SELLER FUNCTIONS ------------ - - /// @notice Places an offer for any NFT in a collection + /// ------------ 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 createCollectionOffer(address _tokenContract) external payable nonReentrant returns (uint256) { - require(msg.value > 0, "createCollectionOffer msg value must be greater than 0"); - + function createOffer(address _tokenContract) external payable nonReentrant returns (uint32) { // Ensure offer is valid and take custody _handleIncomingTransfer(msg.value, ETH); - uint256 offerId = _addOffer(_tokenContract, msg.value, msg.sender); + // Add to collection's offer book + uint32 offerId = _addOffer(_tokenContract, msg.value, msg.sender); - emit CollectionOfferCreated(_tokenContract, offerId, offers[_tokenContract][offerId]); + emit CollectionOfferCreated(_tokenContract, offerId, msg.sender, msg.value); return offerId; } - /// @notice Updates the price of a collection offer + // ,-. + // `-' + // /|\ + // | ,------------------. ,-------------------. + // / \ |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 created offer - /// @param _newAmount The new offer - function setCollectionOfferAmount( + /// @param _offerId The ID of the collection offer + /// @param _amount The new offer amount + function setOfferAmount( address _tokenContract, - uint256 _offerId, - uint256 _newAmount + uint32 _offerId, + uint256 _amount ) external payable nonReentrant { - require(msg.sender == offers[_tokenContract][_offerId].seller, "setCollectionOfferAmount offer must be active & msg sender must be seller"); - require( - (_newAmount > 0) && (_newAmount != offers[_tokenContract][_offerId].amount), - "setCollectionOfferAmount _newAmount must be greater than 0 and not equal to previous offer" - ); - uint256 prevAmount = offers[_tokenContract][_offerId].amount; - - if (_newAmount > prevAmount) { - uint256 increaseAmount = _newAmount - prevAmount; - require(msg.value == increaseAmount, "setCollectionOfferAmount must send exact increase amount"); - - _handleIncomingTransfer(increaseAmount, ETH); - _updateOffer(_tokenContract, _offerId, _newAmount, true); - } else if (_newAmount < prevAmount) { - uint256 decreaseAmount = prevAmount - _newAmount; - - _handleOutgoingTransfer(msg.sender, decreaseAmount, ETH, USE_ALL_GAS_FLAG); - _updateOffer(_tokenContract, _offerId, _newAmount, false); - } + Offer storage offer = offers[_tokenContract][_offerId]; - emit CollectionOfferPriceUpdated(_tokenContract, _offerId, 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"); - /// @notice Updates the finders fee of a collection offer - /// @param _tokenContract The address of the ERC-721 collection - /// @param _offerId The ID of the created offer - /// @param _findersFeeBps The new finders fee bps - function setCollectionOfferFindersFee( - address _tokenContract, - uint256 _offerId, - uint16 _findersFeeBps - ) external nonReentrant { - require(msg.sender == offers[_tokenContract][_offerId].seller, "setCollectionOfferFindersFee msg sender must be seller"); - require((_findersFeeBps > 1) && (_findersFeeBps <= 10000), "setCollectionOfferFindersFee must be less than or equal to 10000 bps"); + uint256 prevAmount = offer.amount; - findersFeeOverrides[_tokenContract][_offerId] = _findersFeeBps; + if (_amount > prevAmount) { + unchecked { + uint256 increaseAmount = _amount - prevAmount; + _handleIncomingTransfer(increaseAmount, ETH); + _updateOffer(offer, _tokenContract, _offerId, _amount, true); + } + } else { + unchecked { + uint256 decreaseAmount = prevAmount - _amount; + _handleOutgoingTransfer(msg.sender, decreaseAmount, ETH, USE_ALL_GAS_FLAG); + _updateOffer(offer, _tokenContract, _offerId, _amount, false); + } + } - emit CollectionOfferFindersFeeUpdated(_tokenContract, _offerId, _findersFeeBps, offers[_tokenContract][_offerId]); + emit CollectionOfferUpdated(_tokenContract, _offerId, msg.sender, _amount); } - /// @notice Cancels a collection offer + // ,-. + // `-' + // /|\ + // | ,------------------. ,-------------------. + // / \ |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 created offer - function cancelCollectionOffer(address _tokenContract, uint256 _offerId) external nonReentrant { - require(msg.sender == offers[_tokenContract][_offerId].seller, "cancelCollectionOffer offer must be active & msg sender must be seller"); + /// @param _offerId The ID of the collection offer + function cancelOffer(address _tokenContract, uint32 _offerId) external nonReentrant { + Offer memory offer = offers[_tokenContract][_offerId]; - // Refund offered amount - _handleOutgoingTransfer(msg.sender, offers[_tokenContract][_offerId].amount, ETH, USE_ALL_GAS_FLAG); + require(msg.sender == offer.maker, "cancelOffer must be maker"); - emit CollectionOfferCanceled(_tokenContract, _offerId, offers[_tokenContract][_offerId]); + // Refund offer + _handleOutgoingTransfer(msg.sender, offer.amount, ETH, USE_ALL_GAS_FLAG); + + emit CollectionOfferCanceled(_tokenContract, _offerId, msg.sender, offer.amount); _removeOffer(_tokenContract, _offerId); } - /// ------------ BUYER FUNCTIONS ------------ - - /// @notice Fills the highest collection offer available, if above the desired minimum + /// ------------ 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 referrer for this sale - function fillCollectionOffer( + /// @param _finder The address of the offer referrer + function fillOffer( address _tokenContract, uint256 _tokenId, uint256 _minAmount, address _finder ) external nonReentrant { - require(_finder != address(0), "fillCollectionOffer _finder must not be 0 address"); - require(msg.sender == IERC721(_tokenContract).ownerOf(_tokenId), "fillCollectionOffer msg sender must own specified token"); + 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, "fillCollectionOffer offer satisfying specified _minAmount not found"); + uint32 offerId = _getMatchingOffer(_tokenContract, _minAmount); + require(offerId != 0, "fillOffer offer satisfying _minAmount not found"); Offer memory offer = offers[_tokenContract][offerId]; @@ -200,34 +341,61 @@ contract CollectionOffersV1 is // Payout optional finder fee if (_finder != address(0)) { uint256 findersFee; - - // If override exists -- - if (findersFeeOverrides[_tokenContract][offerId] != 0) { - // Calculate with override - findersFee = (remainingProfit * findersFeeOverrides[_tokenContract][offerId]) / 10000; - - // Else default 100 bps finders fee - } else { - findersFee = (remainingProfit * 100) / 10000; - } - + // Calculate payout + findersFee = (remainingProfit * findersFeeBps) / 10000; + // Transfer to finder _handleOutgoingTransfer(_finder, findersFee, ETH, USE_ALL_GAS_FLAG); - + // Update remaining profit remainingProfit -= findersFee; } - // Transfer remaining ETH to buyer + // Transfer remaining ETH to taker _handleOutgoingTransfer(msg.sender, remainingProfit, ETH, USE_ALL_GAS_FLAG); - // Transfer NFT to seller - erc721TransferHelper.transferFrom(_tokenContract, msg.sender, offer.seller, _tokenId); + // Transfer NFT to maker + erc721TransferHelper.transferFrom(_tokenContract, msg.sender, offer.maker, _tokenId); ExchangeDetails memory userAExchangeDetails = ExchangeDetails({tokenContract: ETH, tokenId: 0, amount: offer.amount}); ExchangeDetails memory userBExchangeDetails = ExchangeDetails({tokenContract: _tokenContract, tokenId: _tokenId, amount: 1}); - emit ExchangeExecuted(offer.seller, msg.sender, userAExchangeDetails, userBExchangeDetails); - emit CollectionOfferFilled(_tokenContract, offerId, msg.sender, _finder, offer); + 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 index a9086bf6..0f4231ce 100644 --- a/contracts/test/modules/CollectionOffers/V1/CollectionOffers.integration.t.sol +++ b/contracts/test/modules/CollectionOffers/V1/CollectionOffers.integration.t.sol @@ -92,15 +92,15 @@ contract CollectionOffersV1IntegrationTest is DSTest { function offer() public { vm.prank(address(seller)); - offers.createCollectionOffer{value: 1 ether}(address(token)); + offers.createOffer{value: 1 ether}(address(token)); } function fill() public { vm.prank(address(seller)); - offers.createCollectionOffer{value: 1 ether}(address(token)); + offers.createOffer{value: 1 ether}(address(token)); vm.prank(address(buyer)); - offers.fillCollectionOffer(address(token), 0, 1 ether, address(finder)); + offers.fillOffer(address(token), 0, 1 ether, address(finder)); } function test_WithdrawOfferFromSeller() public { @@ -118,7 +118,7 @@ contract CollectionOffersV1IntegrationTest is DSTest { // Increase initial offer to 2 ETH vm.prank(address(seller)); - offers.setCollectionOfferAmount{value: 1 ether}(address(token), 1, 2 ether); + offers.setOfferAmount{value: 1 ether}(address(token), 1, 2 ether); uint256 afterBalance = address(seller).balance; @@ -132,7 +132,7 @@ contract CollectionOffersV1IntegrationTest is DSTest { // Decrease initial offer to 0.5 ETH vm.prank(address(seller)); - offers.setCollectionOfferAmount(address(token), 1, 0.5 ether); + offers.setOfferAmount(address(token), 1, 0.5 ether); uint256 afterBalance = address(seller).balance; diff --git a/contracts/test/modules/CollectionOffers/V1/CollectionOffers.t.sol b/contracts/test/modules/CollectionOffers/V1/CollectionOffers.t.sol index 087d0065..3ad79555 100644 --- a/contracts/test/modules/CollectionOffers/V1/CollectionOffers.t.sol +++ b/contracts/test/modules/CollectionOffers/V1/CollectionOffers.t.sol @@ -109,16 +109,16 @@ contract CollectionOffersV1Test is DSTest { function loadOffers() public { // First offer vm.prank(address(seller)); - offers.createCollectionOffer{value: 1 ether}(address(token)); + offers.createOffer{value: 1 ether}(address(token)); // Ceiling offer vm.prank(address(seller2)); - offers.createCollectionOffer{value: 2 ether}(address(token)); + offers.createOffer{value: 2 ether}(address(token)); // Floor offer vm.prank(address(seller3)); - offers.createCollectionOffer{value: 0.5 ether}(address(token)); + offers.createOffer{value: 0.5 ether}(address(token)); // Middle offer vm.prank(address(seller4)); - offers.createCollectionOffer{value: 1 ether}(address(token)); + offers.createOffer{value: 1 ether}(address(token)); // Floor to Ceiling order: id3 --> id4 --> id1 --> id2 } @@ -126,14 +126,14 @@ contract CollectionOffersV1Test is DSTest { /// ------------ CREATE COLLECTION OFFER ------------ /// function testGas_CreateFirstCollectionOffer() public { - offers.createCollectionOffer{value: 1 ether}(address(token)); + offers.createOffer{value: 1 ether}(address(token)); } function test_CreateCollectionOffer() public { vm.prank(address(seller)); - offers.createCollectionOffer{value: 1 ether}(address(token)); + offers.createOffer{value: 1 ether}(address(token)); - (address offeror, uint256 amount, uint256 id, uint256 _prevId, uint256 _nextId) = offers.offers(address(token), 1); + (address offeror, uint32 id, uint32 _prevId, uint32 _nextId, uint256 amount) = offers.offers(address(token), 1); require(offeror == address(seller)); require(id == 1); @@ -141,9 +141,9 @@ contract CollectionOffersV1Test is DSTest { require(_prevId == 0); require(_nextId == 0); - uint256 floorId = offers.floorOfferId(address(token)); + uint32 floorId = offers.floorOfferId(address(token)); uint256 floorAmt = offers.floorOfferAmount(address(token)); - uint256 ceilingId = offers.ceilingOfferId(address(token)); + uint32 ceilingId = offers.ceilingOfferId(address(token)); uint256 ceilingAmt = offers.ceilingOfferAmount(address(token)); require(floorId == 1); @@ -155,13 +155,13 @@ contract CollectionOffersV1Test is DSTest { function test_CreateCeilingOffer() public { // First offer vm.prank(address(seller)); - offers.createCollectionOffer{value: 1 ether}(address(token)); + offers.createOffer{value: 1 ether}(address(token)); // Ceiling offer vm.prank(address(seller2)); - offers.createCollectionOffer{value: 2 ether}(address(token)); + offers.createOffer{value: 2 ether}(address(token)); - (address offeror1, uint256 amount1, uint256 id1, uint256 _prevId1, uint256 _nextId1) = offers.offers(address(token), 1); - (address offeror2, uint256 amount2, uint256 id2, uint256 _prevId2, uint256 _nextId2) = offers.offers(address(token), 2); + (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); @@ -170,9 +170,9 @@ contract CollectionOffersV1Test is DSTest { // Ensure floor nextId is ceiling id and ceiling prevId is floor id require(_nextId1 == 2 && _prevId2 == 1); - uint256 floorId = offers.floorOfferId(address(token)); + uint32 floorId = offers.floorOfferId(address(token)); uint256 floorAmt = offers.floorOfferAmount(address(token)); - uint256 ceilingId = offers.ceilingOfferId(address(token)); + uint32 ceilingId = offers.ceilingOfferId(address(token)); uint256 ceilingAmt = offers.ceilingOfferAmount(address(token)); require(floorId == id1); @@ -184,17 +184,17 @@ contract CollectionOffersV1Test is DSTest { function test_CreateFloorOffer() public { // First offer vm.prank(address(seller)); - offers.createCollectionOffer{value: 1 ether}(address(token)); + offers.createOffer{value: 1 ether}(address(token)); // Ceiling offer vm.prank(address(seller2)); - offers.createCollectionOffer{value: 2 ether}(address(token)); + offers.createOffer{value: 2 ether}(address(token)); // Floor offer vm.prank(address(seller3)); - offers.createCollectionOffer{value: 0.5 ether}(address(token)); + offers.createOffer{value: 0.5 ether}(address(token)); - (address offeror1, uint256 amount1, uint256 id1, uint256 _prevId1, uint256 _nextId1) = offers.offers(address(token), 1); - (address offeror2, uint256 amount2, uint256 id2, uint256 _prevId2, uint256 _nextId2) = offers.offers(address(token), 2); - (address offeror3, uint256 amount3, uint256 id3, uint256 _prevId3, uint256 _nextId3) = offers.offers(address(token), 3); + (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)); @@ -205,9 +205,9 @@ contract CollectionOffersV1Test is DSTest { require(_nextId3 == id1 && _prevId1 == id3); require(_nextId1 == id2 && _prevId2 == id1); - uint256 floorId = offers.floorOfferId(address(token)); + uint32 floorId = offers.floorOfferId(address(token)); uint256 floorAmt = offers.floorOfferAmount(address(token)); - uint256 ceilingId = offers.ceilingOfferId(address(token)); + uint32 ceilingId = offers.ceilingOfferId(address(token)); uint256 ceilingAmt = offers.ceilingOfferAmount(address(token)); require(floorId == id3); @@ -217,40 +217,99 @@ contract CollectionOffersV1Test is DSTest { } function test_CreateMiddleOffer() public { + // Order: id3 --> **id4** --> id1 --> id2 loadOffers(); - // Floor to ceiling order: id3 --> id4 --> id1 --> id2 - (address offeror4, uint256 amount4, , uint256 _prevId4, uint256 _nextId4) = offers.offers(address(token), 4); - uint256 floorId = offers.floorOfferId(address(token)); - uint256 ceilingId = offers.ceilingOfferId(address(token)); + (address offeror4, , uint32 _prevId4, uint32 _nextId4, uint256 amount4) = offers.offers(address(token), 4); - // Ensure seller and amount are valid - require(offeror4 == address(seller4) && amount4 == 1 ether); + uint32 floorId = offers.floorOfferId(address(token)); + uint32 ceilingId = offers.ceilingOfferId(address(token)); - // Ensure placement between offer 3 and 1 + 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); } - function testRevert_MustAttachFundsToCreateOffer() public { - vm.expectRevert("createCollectionOffer msg value must be greater than 0"); - offers.createCollectionOffer(address(token)); + /// ------------ 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); + uint32 ceilingId = offers.ceilingOfferId(address(token)); + + require(ceilingId == 2); + require(_prevId2 == 1 && _nextId2 == 0); + require(amount2 == 5 ether); } - /// ------------ SET COLLECTION OFFER AMOUNT ------------ /// + 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.setCollectionOfferAmount{value: 4.5 ether}(address(token), 3, 5 ether); + offers.setOfferAmount{value: 4.5 ether}(address(token), 3, 5 ether); - // Updated order: id4 --> id1 --> id2 --> id3 - (, uint256 amount3, , uint256 _prevId3, uint256 _nextId3) = offers.offers(address(token), 3); - uint256 floorId = offers.floorOfferId(address(token)); - uint256 ceilingId = offers.ceilingOfferId(address(token)); + // Updated Order: id4 --> id1 --> id2 --> id3 + (, , uint32 _prevId3, uint256 _nextId3, uint256 amount3) = offers.offers(address(token), 3); + uint32 floorId = offers.floorOfferId(address(token)); + uint32 ceilingId = offers.ceilingOfferId(address(token)); // Ensure book is updated with floor as new ceiling require(ceilingId == 3 && floorId == 4); @@ -259,16 +318,18 @@ contract CollectionOffersV1Test is DSTest { } function test_IncreaseFloorToMiddle() public { + // Initial Order: id3 --> id4 --> id1 --> id2 loadOffers(); // Increase floor offer to equal ceiling vm.prank(address(seller3)); - offers.setCollectionOfferAmount{value: 1.5 ether}(address(token), 3, 2 ether); + 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); - // Updated order: id4 --> id1 --> id3 --> id2 - (, uint256 amount3, , uint256 _prevId3, uint256 _nextId3) = offers.offers(address(token), 3); - uint256 floorId = offers.floorOfferId(address(token)); - uint256 ceilingId = offers.ceilingOfferId(address(token)); + uint32 floorId = offers.floorOfferId(address(token)); + uint32 ceilingId = offers.ceilingOfferId(address(token)); // Ensure book is updated wrt time priority require(ceilingId == 2 && floorId == 4); @@ -276,113 +337,215 @@ contract CollectionOffersV1Test is DSTest { require(amount3 == 2 ether); } - function test_IncreaseCeilingInPlace() public { + function test_IncreaseFloorInPlace() public { + // Initial Order: id3 --> id4 --> id1 --> id2 loadOffers(); - vm.prank(address(seller2)); - offers.setCollectionOfferAmount{value: 3 ether}(address(token), 2, 5 ether); + vm.prank(address(seller3)); + offers.setOfferAmount{value: 0.1 ether}(address(token), 3, 0.6 ether); - (, uint256 amount2, , uint256 _prevId2, uint256 _nextId2) = offers.offers(address(token), 2); - uint256 ceilingId = offers.ceilingOfferId(address(token)); + (, , uint32 _prevId3, uint32 _nextId3, uint256 amount3) = offers.offers(address(token), 3); - require(ceilingId == 2); - require(_prevId2 == 1 && _nextId2 == 0); - require(amount2 == 5 ether); + require(offers.floorOfferId(address(token)) == 3); + + require(_prevId3 == 0); + require(_nextId3 == 4); + require(amount3 == 0.6 ether); } - function testRevert_UpdateOfferMustBeSeller() public { + function test_DecreaseFloor() public { + // Initial Order: id3 --> id4 --> id1 --> id2 loadOffers(); - vm.prank(address(seller2)); - vm.expectRevert("setCollectionOfferAmount offer must be active & msg sender must be seller"); - offers.setCollectionOfferAmount(address(token), 1, 0.5 ether); + 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); } - /// ------------ SET COLLECTION OFFER FINDERS FEE ------------ /// + function test_IncreaseMiddleToCeiling() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); - function test_UpdateFindersFee() public { - vm.startPrank(address(seller)); - offers.createCollectionOffer{value: 1 ether}(address(token)); + vm.prank(address(seller4)); + offers.setOfferAmount{value: 5 ether}(address(token), 4, 5 ether); - vm.warp(1 hours); + // Updated Order: id3 --> id1 --> id2 --> id4 + (, , uint32 _prevId4, uint32 _nextId4, uint256 amount4) = offers.offers(address(token), 4); - offers.setCollectionOfferFindersFee(address(token), 1, 1000); + require(offers.ceilingOfferId(address(token)) == 4); - vm.stopPrank(); + 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(offers.findersFeeOverrides(address(token), 1) == 1000); + require(_prevId4 == 1); + require(_nextId4 == 2); + require(amount4 == 1.5 ether); } - function testRevert_UpdateFindersFeeMustBeSeller() public { + function test_IncreaseMiddleInPlace() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); + vm.prank(address(seller)); - offers.createCollectionOffer{value: 1 ether}(address(token)); + offers.setOfferAmount{value: 0.5 ether}(address(token), 1, 1.5 ether); - vm.warp(1 hours); + (, , uint32 _prevId1, uint32 _nextId1, uint256 amount1) = offers.offers(address(token), 1); - vm.expectRevert("setCollectionOfferFindersFee msg sender must be seller"); - offers.setCollectionOfferFindersFee(address(token), 1, 1000); + require(_prevId1 == 4); + require(_nextId1 == 2); + require(amount1 == 1.5 ether); } - function testRevert_UpdateFindersFeeMustBeValidBps() public { - vm.startPrank(address(seller)); - offers.createCollectionOffer{value: 1 ether}(address(token)); + function test_DecreaseMiddleToFloor() public { + // Initial Order: id3 --> id4 --> id1 --> id2 + loadOffers(); - vm.warp(1 hours); + vm.prank(address(seller)); + offers.setOfferAmount(address(token), 1, 0.25 ether); - vm.expectRevert("setCollectionOfferFindersFee must be less than or equal to 10000 bps"); - offers.setCollectionOfferFindersFee(address(token), 1, 10001); + (, , uint32 _prevId1, uint32 _nextId1, uint256 amount1) = offers.offers(address(token), 1); - vm.stopPrank(); + 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.createCollectionOffer{value: 1 ether}(address(token)); + offers.createOffer{value: 1 ether}(address(token)); vm.warp(1 hours); uint256 beforeSellerBalance = address(seller).balance; vm.prank(address(seller)); - offers.cancelCollectionOffer(address(token), 1); + offers.cancelOffer(address(token), 1); uint256 afterSellerBalance = address(seller).balance; require(afterSellerBalance - beforeSellerBalance == 1 ether); } - function testRevert_CancelOfferMustBeSeller() public { + function testRevert_CancelOfferMustBeMaker() public { vm.prank(address(seller)); - offers.createCollectionOffer{value: 1 ether}(address(token)); + offers.createOffer{value: 1 ether}(address(token)); - vm.expectRevert("cancelCollectionOffer offer must be active & msg sender must be seller"); - offers.cancelCollectionOffer(address(token), 1); + 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.fillCollectionOffer(address(token), 0, 2 ether, address(finder)); + 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("fillCollectionOffer offer satisfying specified _minAmount not found"); - offers.fillCollectionOffer(address(token), 0, 5 ether, address(finder)); + vm.expectRevert("fillOffer offer satisfying _minAmount not found"); + offers.fillOffer(address(token), 0, 5 ether, address(finder)); } - function testRevert_MustOwnCollectionToken() public { - loadOffers(); + /// ------------ 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); + } - vm.expectRevert("fillCollectionOffer msg sender must own specified token"); - offers.fillCollectionOffer(address(token), 0, 2 ether, address(finder)); + 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 From e3cb5a398f686ac5fff5071d8fa802fc11a8cdaa Mon Sep 17 00:00:00 2001 From: joshieDo Date: Thu, 10 Feb 2022 01:38:03 +0400 Subject: [PATCH 3/9] snapshot --- .gas-snapshot | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 .gas-snapshot diff --git a/.gas-snapshot b/.gas-snapshot new file mode 100644 index 00000000..ce87ce31 --- /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: 117447) +CollectionOffersV1IntegrationTest:test_RefundOfferDecreaseToSeller() (gas: 182674) +CollectionOffersV1IntegrationTest:test_WithdrawOfferFromSeller() (gas: 167994) +CollectionOffersV1IntegrationTest:test_WithdrawOfferIncreaseFromSeller() (gas: 180506) +CollectionOffersV1Test:testGas_CreateFirstCollectionOffer() (gas: 167000) +CollectionOffersV1Test:testRevert_CancelOfferMustBeMaker() (gas: 169969) +CollectionOffersV1Test:testRevert_FillMinimumTooHigh() (gas: 350556) +CollectionOffersV1Test:testRevert_FindersFeeCannotExceed10000() (gas: 2557) +CollectionOffersV1Test:testRevert_MustOwnCollectionToken() (gas: 349715) +CollectionOffersV1Test:testRevert_UpdateFindersFeeMustBeRegistrar() (gas: 1903) +CollectionOffersV1Test:testRevert_UpdateOfferMustBeMaker() (gas: 349396) +CollectionOffersV1Test:test_CancelCollectionOffer() (gas: 64376) +CollectionOffersV1Test:test_CreateCeilingOffer() (gas: 235905) +CollectionOffersV1Test:test_CreateCollectionOffer() (gas: 174552) +CollectionOffersV1Test:test_CreateFloorOffer() (gas: 297577) +CollectionOffersV1Test:test_CreateMiddleOffer() (gas: 351318) +CollectionOffersV1Test:test_DecreaseCeilingInPlace() (gas: 364308) +CollectionOffersV1Test:test_DecreaseCeilingToFloor() (gas: 369080) +CollectionOffersV1Test:test_DecreaseCeilingToMiddle() (gas: 372149) +CollectionOffersV1Test:test_DecreaseFloor() (gas: 367525) +CollectionOffersV1Test:test_DecreaseMiddleInPlace() (gas: 363922) +CollectionOffersV1Test:test_DecreaseMiddleToFloor() (gas: 368851) +CollectionOffersV1Test:test_DecreaseMiddleToMiddle() (gas: 368913) +CollectionOffersV1Test:test_FillCollectionOffer() (gas: 375660) +CollectionOffersV1Test:test_IncreaseCeiling() (gas: 367522) +CollectionOffersV1Test:test_IncreaseFloorInPlace() (gas: 363324) +CollectionOffersV1Test:test_IncreaseFloorToMiddle() (gas: 370112) +CollectionOffersV1Test:test_IncreaseFloorToNewCeiling() (gas: 366705) +CollectionOffersV1Test:test_IncreaseMiddleInPlace() (gas: 361682) +CollectionOffersV1Test:test_IncreaseMiddleToCeiling() (gas: 365253) +CollectionOffersV1Test:test_IncreaseMiddleToMiddle() (gas: 367968) +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) From b5bd260562611646486fb5d1a7f5a8e0e7a969d3 Mon Sep 17 00:00:00 2001 From: joshieDo Date: Thu, 10 Feb 2022 01:40:22 +0400 Subject: [PATCH 4/9] no need for uint32 on createOffers --- .gas-snapshot | 58 +++++++++---------- .../V1/CollectionOffersV1.sol | 4 +- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index ce87ce31..e5236e80 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -19,37 +19,37 @@ 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: 117447) -CollectionOffersV1IntegrationTest:test_RefundOfferDecreaseToSeller() (gas: 182674) -CollectionOffersV1IntegrationTest:test_WithdrawOfferFromSeller() (gas: 167994) -CollectionOffersV1IntegrationTest:test_WithdrawOfferIncreaseFromSeller() (gas: 180506) -CollectionOffersV1Test:testGas_CreateFirstCollectionOffer() (gas: 167000) -CollectionOffersV1Test:testRevert_CancelOfferMustBeMaker() (gas: 169969) -CollectionOffersV1Test:testRevert_FillMinimumTooHigh() (gas: 350556) +CollectionOffersV1IntegrationTest:test_ETHIntegration() (gas: 117410) +CollectionOffersV1IntegrationTest:test_RefundOfferDecreaseToSeller() (gas: 182637) +CollectionOffersV1IntegrationTest:test_WithdrawOfferFromSeller() (gas: 167957) +CollectionOffersV1IntegrationTest:test_WithdrawOfferIncreaseFromSeller() (gas: 180469) +CollectionOffersV1Test:testGas_CreateFirstCollectionOffer() (gas: 166928) +CollectionOffersV1Test:testRevert_CancelOfferMustBeMaker() (gas: 169897) +CollectionOffersV1Test:testRevert_FillMinimumTooHigh() (gas: 350268) CollectionOffersV1Test:testRevert_FindersFeeCannotExceed10000() (gas: 2557) -CollectionOffersV1Test:testRevert_MustOwnCollectionToken() (gas: 349715) +CollectionOffersV1Test:testRevert_MustOwnCollectionToken() (gas: 349427) CollectionOffersV1Test:testRevert_UpdateFindersFeeMustBeRegistrar() (gas: 1903) -CollectionOffersV1Test:testRevert_UpdateOfferMustBeMaker() (gas: 349396) -CollectionOffersV1Test:test_CancelCollectionOffer() (gas: 64376) -CollectionOffersV1Test:test_CreateCeilingOffer() (gas: 235905) -CollectionOffersV1Test:test_CreateCollectionOffer() (gas: 174552) -CollectionOffersV1Test:test_CreateFloorOffer() (gas: 297577) -CollectionOffersV1Test:test_CreateMiddleOffer() (gas: 351318) -CollectionOffersV1Test:test_DecreaseCeilingInPlace() (gas: 364308) -CollectionOffersV1Test:test_DecreaseCeilingToFloor() (gas: 369080) -CollectionOffersV1Test:test_DecreaseCeilingToMiddle() (gas: 372149) -CollectionOffersV1Test:test_DecreaseFloor() (gas: 367525) -CollectionOffersV1Test:test_DecreaseMiddleInPlace() (gas: 363922) -CollectionOffersV1Test:test_DecreaseMiddleToFloor() (gas: 368851) -CollectionOffersV1Test:test_DecreaseMiddleToMiddle() (gas: 368913) -CollectionOffersV1Test:test_FillCollectionOffer() (gas: 375660) -CollectionOffersV1Test:test_IncreaseCeiling() (gas: 367522) -CollectionOffersV1Test:test_IncreaseFloorInPlace() (gas: 363324) -CollectionOffersV1Test:test_IncreaseFloorToMiddle() (gas: 370112) -CollectionOffersV1Test:test_IncreaseFloorToNewCeiling() (gas: 366705) -CollectionOffersV1Test:test_IncreaseMiddleInPlace() (gas: 361682) -CollectionOffersV1Test:test_IncreaseMiddleToCeiling() (gas: 365253) -CollectionOffersV1Test:test_IncreaseMiddleToMiddle() (gas: 367968) +CollectionOffersV1Test:testRevert_UpdateOfferMustBeMaker() (gas: 349108) +CollectionOffersV1Test:test_CancelCollectionOffer() (gas: 64304) +CollectionOffersV1Test:test_CreateCeilingOffer() (gas: 235761) +CollectionOffersV1Test:test_CreateCollectionOffer() (gas: 174480) +CollectionOffersV1Test:test_CreateFloorOffer() (gas: 297361) +CollectionOffersV1Test:test_CreateMiddleOffer() (gas: 351030) +CollectionOffersV1Test:test_DecreaseCeilingInPlace() (gas: 364020) +CollectionOffersV1Test:test_DecreaseCeilingToFloor() (gas: 368792) +CollectionOffersV1Test:test_DecreaseCeilingToMiddle() (gas: 371861) +CollectionOffersV1Test:test_DecreaseFloor() (gas: 367237) +CollectionOffersV1Test:test_DecreaseMiddleInPlace() (gas: 363634) +CollectionOffersV1Test:test_DecreaseMiddleToFloor() (gas: 368563) +CollectionOffersV1Test:test_DecreaseMiddleToMiddle() (gas: 368625) +CollectionOffersV1Test:test_FillCollectionOffer() (gas: 375372) +CollectionOffersV1Test:test_IncreaseCeiling() (gas: 367234) +CollectionOffersV1Test:test_IncreaseFloorInPlace() (gas: 363036) +CollectionOffersV1Test:test_IncreaseFloorToMiddle() (gas: 369824) +CollectionOffersV1Test:test_IncreaseFloorToNewCeiling() (gas: 366417) +CollectionOffersV1Test:test_IncreaseMiddleInPlace() (gas: 361394) +CollectionOffersV1Test:test_IncreaseMiddleToCeiling() (gas: 364965) +CollectionOffersV1Test:test_IncreaseMiddleToMiddle() (gas: 367680) CollectionOffersV1Test:test_UpdateFindersFee() (gas: 5132) ERC1155TransferHelperTest:testFail_UserMustApproveTransferHelperToTransferBatch() (gas: 43369) ERC1155TransferHelperTest:testFail_UserMustApproveTransferHelperToTransferSingle() (gas: 37478) diff --git a/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol b/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol index f37921b5..d5294e66 100644 --- a/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol +++ b/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol @@ -128,12 +128,12 @@ contract CollectionOffersV1 is /// @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 (uint32) { + function createOffer(address _tokenContract) external payable nonReentrant returns (uint256) { // Ensure offer is valid and take custody _handleIncomingTransfer(msg.value, ETH); // Add to collection's offer book - uint32 offerId = _addOffer(_tokenContract, msg.value, msg.sender); + uint256 offerId = _addOffer(_tokenContract, msg.value, msg.sender); emit CollectionOfferCreated(_tokenContract, offerId, msg.sender, msg.value); From ddb8b45f0ff26d5892298217e1af97d7bef603a2 Mon Sep 17 00:00:00 2001 From: joshieDo Date: Thu, 10 Feb 2022 01:52:08 +0400 Subject: [PATCH 5/9] use uint32 only for storage --- .gas-snapshot | 58 +++++----- .../V1/CollectionOfferBookV1.sol | 102 +++++++++--------- .../V1/CollectionOffersV1.sol | 2 +- .../V1/CollectionOffers.t.sol | 26 ++--- 4 files changed, 94 insertions(+), 94 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index e5236e80..b917a41a 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -19,37 +19,37 @@ 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: 117410) -CollectionOffersV1IntegrationTest:test_RefundOfferDecreaseToSeller() (gas: 182637) -CollectionOffersV1IntegrationTest:test_WithdrawOfferFromSeller() (gas: 167957) -CollectionOffersV1IntegrationTest:test_WithdrawOfferIncreaseFromSeller() (gas: 180469) -CollectionOffersV1Test:testGas_CreateFirstCollectionOffer() (gas: 166928) -CollectionOffersV1Test:testRevert_CancelOfferMustBeMaker() (gas: 169897) -CollectionOffersV1Test:testRevert_FillMinimumTooHigh() (gas: 350268) +CollectionOffersV1IntegrationTest:test_ETHIntegration() (gas: 116854) +CollectionOffersV1IntegrationTest:test_RefundOfferDecreaseToSeller() (gas: 182344) +CollectionOffersV1IntegrationTest:test_WithdrawOfferFromSeller() (gas: 167691) +CollectionOffersV1IntegrationTest:test_WithdrawOfferIncreaseFromSeller() (gas: 180176) +CollectionOffersV1Test:testGas_CreateFirstCollectionOffer() (gas: 166662) +CollectionOffersV1Test:testRevert_CancelOfferMustBeMaker() (gas: 169631) +CollectionOffersV1Test:testRevert_FillMinimumTooHigh() (gas: 349546) CollectionOffersV1Test:testRevert_FindersFeeCannotExceed10000() (gas: 2557) -CollectionOffersV1Test:testRevert_MustOwnCollectionToken() (gas: 349427) +CollectionOffersV1Test:testRevert_MustOwnCollectionToken() (gas: 348711) CollectionOffersV1Test:testRevert_UpdateFindersFeeMustBeRegistrar() (gas: 1903) -CollectionOffersV1Test:testRevert_UpdateOfferMustBeMaker() (gas: 349108) -CollectionOffersV1Test:test_CancelCollectionOffer() (gas: 64304) -CollectionOffersV1Test:test_CreateCeilingOffer() (gas: 235761) -CollectionOffersV1Test:test_CreateCollectionOffer() (gas: 174480) -CollectionOffersV1Test:test_CreateFloorOffer() (gas: 297361) -CollectionOffersV1Test:test_CreateMiddleOffer() (gas: 351030) -CollectionOffersV1Test:test_DecreaseCeilingInPlace() (gas: 364020) -CollectionOffersV1Test:test_DecreaseCeilingToFloor() (gas: 368792) -CollectionOffersV1Test:test_DecreaseCeilingToMiddle() (gas: 371861) -CollectionOffersV1Test:test_DecreaseFloor() (gas: 367237) -CollectionOffersV1Test:test_DecreaseMiddleInPlace() (gas: 363634) -CollectionOffersV1Test:test_DecreaseMiddleToFloor() (gas: 368563) -CollectionOffersV1Test:test_DecreaseMiddleToMiddle() (gas: 368625) -CollectionOffersV1Test:test_FillCollectionOffer() (gas: 375372) -CollectionOffersV1Test:test_IncreaseCeiling() (gas: 367234) -CollectionOffersV1Test:test_IncreaseFloorInPlace() (gas: 363036) -CollectionOffersV1Test:test_IncreaseFloorToMiddle() (gas: 369824) -CollectionOffersV1Test:test_IncreaseFloorToNewCeiling() (gas: 366417) -CollectionOffersV1Test:test_IncreaseMiddleInPlace() (gas: 361394) -CollectionOffersV1Test:test_IncreaseMiddleToCeiling() (gas: 364965) -CollectionOffersV1Test:test_IncreaseMiddleToMiddle() (gas: 367680) +CollectionOffersV1Test:testRevert_UpdateOfferMustBeMaker() (gas: 348392) +CollectionOffersV1Test:test_CancelCollectionOffer() (gas: 63781) +CollectionOffersV1Test:test_CreateCeilingOffer() (gas: 235108) +CollectionOffersV1Test:test_CreateCollectionOffer() (gas: 174034) +CollectionOffersV1Test:test_CreateFloorOffer() (gas: 296501) +CollectionOffersV1Test:test_CreateMiddleOffer() (gas: 350134) +CollectionOffersV1Test:test_DecreaseCeilingInPlace() (gas: 363178) +CollectionOffersV1Test:test_DecreaseCeilingToFloor() (gas: 367693) +CollectionOffersV1Test:test_DecreaseCeilingToMiddle() (gas: 370844) +CollectionOffersV1Test:test_DecreaseFloor() (gas: 366362) +CollectionOffersV1Test:test_DecreaseMiddleInPlace() (gas: 362867) +CollectionOffersV1Test:test_DecreaseMiddleToFloor() (gas: 367591) +CollectionOffersV1Test:test_DecreaseMiddleToMiddle() (gas: 367825) +CollectionOffersV1Test:test_FillCollectionOffer() (gas: 374367) +CollectionOffersV1Test:test_IncreaseCeiling() (gas: 366135) +CollectionOffersV1Test:test_IncreaseFloorInPlace() (gas: 362161) +CollectionOffersV1Test:test_IncreaseFloorToMiddle() (gas: 368714) +CollectionOffersV1Test:test_IncreaseFloorToNewCeiling() (gas: 365225) +CollectionOffersV1Test:test_IncreaseMiddleInPlace() (gas: 360627) +CollectionOffersV1Test:test_IncreaseMiddleToCeiling() (gas: 363993) +CollectionOffersV1Test:test_IncreaseMiddleToMiddle() (gas: 366790) CollectionOffersV1Test:test_UpdateFindersFee() (gas: 5132) ERC1155TransferHelperTest:testFail_UserMustApproveTransferHelperToTransferBatch() (gas: 43369) ERC1155TransferHelperTest:testFail_UserMustApproveTransferHelperToTransferSingle() (gas: 37478) diff --git a/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol b/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol index 91a4ae76..0fff2717 100644 --- a/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol +++ b/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol @@ -32,7 +32,7 @@ contract CollectionOfferBookV1 { /// @notice The floor offer ID for a given collection /// @dev ERC-721 token address => Floor offer ID - mapping(address => uint32) public floorOfferId; + mapping(address => uint256) public floorOfferId; /// @notice The floor offer amount for a given collection /// @dev ERC-721 token address => Floor offer amount @@ -40,7 +40,7 @@ contract CollectionOfferBookV1 { /// @notice The ceiling offer ID for a given collection /// @dev ERC-721 token address => Ceiling offer ID - mapping(address => uint32) public ceilingOfferId; + mapping(address => uint256) public ceilingOfferId; /// @notice The ceiling offer amount for a given collection /// @dev ERC-721 token address => Ceiling offer amount @@ -56,7 +56,7 @@ contract CollectionOfferBookV1 { address _collection, uint256 _amount, address _maker - ) internal returns (uint32) { + ) internal returns (uint256) { unchecked { ++offerCount; } @@ -73,20 +73,20 @@ contract CollectionOfferBookV1 { // Else if offer is greater than current ceiling, mark as new ceiling } else if (_isNewCeiling(_collection, _amount)) { - uint32 prevCeilingId = ceilingOfferId[_collection]; + uint256 prevCeilingId = ceilingOfferId[_collection]; - offers[_collection][prevCeilingId].nextId = offerCount; - offers[_collection][offerCount] = Offer({maker: _maker, amount: _amount, id: offerCount, prevId: prevCeilingId, nextId: 0}); + offers[_collection][prevCeilingId].nextId = uint32(offerCount); + offers[_collection][offerCount] = Offer({maker: _maker, amount: _amount, id: 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)) { - uint32 prevFloorId = floorOfferId[_collection]; + uint256 prevFloorId = floorOfferId[_collection]; - offers[_collection][prevFloorId].prevId = offerCount; - offers[_collection][offerCount] = Offer({maker: _maker, amount: _amount, id: offerCount, prevId: 0, nextId: prevFloorId}); + offers[_collection][prevFloorId].prevId = uint32(offerCount); + offers[_collection][offerCount] = Offer({maker: _maker, amount: _amount, id: offerCount, prevId: 0, nextId: uint32(prevFloorId)}); floorOfferId[_collection] = offerCount; floorOfferAmount[_collection] = _amount; @@ -105,8 +105,8 @@ contract CollectionOfferBookV1 { offers[_collection][offerCount] = Offer({maker: _maker, amount: _amount, id: offerCount, prevId: offer.prevId, nextId: offer.id}); // Update neighboring pointers - offers[_collection][offer.id].prevId = offerCount; - offers[_collection][offer.prevId].nextId = offerCount; + offers[_collection][offer.id].prevId = uint32(offerCount); + offers[_collection][offer.prevId].nextId = uint32(offerCount); } return offerCount; @@ -121,7 +121,7 @@ contract CollectionOfferBookV1 { function _updateOffer( Offer storage _offer, address _collection, - uint32 _offerId, + uint256 _offerId, uint256 _newAmount, bool _increase ) internal { @@ -154,18 +154,18 @@ contract CollectionOfferBookV1 { // Else if offer is new ceiling -- } else if (_isNewCeiling(_collection, _newAmount)) { // Get previous neighbors - uint32 prevId = _offer.prevId; - uint32 nextId = _offer.nextId; + uint256 prevId = _offer.prevId; + uint256 nextId = _offer.nextId; // Update previous neighbors _connectNeighbors(_collection, _offerId, prevId, nextId); // Update previous ceiling - uint32 prevCeilingId = ceilingOfferId[_collection]; - offers[_collection][prevCeilingId].nextId = _offerId; + uint256 prevCeilingId = ceilingOfferId[_collection]; + offers[_collection][prevCeilingId].nextId = uint32(_offerId); // Update offer to be new ceiling - _offer.prevId = prevCeilingId; + _offer.prevId = uint32(prevCeilingId); _offer.nextId = 0; _offer.amount = _newAmount; @@ -176,18 +176,18 @@ contract CollectionOfferBookV1 { // Else if offer is new floor -- } else if (_isNewFloor(_collection, _newAmount)) { // Get previous neighbors - uint32 prevId = _offer.prevId; - uint32 nextId = _offer.nextId; + uint256 prevId = _offer.prevId; + uint256 nextId = _offer.nextId; // Update previous neighbors _connectNeighbors(_collection, _offerId, prevId, nextId); // Update previous floor - uint32 prevFloorId = floorOfferId[_collection]; - offers[_collection][prevFloorId].prevId = _offerId; + uint256 prevFloorId = floorOfferId[_collection]; + offers[_collection][prevFloorId].prevId = uint32(_offerId); // Update offer to be new floor - _offer.nextId = prevFloorId; + _offer.nextId = uint32(prevFloorId); _offer.prevId = 0; _offer.amount = _newAmount; @@ -215,7 +215,7 @@ contract CollectionOfferBookV1 { /// @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, uint32 _offerId) internal { + 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]; @@ -226,7 +226,7 @@ contract CollectionOfferBookV1 { // Else if the offer is the current floor, update the collection's floor before removing } else if (_isFloorOffer(_collection, _offerId)) { - uint32 newFloorId = offers[_collection][_offerId].nextId; + uint256 newFloorId = offers[_collection][_offerId].nextId; uint256 newFloorAmount = offers[_collection][newFloorId].amount; offers[_collection][newFloorId].prevId = 0; @@ -238,7 +238,7 @@ contract CollectionOfferBookV1 { // Else if the offer is the current ceiling, update the collection's ceiling before removing } else if (_isCeilingOffer(_collection, _offerId)) { - uint32 newCeilingId = offers[_collection][_offerId].prevId; + uint256 newCeilingId = offers[_collection][_offerId].prevId; uint256 newCeilingAmount = offers[_collection][newCeilingId].amount; offers[_collection][newCeilingId].nextId = 0; @@ -252,8 +252,8 @@ contract CollectionOfferBookV1 { } else { Offer memory offer = offers[_collection][_offerId]; - offers[_collection][offer.nextId].prevId = offer.prevId; - offers[_collection][offer.prevId].nextId = offer.nextId; + offers[_collection][offer.nextId].prevId = uint32(offer.prevId); + offers[_collection][offer.prevId].nextId = uint32(offer.nextId); delete offers[_collection][_offerId]; } @@ -262,7 +262,7 @@ contract CollectionOfferBookV1 { /// @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 (uint32) { + 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]; @@ -283,21 +283,21 @@ contract CollectionOfferBookV1 { /// @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, uint32 _offerId) private view returns (bool) { + 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, uint32 _offerId) private view returns (bool) { + 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, uint32 _offerId) private view returns (bool) { + function _isFloorOffer(address _collection, uint256 _offerId) private view returns (bool) { return (_offerId == floorOfferId[_collection]); } @@ -322,12 +322,12 @@ contract CollectionOfferBookV1 { /// @param _increase Whether the update is an amount increase or decrease function _isUpdateInPlace( address _collection, - uint32 _offerId, + uint256 _offerId, uint256 _newAmount, bool _increase ) private view returns (bool) { - uint32 nextOffer = offers[_collection][_offerId].nextId; - uint32 prevOffer = offers[_collection][_offerId].prevId; + 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)); @@ -340,9 +340,9 @@ contract CollectionOfferBookV1 { /// @param _nextId The ID of the offer's next pointer function _connectNeighbors( address _collection, - uint32 _offerId, - uint32 _prevId, - uint32 _nextId + uint256 _offerId, + uint256 _prevId, + uint256 _nextId ) private { // If offer is floor -- if (_offerId == floorOfferId[_collection]) { @@ -363,8 +363,8 @@ contract CollectionOfferBookV1 { // Else offer is in middle -- } else { // Update neighbor pointers - offers[_collection][_nextId].prevId = _prevId; - offers[_collection][_prevId].nextId = _nextId; + offers[_collection][_nextId].prevId = uint32(_prevId); + offers[_collection][_prevId].nextId = uint32(_nextId); } } @@ -375,8 +375,8 @@ contract CollectionOfferBookV1 { /// @param _newAmount The new offer amount function _insertIncreasedOffer( address _collection, - uint32 _offerId, - uint32 _nextId, + uint256 _offerId, + uint256 _nextId, uint256 _newAmount ) private { Offer memory offer = offers[_collection][_nextId]; @@ -387,12 +387,12 @@ contract CollectionOfferBookV1 { } // Update offer pointers - offers[_collection][_offerId].nextId = offer.id; - offers[_collection][_offerId].prevId = offer.prevId; + offers[_collection][_offerId].nextId = uint32(offer.id); + offers[_collection][_offerId].prevId = uint32(offer.prevId); // Update neighbor pointers - offers[_collection][offer.id].prevId = _offerId; - offers[_collection][offer.prevId].nextId = _offerId; + offers[_collection][offer.id].prevId = uint32(_offerId); + offers[_collection][offer.prevId].nextId = uint32(_offerId); // Update offer amount offers[_collection][_offerId].amount = _newAmount; @@ -405,8 +405,8 @@ contract CollectionOfferBookV1 { /// @param _newAmount The new offer amount function _insertDecreasedOffer( address _collection, - uint32 _offerId, - uint32 _prevId, + uint256 _offerId, + uint256 _prevId, uint256 _newAmount ) private { Offer memory offer = offers[_collection][_prevId]; @@ -417,12 +417,12 @@ contract CollectionOfferBookV1 { } // Update offer pointers - offers[_collection][_offerId].prevId = offer.id; - offers[_collection][_offerId].nextId = offer.nextId; + offers[_collection][_offerId].prevId = uint32(offer.id); + offers[_collection][_offerId].nextId = uint32(offer.nextId); // Update neighbor pointers - offers[_collection][offer.id].nextId = _offerId; - offers[_collection][offer.nextId].prevId = _offerId; + 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 index d5294e66..c4438a18 100644 --- a/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol +++ b/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol @@ -327,7 +327,7 @@ contract CollectionOffersV1 is require(msg.sender == IERC721(_tokenContract).ownerOf(_tokenId), "fillOffer must own specified token"); // Get matching offer (if exists) - uint32 offerId = _getMatchingOffer(_tokenContract, _minAmount); + uint256 offerId = _getMatchingOffer(_tokenContract, _minAmount); require(offerId != 0, "fillOffer offer satisfying _minAmount not found"); Offer memory offer = offers[_tokenContract][offerId]; diff --git a/contracts/test/modules/CollectionOffers/V1/CollectionOffers.t.sol b/contracts/test/modules/CollectionOffers/V1/CollectionOffers.t.sol index 3ad79555..d4565ea4 100644 --- a/contracts/test/modules/CollectionOffers/V1/CollectionOffers.t.sol +++ b/contracts/test/modules/CollectionOffers/V1/CollectionOffers.t.sol @@ -141,9 +141,9 @@ contract CollectionOffersV1Test is DSTest { require(_prevId == 0); require(_nextId == 0); - uint32 floorId = offers.floorOfferId(address(token)); + uint256 floorId = offers.floorOfferId(address(token)); uint256 floorAmt = offers.floorOfferAmount(address(token)); - uint32 ceilingId = offers.ceilingOfferId(address(token)); + uint256 ceilingId = offers.ceilingOfferId(address(token)); uint256 ceilingAmt = offers.ceilingOfferAmount(address(token)); require(floorId == 1); @@ -170,9 +170,9 @@ contract CollectionOffersV1Test is DSTest { // Ensure floor nextId is ceiling id and ceiling prevId is floor id require(_nextId1 == 2 && _prevId2 == 1); - uint32 floorId = offers.floorOfferId(address(token)); + uint256 floorId = offers.floorOfferId(address(token)); uint256 floorAmt = offers.floorOfferAmount(address(token)); - uint32 ceilingId = offers.ceilingOfferId(address(token)); + uint256 ceilingId = offers.ceilingOfferId(address(token)); uint256 ceilingAmt = offers.ceilingOfferAmount(address(token)); require(floorId == id1); @@ -205,9 +205,9 @@ contract CollectionOffersV1Test is DSTest { require(_nextId3 == id1 && _prevId1 == id3); require(_nextId1 == id2 && _prevId2 == id1); - uint32 floorId = offers.floorOfferId(address(token)); + uint256 floorId = offers.floorOfferId(address(token)); uint256 floorAmt = offers.floorOfferAmount(address(token)); - uint32 ceilingId = offers.ceilingOfferId(address(token)); + uint256 ceilingId = offers.ceilingOfferId(address(token)); uint256 ceilingAmt = offers.ceilingOfferAmount(address(token)); require(floorId == id3); @@ -222,8 +222,8 @@ contract CollectionOffersV1Test is DSTest { (address offeror4, , uint32 _prevId4, uint32 _nextId4, uint256 amount4) = offers.offers(address(token), 4); - uint32 floorId = offers.floorOfferId(address(token)); - uint32 ceilingId = offers.ceilingOfferId(address(token)); + uint256 floorId = offers.floorOfferId(address(token)); + uint256 ceilingId = offers.ceilingOfferId(address(token)); require(offeror4 == address(seller4)); require(amount4 == 1 ether); @@ -244,7 +244,7 @@ contract CollectionOffersV1Test is DSTest { offers.setOfferAmount{value: 3 ether}(address(token), 2, 5 ether); (, , uint32 _prevId2, uint32 _nextId2, uint256 amount2) = offers.offers(address(token), 2); - uint32 ceilingId = offers.ceilingOfferId(address(token)); + uint256 ceilingId = offers.ceilingOfferId(address(token)); require(ceilingId == 2); require(_prevId2 == 1 && _nextId2 == 0); @@ -308,8 +308,8 @@ contract CollectionOffersV1Test is DSTest { // Updated Order: id4 --> id1 --> id2 --> id3 (, , uint32 _prevId3, uint256 _nextId3, uint256 amount3) = offers.offers(address(token), 3); - uint32 floorId = offers.floorOfferId(address(token)); - uint32 ceilingId = offers.ceilingOfferId(address(token)); + 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); @@ -328,8 +328,8 @@ contract CollectionOffersV1Test is DSTest { // Updated Order: id4 --> id1 --> id3 --> id2 (, , uint32 _prevId3, uint32 _nextId3, uint256 amount3) = offers.offers(address(token), 3); - uint32 floorId = offers.floorOfferId(address(token)); - uint32 ceilingId = offers.ceilingOfferId(address(token)); + uint256 floorId = offers.floorOfferId(address(token)); + uint256 ceilingId = offers.ceilingOfferId(address(token)); // Ensure book is updated wrt time priority require(ceilingId == 2 && floorId == 4); From 65e8d9377c5c590c4f20fb04ae8d4322707a7cec Mon Sep 17 00:00:00 2001 From: joshieDo Date: Fri, 11 Feb 2022 20:29:28 +0400 Subject: [PATCH 6/9] _offerCount to save sloads --- .gas-snapshot | 58 +++++++++---------- .../V1/CollectionOfferBookV1.sol | 47 ++++++++++----- 2 files changed, 62 insertions(+), 43 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index b917a41a..d3a6b887 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -19,37 +19,37 @@ 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: 116854) -CollectionOffersV1IntegrationTest:test_RefundOfferDecreaseToSeller() (gas: 182344) -CollectionOffersV1IntegrationTest:test_WithdrawOfferFromSeller() (gas: 167691) -CollectionOffersV1IntegrationTest:test_WithdrawOfferIncreaseFromSeller() (gas: 180176) -CollectionOffersV1Test:testGas_CreateFirstCollectionOffer() (gas: 166662) -CollectionOffersV1Test:testRevert_CancelOfferMustBeMaker() (gas: 169631) -CollectionOffersV1Test:testRevert_FillMinimumTooHigh() (gas: 349546) +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: 348711) +CollectionOffersV1Test:testRevert_MustOwnCollectionToken() (gas: 346343) CollectionOffersV1Test:testRevert_UpdateFindersFeeMustBeRegistrar() (gas: 1903) -CollectionOffersV1Test:testRevert_UpdateOfferMustBeMaker() (gas: 348392) -CollectionOffersV1Test:test_CancelCollectionOffer() (gas: 63781) -CollectionOffersV1Test:test_CreateCeilingOffer() (gas: 235108) -CollectionOffersV1Test:test_CreateCollectionOffer() (gas: 174034) -CollectionOffersV1Test:test_CreateFloorOffer() (gas: 296501) -CollectionOffersV1Test:test_CreateMiddleOffer() (gas: 350134) -CollectionOffersV1Test:test_DecreaseCeilingInPlace() (gas: 363178) -CollectionOffersV1Test:test_DecreaseCeilingToFloor() (gas: 367693) -CollectionOffersV1Test:test_DecreaseCeilingToMiddle() (gas: 370844) -CollectionOffersV1Test:test_DecreaseFloor() (gas: 366362) -CollectionOffersV1Test:test_DecreaseMiddleInPlace() (gas: 362867) -CollectionOffersV1Test:test_DecreaseMiddleToFloor() (gas: 367591) -CollectionOffersV1Test:test_DecreaseMiddleToMiddle() (gas: 367825) -CollectionOffersV1Test:test_FillCollectionOffer() (gas: 374367) -CollectionOffersV1Test:test_IncreaseCeiling() (gas: 366135) -CollectionOffersV1Test:test_IncreaseFloorInPlace() (gas: 362161) -CollectionOffersV1Test:test_IncreaseFloorToMiddle() (gas: 368714) -CollectionOffersV1Test:test_IncreaseFloorToNewCeiling() (gas: 365225) -CollectionOffersV1Test:test_IncreaseMiddleInPlace() (gas: 360627) -CollectionOffersV1Test:test_IncreaseMiddleToCeiling() (gas: 363993) -CollectionOffersV1Test:test_IncreaseMiddleToMiddle() (gas: 366790) +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) diff --git a/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol b/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol index 0fff2717..72854363 100644 --- a/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol +++ b/contracts/modules/CollectionOffers/V1/CollectionOfferBookV1.sol @@ -57,38 +57,51 @@ contract CollectionOfferBookV1 { uint256 _amount, address _maker ) internal returns (uint256) { + uint256 _offerCount; unchecked { - ++offerCount; + _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: offerCount, prevId: 0, nextId: 0}); + offers[_collection][_offerCount] = Offer({maker: _maker, amount: _amount, id: uint32(_offerCount), prevId: 0, nextId: 0}); - floorOfferId[_collection] = offerCount; + floorOfferId[_collection] = _offerCount; floorOfferAmount[_collection] = _amount; - ceilingOfferId[_collection] = offerCount; + 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: offerCount, prevId: uint32(prevCeilingId), nextId: 0}); + 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; + 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: offerCount, prevId: 0, nextId: uint32(prevFloorId)}); + 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; + floorOfferId[_collection] = _offerCount; floorOfferAmount[_collection] = _amount; // Else offer is between floor and ceiling -- @@ -102,14 +115,20 @@ contract CollectionOfferBookV1 { } // Insert new offer before (time priority) - offers[_collection][offerCount] = Offer({maker: _maker, amount: _amount, id: offerCount, prevId: offer.prevId, nextId: offer.id}); + 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); + offers[_collection][offer.id].prevId = uint32(_offerCount); + offers[_collection][offer.prevId].nextId = uint32(_offerCount); } - return offerCount; + return _offerCount; } /// @notice Updates an offer and (if needed) its location relative to other offers in the collection From 1cfeb54af90d69ae705ad01c48d13f2e0db9c6b5 Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Mon, 28 Feb 2022 15:29:27 -0500 Subject: [PATCH 7/9] [fix] add gas limit --- .../V1/CollectionOffersV1.sol | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol b/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol index c4438a18..d7396f0a 100644 --- a/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol +++ b/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol @@ -25,10 +25,6 @@ contract CollectionOffersV1 is ModuleNamingSupportV1, CollectionOfferBookV1 { - /// @dev The indicator to denominate all transfers in ETH - address private constant ETH = address(0); - /// @dev The indicator to pass all remaining gas when paying out royalties - uint256 private constant USE_ALL_GAS_FLAG = 0; /// @notice The finders fee bps configured by the DAO uint16 public findersFeeBps; @@ -130,7 +126,7 @@ contract CollectionOffersV1 is /// @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, ETH); + _handleIncomingTransfer(msg.value, address(0)); // Add to collection's offer book uint256 offerId = _addOffer(_tokenContract, msg.value, msg.sender); @@ -193,13 +189,13 @@ contract CollectionOffersV1 is if (_amount > prevAmount) { unchecked { uint256 increaseAmount = _amount - prevAmount; - _handleIncomingTransfer(increaseAmount, ETH); + _handleIncomingTransfer(increaseAmount, address(0)); _updateOffer(offer, _tokenContract, _offerId, _amount, true); } } else { unchecked { uint256 decreaseAmount = prevAmount - _amount; - _handleOutgoingTransfer(msg.sender, decreaseAmount, ETH, USE_ALL_GAS_FLAG); + _handleOutgoingTransfer(msg.sender, decreaseAmount, address(0), 50000); _updateOffer(offer, _tokenContract, _offerId, _amount, false); } } @@ -245,7 +241,7 @@ contract CollectionOffersV1 is require(msg.sender == offer.maker, "cancelOffer must be maker"); // Refund offer - _handleOutgoingTransfer(msg.sender, offer.amount, ETH, USE_ALL_GAS_FLAG); + _handleOutgoingTransfer(msg.sender, offer.amount, address(0), 50000); emit CollectionOfferCanceled(_tokenContract, _offerId, msg.sender, offer.amount); @@ -333,10 +329,10 @@ contract CollectionOffersV1 is Offer memory offer = offers[_tokenContract][offerId]; // Ensure royalties are honored - (uint256 remainingProfit, ) = _handleRoyaltyPayout(_tokenContract, _tokenId, offer.amount, ETH, USE_ALL_GAS_FLAG); + (uint256 remainingProfit, ) = _handleRoyaltyPayout(_tokenContract, _tokenId, offer.amount, address(0), 300000); // Payout optional protocol fee - remainingProfit = _handleProtocolFeePayout(remainingProfit, ETH); + remainingProfit = _handleProtocolFeePayout(remainingProfit, address(0)); // Payout optional finder fee if (_finder != address(0)) { @@ -344,18 +340,18 @@ contract CollectionOffersV1 is // Calculate payout findersFee = (remainingProfit * findersFeeBps) / 10000; // Transfer to finder - _handleOutgoingTransfer(_finder, findersFee, ETH, USE_ALL_GAS_FLAG); + _handleOutgoingTransfer(_finder, findersFee, address(0), 50000); // Update remaining profit remainingProfit -= findersFee; } // Transfer remaining ETH to taker - _handleOutgoingTransfer(msg.sender, remainingProfit, ETH, USE_ALL_GAS_FLAG); + _handleOutgoingTransfer(msg.sender, remainingProfit, address(0), 50000); // Transfer NFT to maker erc721TransferHelper.transferFrom(_tokenContract, msg.sender, offer.maker, _tokenId); - ExchangeDetails memory userAExchangeDetails = ExchangeDetails({tokenContract: ETH, tokenId: 0, amount: offer.amount}); + 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); From ebbebd45038af2f886ea87d135b9895e15a7d760 Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Mon, 28 Feb 2022 15:37:04 -0500 Subject: [PATCH 8/9] [fix] events param id -> offerId --- .../CollectionOffers/V1/CollectionOffersV1.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol b/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol index d7396f0a..613ecd35 100644 --- a/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol +++ b/contracts/modules/CollectionOffers/V1/CollectionOffersV1.sol @@ -35,32 +35,32 @@ contract CollectionOffersV1 is /// @notice Emitted when a collection offer is created /// @param collection The ERC-721 token address of the created offer - /// @param id The ID 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 id, address maker, uint256 amount); + 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 id The ID 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 id, address maker, uint256 amount); + 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 id The ID 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 id, address maker, uint256 amount); + 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 id The 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 id, address taker, address finder); + 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 From 4e6b08a6849d42db0aadf9144df0087da033dfa2 Mon Sep 17 00:00:00 2001 From: "almndbtr.eth" <78528185+almndbtr@users.noreply.github.com> Date: Tue, 1 Mar 2022 02:34:50 +0000 Subject: [PATCH 9/9] test: add assertions when protocol fee is non-zero --- .../V1/CollectionOffers.integration.t.sol | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/contracts/test/modules/CollectionOffers/V1/CollectionOffers.integration.t.sol b/contracts/test/modules/CollectionOffers/V1/CollectionOffers.integration.t.sol index 0f4231ce..e6f08a3e 100644 --- a/contracts/test/modules/CollectionOffers/V1/CollectionOffers.integration.t.sol +++ b/contracts/test/modules/CollectionOffers/V1/CollectionOffers.integration.t.sol @@ -36,6 +36,7 @@ contract CollectionOffersV1IntegrationTest is DSTest { Zorb internal buyer; Zorb internal finder; Zorb internal royaltyRecipient; + Zorb internal protocolFeeRecipient; function setUp() public { // Cheatcodes @@ -45,6 +46,7 @@ contract CollectionOffersV1IntegrationTest is DSTest { 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)); @@ -73,6 +75,10 @@ contract CollectionOffersV1IntegrationTest is DSTest { ); 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); @@ -144,6 +150,7 @@ contract CollectionOffersV1IntegrationTest is DSTest { 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(); @@ -152,16 +159,19 @@ contract CollectionOffersV1IntegrationTest is DSTest { 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); - // 100 bps finders fee (Remaining 0.95 ETH * 10% finders fee = 0.0095 ETH) - require((afterFinderBalance - beforeFinderBalance) == 0.0095 ether); - // Remaining 0.9405 ETH paid to buyer - require((afterBuyerBalance - beforeBuyerBalance) == 0.9405 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)); }