From 99d24509c18aaa8aea995c88e7e8211897349011 Mon Sep 17 00:00:00 2001 From: James Geary <36774175+jgeary@users.noreply.github.com> Date: Fri, 7 Oct 2022 16:12:34 -0400 Subject: [PATCH] initial commit --- ...ionListingAdjustableBufferIncrementEth.sol | 56 ++ ...ionListingAdjustableBufferIncrementEth.sol | 583 ++++++++++++++++++ ...stableBufferIncrementEth.integration.t.sol | 165 +++++ ...nListingAdjustableBufferIncrementEth.t.sol | 510 +++++++++++++++ 4 files changed, 1314 insertions(+) create mode 100644 contracts/modules/ReserveAuction/Listing/ETH/IReserveAuctionListingAdjustableBufferIncrementEth.sol create mode 100644 contracts/modules/ReserveAuction/Listing/ETH/ReserveAuctionListingAdjustableBufferIncrementEth.sol create mode 100644 contracts/test/modules/ReserveAuction/Listing/ETH/ReserveAuctionListingAdjustableBufferIncrementEth.integration.t.sol create mode 100644 contracts/test/modules/ReserveAuction/Listing/ETH/ReserveAuctionListingAdjustableBufferIncrementEth.t.sol diff --git a/contracts/modules/ReserveAuction/Listing/ETH/IReserveAuctionListingAdjustableBufferIncrementEth.sol b/contracts/modules/ReserveAuction/Listing/ETH/IReserveAuctionListingAdjustableBufferIncrementEth.sol new file mode 100644 index 00000000..77da53a2 --- /dev/null +++ b/contracts/modules/ReserveAuction/Listing/ETH/IReserveAuctionListingAdjustableBufferIncrementEth.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.10; + +/// @title IReserveAuctionListingAdjustableBufferIncrementEth +/// @author jgeary +/// @notice Interface for Reserve Auction w/ Listing Fee, Adjustable Buffer & Increment ETH +interface IReserveAuctionListingAdjustableBufferIncrementEth { + /// @notice Creates an auction for a given NFT + /// @param _tokenContract The address of the ERC-721 token + /// @param _tokenId The id of the ERC-721 token + /// @param _duration The length of time the auction should run after the first bid + /// @param _reservePrice The minimum bid amount to start the auction + /// @param _sellerFundsRecipient The address to send funds to once the auction is complete + /// @param _startTime The time that users can begin placing bids + /// @param _listingFeeBps The fee to send to the lister of the auction + /// @param _listingFeeRecipient The address listing the auction + /// @param _timeBuffer Time buffer in seconds + /// @param _percentIncrement The minimum percent increase for a new bid + function createAuction( + address _tokenContract, + uint256 _tokenId, + uint256 _duration, + uint256 _reservePrice, + address _sellerFundsRecipient, + uint256 _startTime, + uint256 _listingFeeBps, + address _listingFeeRecipient, + uint16 _timeBuffer, + uint8 _percentIncrement + ) external; + + /// @notice Updates the reserve price for a given auction + /// @param _tokenContract The address of the ERC-721 token + /// @param _tokenId The id of the ERC-721 token + /// @param _reservePrice The new reserve price + function setAuctionReservePrice( + address _tokenContract, + uint256 _tokenId, + uint256 _reservePrice + ) external; + + /// @notice Cancels the auction for a given NFT + /// @param _tokenContract The address of the ERC-721 token + /// @param _tokenId The id of the ERC-721 token + function cancelAuction(address _tokenContract, uint256 _tokenId) external; + + /// @notice Places a bid on the auction for a given NFT + /// @param _tokenContract The address of the ERC-721 token + /// @param _tokenId The id of the ERC-721 token + function createBid(address _tokenContract, uint256 _tokenId) external payable; + + /// @notice Ends the auction for a given NFT + /// @param _tokenContract The address of the ERC-721 token + /// @param _tokenId The id of the ERC-721 token + function settleAuction(address _tokenContract, uint256 _tokenId) external; +} diff --git a/contracts/modules/ReserveAuction/Listing/ETH/ReserveAuctionListingAdjustableBufferIncrementEth.sol b/contracts/modules/ReserveAuction/Listing/ETH/ReserveAuctionListingAdjustableBufferIncrementEth.sol new file mode 100644 index 00000000..8da44475 --- /dev/null +++ b/contracts/modules/ReserveAuction/Listing/ETH/ReserveAuctionListingAdjustableBufferIncrementEth.sol @@ -0,0 +1,583 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.10; + +import {ReentrancyGuard} from "@rari-capital/solmate/src/utils/ReentrancyGuard.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +import {ERC721TransferHelper} from "../../../../transferHelpers/ERC721TransferHelper.sol"; +import {FeePayoutSupportV1} from "../../../../common/FeePayoutSupport/FeePayoutSupportV1.sol"; +import {ModuleNamingSupportV1} from "../../../../common/ModuleNamingSupport/ModuleNamingSupportV1.sol"; +import {IReserveAuctionListingAdjustableBufferIncrementEth} from "./IReserveAuctionListingAdjustableBufferIncrementEth.sol"; + +/// @title Reserve Auction Listing Adjustable Buffer Increment ETH +/// @author jgeary +/// @notice Module adding Adjustable Buffer Increment to Reserve Auction Listing Fee ETH +contract ReserveAuctionListingAdjustableBufferIncrementEth is + IReserveAuctionListingAdjustableBufferIncrementEth, + ReentrancyGuard, + FeePayoutSupportV1, + ModuleNamingSupportV1 +{ + /// /// + /// CONSTANTS /// + /// /// + + /// @notice The minimum amount of time left in an auction after a new bid is created + uint16 constant DEFAULT_TIME_BUFFER = 15 minutes; + + /// @notice The minimum percentage difference between two bids + uint8 constant DEFAULT_MIN_BID_INCREMENT_PERCENTAGE = 10; + + /// /// + /// IMMUTABLES /// + /// /// + + /// @notice The ZORA ERC-721 Transfer Helper + ERC721TransferHelper public immutable erc721TransferHelper; + + /// /// + /// CONSTRUCTOR /// + /// /// + + /// @param _erc721TransferHelper The ZORA ERC-721 Transfer Helper address + /// @param _royaltyEngine The Manifold Royalty Engine address + /// @param _protocolFeeSettings The ZORA Protocol Fee Settings address + /// @param _weth The WETH token address + constructor( + address _erc721TransferHelper, + address _royaltyEngine, + address _protocolFeeSettings, + address _weth + ) + FeePayoutSupportV1(_royaltyEngine, _protocolFeeSettings, _weth, ERC721TransferHelper(_erc721TransferHelper).ZMM().registrar()) + ModuleNamingSupportV1("Reserve Auction Listing ETH") + { + erc721TransferHelper = ERC721TransferHelper(_erc721TransferHelper); + } + + /// /// + /// EIP-165 /// + /// /// + + /// @notice Implements EIP-165 for standard interface detection + /// @dev `0x01ffc9a7` is the IERC165 interface id + /// @param _interfaceId The identifier of a given interface + /// @return If the given interface is supported + function supportsInterface(bytes4 _interfaceId) external pure returns (bool) { + return _interfaceId == type(IReserveAuctionListingAdjustableBufferIncrementEth).interfaceId || _interfaceId == 0x01ffc9a7; + } + + /// /// + /// AUCTION STORAGE /// + /// /// + + /// @notice The metadata for a given auction + /// @param seller The address of the seller + /// @param reservePrice The reserve price to start the auction + /// @param sellerFundsRecipient The address where funds are sent after the auction + /// @param highestBid The highest bid of the auction + /// @param highestBidder The address of the highest bidder + /// @param duration The length of time that the auction runs after the first bid is placed + /// @param startTime The time that the first bid can be placed + /// @param listingFeeRecipient The address that listed the auction + /// @param firstBidTime The time that the first bid is placed + /// @param listingFeeBps The fee that is sent to the lister of the auction + /// @param timeBuffer Time buffer in seconds + /// @param percentIncrement The minimum percent increase for a new bid + struct Auction { + address seller; + uint96 reservePrice; + address sellerFundsRecipient; + uint96 highestBid; + address highestBidder; + uint48 duration; + uint48 startTime; + address listingFeeRecipient; + uint80 firstBidTime; + uint16 listingFeeBps; + uint16 timeBuffer; + uint8 percentIncrement; + } + + /// @notice The auction for a given NFT, if one exists + /// @dev ERC-721 token contract => ERC-721 token id => Auction + mapping(address => mapping(uint256 => Auction)) public auctionForNFT; + + /// /// + /// CREATE AUCTION /// + /// /// + + // ,-. + // `-' + // /|\ + // | ,------------------------. + // / \ |ReserveAuctionListingEth| + // Caller `-----------+------------' + // | createAuction() | + // | -------------------------> + // | | + // | |----. + // | | | store auction metadata + // | |<---' + // | | + // | |----. + // | | | emit AuctionCreated() + // | |<---' + // Caller ,-----------+------------. + // ,-. |ReserveAuctionListingEth| + // `-' `------------------------' + // /|\ + // | + // / \ + + /// @notice Emitted when an auction is created + /// @param tokenContract The ERC-721 token address of the created auction + /// @param tokenId The ERC-721 token id of the created auction + /// @param auction The metadata of the created auction + event AuctionCreated(address indexed tokenContract, uint256 indexed tokenId, Auction auction); + + /// @notice Creates an auction for a given NFT + /// @param _tokenContract The address of the ERC-721 token + /// @param _tokenId The id of the ERC-721 token + /// @param _duration The length of time the auction should run after the first bid + /// @param _reservePrice The minimum bid amount to start the auction + /// @param _sellerFundsRecipient The address to send funds to once the auction is complete + /// @param _startTime The time that users can begin placing bids + /// @param _listingFeeBps The fee to send to the lister of the auction + /// @param _listingFeeRecipient The address listing the auction + function createAuction( + address _tokenContract, + uint256 _tokenId, + uint256 _duration, + uint256 _reservePrice, + address _sellerFundsRecipient, + uint256 _startTime, + uint256 _listingFeeBps, + address _listingFeeRecipient, + uint16 _timeBuffer, + uint8 _percentIncrement + ) external nonReentrant { + // Get the owner of the specified token + address tokenOwner = IERC721(_tokenContract).ownerOf(_tokenId); + + // Ensure the caller is the owner or an approved operator + require(msg.sender == tokenOwner || IERC721(_tokenContract).isApprovedForAll(tokenOwner, msg.sender), "ONLY_TOKEN_OWNER_OR_OPERATOR"); + + // Ensure the funds recipient is specified + require(_sellerFundsRecipient != address(0), "INVALID_FUNDS_RECIPIENT"); + + // Ensure the listing fee does not exceed 10,000 basis points + require(_listingFeeBps <= 10000, "INVALID_LISTING_FEE"); + + // Get the auction's storage pointer + Auction storage auction = auctionForNFT[_tokenContract][_tokenId]; + + if (_timeBuffer > 0) { + require(_timeBuffer >= 1 minutes && _timeBuffer <= 1 hours, "INVALID_TIME_BUFFER"); + auction.timeBuffer = _timeBuffer; + } + + if (_percentIncrement > 0) { + require(_percentIncrement < 50, "INVALID_PERCENT_INCREMENT"); + auction.percentIncrement = _percentIncrement; + } + + // Store the associated metadata + auction.seller = tokenOwner; + auction.reservePrice = uint96(_reservePrice); + auction.sellerFundsRecipient = _sellerFundsRecipient; + auction.duration = uint48(_duration); + auction.startTime = uint48(_startTime); + auction.listingFeeRecipient = _listingFeeRecipient; + auction.listingFeeBps = uint16(_listingFeeBps); + + emit AuctionCreated(_tokenContract, _tokenId, auction); + } + + /// /// + /// UPDATE RESERVE PRICE /// + /// /// + + // ,-. + // `-' + // /|\ + // | ,------------------------. + // / \ |ReserveAuctionListingEth| + // Caller `-----------+------------' + // | setAuctionReservePrice() | + // | -------------------------> + // | | + // | |----. + // | | | update reserve price + // | |<---' + // | | + // | |----. + // | | | emit AuctionReservePriceUpdated() + // | |<---' + // Caller ,-----------+------------. + // ,-. |ReserveAuctionListingEth| + // `-' `------------------------' + // /|\ + // | + // / \ + + /// @notice Emitted when a reserve price is updated + /// @param tokenContract The ERC-721 token address of the updated auction + /// @param tokenId The ERC-721 token id of the updated auction + /// @param auction The metadata of the updated auction + event AuctionReservePriceUpdated(address indexed tokenContract, uint256 indexed tokenId, Auction auction); + + /// @notice Updates the reserve price for a given auction + /// @param _tokenContract The address of the ERC-721 token + /// @param _tokenId The id of the ERC-721 token + /// @param _reservePrice The new reserve price + function setAuctionReservePrice( + address _tokenContract, + uint256 _tokenId, + uint256 _reservePrice + ) external nonReentrant { + // Get the auction for the specified token + Auction storage auction = auctionForNFT[_tokenContract][_tokenId]; + + // Ensure the auction has not started + require(auction.firstBidTime == 0, "AUCTION_STARTED"); + + // Ensure the caller is the seller + require(msg.sender == auction.seller, "ONLY_SELLER"); + + // Update the reserve price + auction.reservePrice = uint96(_reservePrice); + + emit AuctionReservePriceUpdated(_tokenContract, _tokenId, auction); + } + + /// /// + /// CANCEL AUCTION /// + /// /// + + // ,-. + // `-' + // /|\ + // | ,------------------------. + // / \ |ReserveAuctionListingEth| + // Caller `-----------+------------' + // | cancelAuction() | + // | -------------------------> + // | | + // | |----. + // | | | emit AuctionCanceled() + // | |<---' + // | | + // | |----. + // | | | delete auction + // | |<---' + // Caller ,-----------+------------. + // ,-. |ReserveAuctionListingEth| + // `-' `------------------------' + // /|\ + // | + // / \ + + /// @notice Emitted when an auction is canceled + /// @param tokenContract The ERC-721 token address of the canceled auction + /// @param tokenId The ERC-721 token id of the canceled auction + /// @param auction The metadata of the canceled auction + event AuctionCanceled(address indexed tokenContract, uint256 indexed tokenId, Auction auction); + + /// @notice Cancels the auction for a given NFT + /// @param _tokenContract The address of the ERC-721 token + /// @param _tokenId The id of the ERC-721 token + function cancelAuction(address _tokenContract, uint256 _tokenId) external nonReentrant { + // Get the auction for the specified token + Auction memory auction = auctionForNFT[_tokenContract][_tokenId]; + + // Ensure the auction has not started + require(auction.firstBidTime == 0, "AUCTION_STARTED"); + + // Ensure the caller is the seller or a new owner of the token + require(msg.sender == auction.seller || msg.sender == IERC721(_tokenContract).ownerOf(_tokenId), "ONLY_SELLER_OR_TOKEN_OWNER"); + + emit AuctionCanceled(_tokenContract, _tokenId, auction); + + // Remove the auction from storage + delete auctionForNFT[_tokenContract][_tokenId]; + } + + /// /// + /// CREATE BID /// + /// /// + + // ,-. + // `-' + // /|\ + // | ,------------------------. ,--------------------. + // / \ |ReserveAuctionListingEth| |ERC721TransferHelper| + // Caller `-----------+------------' `---------+----------' + // | createBid() | | + // | -------------------------> | + // | | | + // | | | + // | ___________________________________________________________________ + // | ! ALT / First bid? | | ! + // | !_____/ | | ! + // | ! |----. | ! + // | ! | | start auction | ! + // | ! |<---' | ! + // | ! | | ! + // | ! |----. | ! + // | ! | | transferFrom() | ! + // | ! |<---' | ! + // | ! | | ! + // | ! |----. ! + // | ! | | transfer NFT from seller to escrow ! + // | ! |<---' ! + // | !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~! + // | ! [refund previous bidder] | ! + // | ! |----. | ! + // | ! | | transfer ETH to bidder | ! + // | ! |<---' | ! + // | !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~! + // | | | + // | | | + // | _______________________________________________ | + // | ! ALT / Bid placed within 15 min of end? ! | + // | !_____/ | ! | + // | ! |----. ! | + // | ! | | extend auction ! | + // | ! |<---' ! | + // | !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~! | + // | !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~! | + // | | | + // | |----. | + // | | | emit AuctionBid() | + // | |<---' | + // Caller ,-----------+------------. ,---------+----------. + // ,-. |ReserveAuctionListingEth| |ERC721TransferHelper| + // `-' `------------------------' `--------------------' + // /|\ + // | + // / \ + + /// @notice Emitted when a bid is placed + /// @param tokenContract The ERC-721 token address of the auction + /// @param tokenId The ERC-721 token id of the auction + /// @param firstBid If the bid started the auction + /// @param extended If the bid extended the auction + /// @param auction The metadata of the auction + event AuctionBid(address indexed tokenContract, uint256 indexed tokenId, bool firstBid, bool extended, Auction auction); + + /// @notice Places a bid on the auction for a given NFT + /// @param _tokenContract The address of the ERC-721 token + /// @param _tokenId The id of the ERC-721 token + function createBid(address _tokenContract, uint256 _tokenId) external payable nonReentrant { + // Get the auction for the specified token + Auction storage auction = auctionForNFT[_tokenContract][_tokenId]; + + // Cache the seller + address seller = auction.seller; + + // Ensure the auction exists + require(seller != address(0), "AUCTION_DOES_NOT_EXIST"); + + // Ensure the auction has started or is valid to start + require(block.timestamp >= auction.startTime, "AUCTION_NOT_STARTED"); + + // Cache more auction metadata + uint256 firstBidTime = auction.firstBidTime; + uint256 duration = auction.duration; + + // Used to emit whether the bid started the auction + bool firstBid; + + // If this is the first bid, start the auction + if (firstBidTime == 0) { + // Ensure the bid meets the reserve price + require(msg.value >= auction.reservePrice, "RESERVE_PRICE_NOT_MET"); + + // Store the current time as the first bid time + auction.firstBidTime = uint80(block.timestamp); + + // Mark this bid as the first + firstBid = true; + + // Transfer the NFT from the seller into escrow for the duration of the auction + // Reverts if the seller did not approve the ERC721TransferHelper or no longer owns the token + erc721TransferHelper.transferFrom(_tokenContract, seller, address(this), _tokenId); + + // Else this is a subsequent bid, so refund the previous bidder + } else { + // Ensure the auction has not ended + require(block.timestamp < (firstBidTime + duration), "AUCTION_OVER"); + + // Cache the highest bid + uint256 highestBid = auction.highestBid; + + // Used to store the minimum bid required to outbid the highest bidder + uint256 minValidBid; + + // Calculate the minimum bid required (10% higher than the highest bid) + // Cannot overflow as the highest bid would have to be magnitudes higher than the total supply of ETH + uint8 minPercentIncrement; + if (auction.percentIncrement > 0) { + minPercentIncrement = auction.percentIncrement; + } else { + minPercentIncrement = DEFAULT_MIN_BID_INCREMENT_PERCENTAGE; + } + unchecked { + minValidBid = highestBid + ((highestBid * minPercentIncrement) / 100); + } + + // Ensure the incoming bid meets the minimum + require(msg.value >= minValidBid, "MINIMUM_BID_NOT_MET"); + + // Refund the previous bidder + _handleOutgoingTransfer(auction.highestBidder, highestBid, address(0), 50000); + } + + // Store the attached ETH as the highest bid + auction.highestBid = uint96(msg.value); + + // Store the caller as the highest bidder + auction.highestBidder = msg.sender; + + // Used to emit whether the bid extended the auction + bool extended; + + // Used to store the auction time remaining + uint256 timeRemaining; + + // Get the auction time remaining + // Cannot underflow as `firstBidTime + duration` is ensured to be greater than `block.timestamp` + unchecked { + timeRemaining = firstBidTime + duration - block.timestamp; + } + + // If the bid is placed within 15 minutes of the auction end, extend the auction + uint16 timeBuffer; + if (auction.timeBuffer > 0) { + timeBuffer = auction.timeBuffer; + } else { + timeBuffer = DEFAULT_TIME_BUFFER; + } + if (timeRemaining < timeBuffer) { + // Add (time buffer - remaining time) to the duration so that the buffer remains + // Cannot underflow as `timeRemaining` is ensured to be less than `timeBuffer` + unchecked { + auction.duration += uint48(timeBuffer - timeRemaining); + } + + // Mark the bid as one that extended the auction + extended = true; + } + + emit AuctionBid(_tokenContract, _tokenId, firstBid, extended, auction); + } + + /// /// + /// SETTLE AUCTION /// + /// /// + + // ,-. + // `-' + // /|\ + // | ,------------------------. + // / \ |ReserveAuctionListingEth| + // Caller `-----------+------------' + // | settleAuction() | + // | -------------------------> + // | | + // | |----. + // | | | validate auction ended + // | |<---' + // | | + // | |----. + // | | | handle royalty payouts + // | |<---' + // | | + // | | + // | __________________________________________________________ + // | ! ALT / listing fee configured for this auction? ! + // | !_____/ | ! + // | ! |----. ! + // | ! | | handle listing fee payout ! + // | ! |<---' ! + // | !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~! + // | !~[noop]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~! + // | | + // | |----. + // | | | handle seller funds recipient payout + // | |<---' + // | | + // | |----. + // | | | transfer NFT from escrow to winning bidder + // | |<---' + // | | + // | |----. + // | | | emit AuctionEnded() + // | |<---' + // | | + // | |----. + // | | | delete auction from contract + // | |<---' + // Caller ,-----------+------------. + // ,-. |ReserveAuctionListingEth| + // `-' `------------------------' + // /|\ + // | + // / \ + + /// @notice Emitted when an auction has ended + /// @param tokenContract The ERC-721 token address of the auction + /// @param tokenId The ERC-721 token id of the auction + /// @param auction The metadata of the settled auction + event AuctionEnded(address indexed tokenContract, uint256 indexed tokenId, Auction auction); + + /// @notice Ends the auction for a given NFT + /// @param _tokenContract The address of the ERC-721 token + /// @param _tokenId The id of the ERC-721 token + function settleAuction(address _tokenContract, uint256 _tokenId) external nonReentrant { + // Get the auction for the specified token + Auction memory auction = auctionForNFT[_tokenContract][_tokenId]; + + // Cache the time of the first bid + uint256 firstBidTime = auction.firstBidTime; + + // Ensure the auction had started + require(firstBidTime != 0, "AUCTION_NOT_STARTED"); + + // Ensure the auction has ended + require(block.timestamp >= (firstBidTime + auction.duration), "AUCTION_NOT_OVER"); + + // Payout associated token royalties, if any + (uint256 remainingProfit, ) = _handleRoyaltyPayout(_tokenContract, _tokenId, auction.highestBid, address(0), 300000); + + // Payout the module fee, if configured by the owner + remainingProfit = _handleProtocolFeePayout(remainingProfit, address(0)); + + // Cache the listing fee recipient + address listingFeeRecipient = auction.listingFeeRecipient; + + // Payout the listing fee, if a recipient exists + if (listingFeeRecipient != address(0)) { + // Get the listing fee from the remaining profit + uint256 listingFee = (remainingProfit * auction.listingFeeBps) / 10000; + + // Transfer the amount to the listing fee recipient + _handleOutgoingTransfer(listingFeeRecipient, listingFee, address(0), 50000); + + // Update the remaining profit + remainingProfit -= listingFee; + } + + // Transfer the remaining profit to the funds recipient + _handleOutgoingTransfer(auction.sellerFundsRecipient, remainingProfit, address(0), 50000); + + // Transfer the NFT to the winning bidder + IERC721(_tokenContract).transferFrom(address(this), auction.highestBidder, _tokenId); + + emit AuctionEnded(_tokenContract, _tokenId, auction); + + // Remove the auction from storage + delete auctionForNFT[_tokenContract][_tokenId]; + } +} diff --git a/contracts/test/modules/ReserveAuction/Listing/ETH/ReserveAuctionListingAdjustableBufferIncrementEth.integration.t.sol b/contracts/test/modules/ReserveAuction/Listing/ETH/ReserveAuctionListingAdjustableBufferIncrementEth.integration.t.sol new file mode 100644 index 00000000..ee96ed64 --- /dev/null +++ b/contracts/test/modules/ReserveAuction/Listing/ETH/ReserveAuctionListingAdjustableBufferIncrementEth.integration.t.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.10; + +import {DSTest} from "ds-test/test.sol"; + +import {ReserveAuctionListingAdjustableBufferIncrementEth} from "../../../../../modules/ReserveAuction/Listing/ETH/ReserveAuctionListingAdjustableBufferIncrementEth.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 ReserveAuctionListingAdjustableBufferIncrementEthIntegrationTest +/// @notice Integration Tests for Reserve Auction Listing w/ Adjustable Buffer & Increment ETH +contract ReserveAuctionListingAdjustableBufferIncrementEthIntegrationTest is DSTest { + VM internal vm; + + ZoraRegistrar internal registrar; + ZoraProtocolFeeSettings internal ZPFS; + ZoraModuleManager internal ZMM; + ERC20TransferHelper internal erc20TransferHelper; + ERC721TransferHelper internal erc721TransferHelper; + RoyaltyEngine internal royaltyEngine; + + ReserveAuctionListingAdjustableBufferIncrementEth internal auctions; + TestERC721 internal token; + WETH internal weth; + + Zorb internal seller; + Zorb internal sellerFundsRecipient; + Zorb internal operator; + Zorb internal bidder; + Zorb internal otherBidder; + Zorb internal listingFeeRecipient; + Zorb internal royaltyRecipient; + Zorb internal protocolFeeRecipient; + + function setUp() public { + // Cheatcodes + vm = VM(HEVM_ADDRESS); + + // 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)); + sellerFundsRecipient = new Zorb(address(ZMM)); + operator = new Zorb(address(ZMM)); + bidder = new Zorb(address(ZMM)); + otherBidder = new Zorb(address(ZMM)); + listingFeeRecipient = new Zorb(address(ZMM)); + royaltyRecipient = new Zorb(address(ZMM)); + protocolFeeRecipient = new Zorb(address(ZMM)); + + // Deploy mocks + royaltyEngine = new RoyaltyEngine(address(royaltyRecipient)); + token = new TestERC721(); + weth = new WETH(); + + auctions = new ReserveAuctionListingAdjustableBufferIncrementEth( + address(erc721TransferHelper), + address(royaltyEngine), + address(ZPFS), + address(weth) + ); + registrar.registerModule(address(auctions)); + + // Set module fee + vm.prank(address(registrar)); + ZPFS.setFeeParams(address(auctions), address(protocolFeeRecipient), 1); + + // Set balances + vm.deal(address(seller), 100 ether); + vm.deal(address(bidder), 100 ether); + vm.deal(address(otherBidder), 100 ether); + + // Mint seller token + token.mint(address(seller), 0); + + // Bidder swap 50 ETH <> 50 WETH + vm.prank(address(bidder)); + weth.deposit{value: 50 ether}(); + + // otherBidder swap 50 ETH <> 50 WETH + vm.prank(address(otherBidder)); + weth.deposit{value: 50 ether}(); + + // Users approve module + seller.setApprovalForModule(address(auctions), true); + bidder.setApprovalForModule(address(auctions), true); + otherBidder.setApprovalForModule(address(auctions), true); + + // Seller approve ERC721TransferHelper + vm.prank(address(seller)); + token.setApprovalForAll(address(erc721TransferHelper), true); + } + + function runETH() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 0.1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.warp(1 hours); + vm.prank(address(bidder)); + auctions.createBid{value: 0.1 ether}(address(token), 0); + + vm.warp(10 hours); + vm.prank(address(otherBidder)); + auctions.createBid{value: 0.5 ether}(address(token), 0); + + vm.warp(1 days); + vm.prank(address(bidder)); + auctions.createBid{value: 1 ether}(address(token), 0); + + vm.warp(1 days + 1 hours); + auctions.settleAuction(address(token), 0); + } + + function test_ETHIntegration() public { + uint256 beforeSellerBalance = address(sellerFundsRecipient).balance; + uint256 beforeBidderBalance = address(bidder).balance; + uint256 beforeOtherBidderBalance = address(otherBidder).balance; + uint256 beforeRoyaltyRecipientBalance = address(royaltyRecipient).balance; + uint256 beforelistingFeeRecipientBalance = address(listingFeeRecipient).balance; + uint256 beforeProtocolFeeRecipient = address(protocolFeeRecipient).balance; + address beforeTokenOwner = token.ownerOf(0); + + runETH(); + + uint256 afterSellerBalance = address(sellerFundsRecipient).balance; + uint256 afterBidderBalance = address(bidder).balance; + uint256 afterOtherBidderBalance = address(otherBidder).balance; + uint256 afterRoyaltyRecipientBalance = address(royaltyRecipient).balance; + uint256 afterlistingFeeRecipientBalance = address(listingFeeRecipient).balance; + uint256 afterProtocolFeeRecipient = address(protocolFeeRecipient).balance; + address afterTokenOwner = token.ownerOf(0); + + // 1 ETH withdrawn from winning bidder + require((beforeBidderBalance - afterBidderBalance) == 1 ether); + // Losing bidder refunded + require(beforeOtherBidderBalance == afterOtherBidderBalance); + // 0.05 ETH creator royalty + require((afterRoyaltyRecipientBalance - beforeRoyaltyRecipientBalance) == 0.05 ether); + // 1 bps protocol fee (Remaining 0.95 ETH * 0.01% protocol fee = 0.000095 ETH) + require((afterProtocolFeeRecipient - beforeProtocolFeeRecipient) == 0.000095 ether); + // 1000 bps listing fee (Remaining 0.949905 ETH * 10% listing fee = 0.0949905 ETH) + require((afterlistingFeeRecipientBalance - beforelistingFeeRecipientBalance) == 0.0949905 ether); + // Remaining 0.8549145 ETH paid to seller + require((afterSellerBalance - beforeSellerBalance) == 0.8549145 ether); + // NFT transferred to winning bidder + require(beforeTokenOwner == address(seller) && afterTokenOwner == address(bidder)); + } +} diff --git a/contracts/test/modules/ReserveAuction/Listing/ETH/ReserveAuctionListingAdjustableBufferIncrementEth.t.sol b/contracts/test/modules/ReserveAuction/Listing/ETH/ReserveAuctionListingAdjustableBufferIncrementEth.t.sol new file mode 100644 index 00000000..c209eef4 --- /dev/null +++ b/contracts/test/modules/ReserveAuction/Listing/ETH/ReserveAuctionListingAdjustableBufferIncrementEth.t.sol @@ -0,0 +1,510 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.10; + +import {DSTest} from "ds-test/test.sol"; + +import {ReserveAuctionListingAdjustableBufferIncrementEth} from "../../../../../modules/ReserveAuction/Listing/ETH/ReserveAuctionListingAdjustableBufferIncrementEth.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 ReserveAuctionListingAdjustableBufferIncrementEthTest +/// @notice Unit Tests for Reserve Auction Listing w/ Adjustable Buffer & Increment ETH +contract ReserveAuctionListingAdjustableBufferIncrementEthTest is DSTest { + VM internal vm; + + ZoraRegistrar internal registrar; + ZoraProtocolFeeSettings internal ZPFS; + ZoraModuleManager internal ZMM; + ERC20TransferHelper internal erc20TransferHelper; + ERC721TransferHelper internal erc721TransferHelper; + RoyaltyEngine internal royaltyEngine; + + ReserveAuctionListingAdjustableBufferIncrementEth internal auctions; + + TestERC721 internal token; + WETH internal weth; + Zorb internal seller; + Zorb internal sellerFundsRecipient; + Zorb internal operator; + Zorb internal bidder; + Zorb internal otherBidder; + Zorb internal listingFeeRecipient; + Zorb internal royaltyRecipient; + + function setUp() public { + // Cheatcodes + vm = VM(HEVM_ADDRESS); + + // 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)); + sellerFundsRecipient = new Zorb(address(ZMM)); + operator = new Zorb(address(ZMM)); + bidder = new Zorb(address(ZMM)); + otherBidder = new Zorb(address(ZMM)); + listingFeeRecipient = new Zorb(address(ZMM)); + royaltyRecipient = new Zorb(address(ZMM)); + + // Deploy mocks + royaltyEngine = new RoyaltyEngine(address(royaltyRecipient)); + token = new TestERC721(); + weth = new WETH(); + + // Deploy Reserve Auction Listing ETH + auctions = new ReserveAuctionListingAdjustableBufferIncrementEth( + address(erc721TransferHelper), + address(royaltyEngine), + address(ZPFS), + address(weth) + ); + registrar.registerModule(address(auctions)); + + // Set balances + vm.deal(address(seller), 100 ether); + vm.deal(address(bidder), 100 ether); + vm.deal(address(otherBidder), 100 ether); + + // Mint seller token + token.mint(address(seller), 0); + + // Users approve module + seller.setApprovalForModule(address(auctions), true); + bidder.setApprovalForModule(address(auctions), true); + otherBidder.setApprovalForModule(address(auctions), true); + + // Seller approve ERC721TransferHelper + vm.prank(address(seller)); + token.setApprovalForAll(address(erc721TransferHelper), true); + } + + /// /// + /// CREATE AUCTION /// + /// /// + + function test_CreateAuction() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + ( + address creator, + uint256 reservePrice, + address fundsRecipient, + uint256 highestBid, + address highestBidder, + uint256 duration, + uint256 startTime, + address lister, + uint256 firstBidTime, + uint256 listingFeeBps, + , + + ) = auctions.auctionForNFT(address(token), 0); + + require(creator == address(seller)); + require(reservePrice == 1 ether); + require(fundsRecipient == address(sellerFundsRecipient)); + require(highestBid == 0 ether); + require(highestBidder == address(0)); + require(duration == 1 days); + require(startTime == 0); + require(lister == address(listingFeeRecipient)); + require(listingFeeBps == 1000); + require(firstBidTime == 0); + } + + function test_CreateFutureAuction() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 1 days, 1000, address(listingFeeRecipient), 0, 0); + + (, , , , , , uint256 startTime, , , , , ) = auctions.auctionForNFT(address(token), 0); + require(startTime == 1 days); + } + + function test_CreateAuctionAndCancelPrevious() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.prank(address(seller)); + token.transferFrom(address(seller), address(sellerFundsRecipient), 0); + + sellerFundsRecipient.setApprovalForModule(address(auctions), true); + + vm.startPrank(address(sellerFundsRecipient)); + token.setApprovalForAll(address(erc721TransferHelper), true); + auctions.createAuction(address(token), 0, 5 days, 12 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + vm.stopPrank(); + + (address creator, uint256 reservePrice, , , , uint256 duration, , , , , , ) = auctions.auctionForNFT(address(token), 0); + require(creator == address(sellerFundsRecipient)); + require(duration == 5 days); + require(reservePrice == 12 ether); + } + + function testRevert_MustBeTokenOwnerOrOperator() public { + vm.expectRevert("ONLY_TOKEN_OWNER_OR_OPERATOR"); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + } + + function testRevert_ListingFeeBPSCannotExceed10000() public { + vm.prank(address(seller)); + vm.expectRevert("INVALID_LISTING_FEE"); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 10001, address(listingFeeRecipient), 0, 0); + } + + function testRevert_MustSpecifySellerFundsRecipient() public { + vm.prank(address(seller)); + vm.expectRevert("INVALID_FUNDS_RECIPIENT"); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(0), 0, 1000, address(listingFeeRecipient), 0, 0); + } + + /// /// + /// UPDATE RESERVE PRICE /// + /// /// + + function test_SetReservePrice() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.prank(address(seller)); + auctions.setAuctionReservePrice(address(token), 0, 5 ether); + + (, uint256 reservePrice, , , , , , , , , , ) = auctions.auctionForNFT(address(token), 0); + require(reservePrice == 5 ether); + } + + function testRevert_UpdateMustBeSeller() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.expectRevert("ONLY_SELLER"); + auctions.setAuctionReservePrice(address(token), 0, 5 ether); + } + + function testRevert_CannotUpdateAuctionDoesNotExist() public { + vm.expectRevert("ONLY_SELLER"); + auctions.setAuctionReservePrice(address(token), 0, 5 ether); + } + + function testRevert_CannotUpdateActiveAuction() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.warp(1 hours); + + vm.prank(address(bidder)); + auctions.createBid{value: 5 ether}(address(token), 0); + + vm.prank(address(seller)); + vm.expectRevert("AUCTION_STARTED"); + auctions.setAuctionReservePrice(address(token), 0, 20 ether); + } + + /// /// + /// CANCEL AUCTION /// + /// /// + + function test_CancelAuction() public { + vm.startPrank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.warp(1 minutes); + + auctions.cancelAuction(address(token), 0); + vm.stopPrank(); + + (address creator, , , , , , , , , , , ) = auctions.auctionForNFT(address(token), 0); + require(creator == address(0)); + } + + function testRevert_OnlySellerOrOwnerCanCancel() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.expectRevert("ONLY_SELLER_OR_TOKEN_OWNER"); + auctions.cancelAuction(address(token), 0); + } + + function testRevert_CannotCancelActiveAuction() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.warp(1 hours); + + vm.prank(address(bidder)); + auctions.createBid{value: 1 ether}(address(token), 0); + + vm.prank(address(seller)); + vm.expectRevert("AUCTION_STARTED"); + auctions.cancelAuction(address(token), 0); + } + + /// /// + /// CREATE BID /// + /// /// + + function test_CreateFirstBid() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + vm.prank(address(bidder)); + auctions.createBid{value: 1 ether}(address(token), 0); + } + + function test_StoreTimeOfFirstBid() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.warp(1 hours); + vm.prank(address(bidder)); + auctions.createBid{value: 1 ether}(address(token), 0); + + (, , , , , , , , uint256 firstBidTime, , , ) = auctions.auctionForNFT(address(token), 0); + require(firstBidTime == 1 hours); + } + + function test_RefundPreviousBidder() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.warp(1 hours); + vm.prank(address(bidder)); + auctions.createBid{value: 1 ether}(address(token), 0); + uint256 beforeBalance = address(bidder).balance; + + vm.prank(address(otherBidder)); + auctions.createBid{value: 2 ether}(address(token), 0); + + uint256 afterBalance = address(bidder).balance; + + require(afterBalance - beforeBalance == 1 ether); + } + + function test_TransferNFTIntoEscrow() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + vm.prank(address(bidder)); + auctions.createBid{value: 1 ether}(address(token), 0); + require(token.ownerOf(0) == address(auctions)); + } + + function test_ExtendAuction() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 hours, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.warp(5 minutes); + vm.prank(address(bidder)); + auctions.createBid{value: 1 ether}(address(token), 0); + + vm.warp(55 minutes); + vm.prank(address(otherBidder)); + auctions.createBid{value: 2 ether}(address(token), 0); + + (, , , , , uint256 newDuration, , , , , , ) = auctions.auctionForNFT(address(token), 0); + + require(newDuration == 1 hours + 5 minutes); + } + + function test_ExtendAuctionWithProvidedDuration() public { + vm.prank(address(seller)); + auctions.createAuction( + address(token), + 0, + 1 hours, + 1 ether, + address(sellerFundsRecipient), + 0, + 1000, + address(listingFeeRecipient), + 2 minutes, + 0 + ); + + vm.warp(1 minutes); + vm.prank(address(bidder)); + auctions.createBid{value: 1 ether}(address(token), 0); + + vm.warp(1 hours); // 59 minutes into auction, 1 minute left + vm.prank(address(otherBidder)); + auctions.createBid{value: 2 ether}(address(token), 0); + + (, , , , , uint256 newDuration, , , , , , ) = auctions.auctionForNFT(address(token), 0); + + assertEq(newDuration, 1 hours + 1 minutes); // time of last bid + 2 minute buffer + } + + function testRevert_MustApproveModule() public { + seller.setApprovalForModule(address(auctions), false); + + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 hours, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.prank(address(bidder)); + vm.expectRevert("module has not been approved by user"); + auctions.createBid{value: 1 ether}(address(token), 0); + } + + function testRevert_SellerMustApproveERC721TransferHelper() public { + vm.prank(address(seller)); + token.setApprovalForAll(address(erc721TransferHelper), false); + + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 hours, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.prank(address(bidder)); + vm.expectRevert("ERC721: transfer caller is not owner nor approved"); + auctions.createBid{value: 1 ether}(address(token), 0); + } + + function testRevert_InvalidTransferBeforeFirstBid() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 hours, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.prank(address(seller)); + token.transferFrom(address(seller), address(otherBidder), 0); + + vm.prank(address(bidder)); + vm.expectRevert("ERC721: transfer caller is not owner nor approved"); + auctions.createBid{value: 1 ether}(address(token), 0); + } + + function testRevert_CannotBidOnExpiredAuction() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 10 hours, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.warp(1 hours); + + vm.prank(address(bidder)); + auctions.createBid{value: 1 ether}(address(token), 0); + + vm.warp(12 hours); + + vm.prank(address(otherBidder)); + vm.expectRevert("AUCTION_OVER"); + auctions.createBid{value: 2 ether}(address(token), 0); + } + + function testRevert_CannotBidOnAuctionNotStarted() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 1 days, 1000, address(listingFeeRecipient), 0, 0); + + vm.prank(address(bidder)); + vm.expectRevert("AUCTION_NOT_STARTED"); + auctions.createBid(address(token), 0); + } + + function testRevert_CannotBidOnAuctionNotActive() public { + vm.expectRevert("AUCTION_DOES_NOT_EXIST"); + auctions.createBid{value: 1 ether}(address(token), 0); + } + + function testRevert_BidMustMeetReservePrice() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.prank(address(bidder)); + vm.expectRevert("RESERVE_PRICE_NOT_MET"); + auctions.createBid{value: 0.5 ether}(address(token), 0); + } + + function testRevert_BidMustBeDefaultPercentGreaterThanPrevious() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.warp(1 hours); + + vm.prank(address(bidder)); + auctions.createBid{value: 1 ether}(address(token), 0); + + vm.warp(1 hours + 1 minutes); + + vm.prank(address(otherBidder)); + vm.expectRevert("MINIMUM_BID_NOT_MET"); + auctions.createBid{value: 1.01 ether}(address(token), 0); + + vm.prank(address(otherBidder)); + auctions.createBid{value: 1.10 ether}(address(token), 0); + } + + function testRevert_BidMustBeProvidedPercentGreaterThanPrevious() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 4); + + vm.warp(1 hours); + + vm.prank(address(bidder)); + auctions.createBid{value: 1 ether}(address(token), 0); + + vm.warp(1 hours + 1 minutes); + + vm.prank(address(otherBidder)); + vm.expectRevert("MINIMUM_BID_NOT_MET"); + auctions.createBid{value: 1.03 ether}(address(token), 0); + + vm.prank(address(otherBidder)); + auctions.createBid{value: 1.04 ether}(address(token), 0); + } + + /// /// + /// SETTLE AUCTION /// + /// /// + + function test_SettleAuction() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.warp(1 hours); + + vm.prank(address(bidder)); + auctions.createBid{value: 1 ether}(address(token), 0); + + vm.warp(10 hours); + + vm.prank(address(otherBidder)); + auctions.createBid{value: 5 ether}(address(token), 0); + + vm.warp(1 days + 1 hours); + auctions.settleAuction(address(token), 0); + + require(token.ownerOf(0) == address(otherBidder)); + } + + function testRevert_AuctionNotStarted() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.expectRevert("AUCTION_NOT_STARTED"); + auctions.settleAuction(address(token), 0); + } + + function testRevert_AuctionNotOver() public { + vm.prank(address(seller)); + auctions.createAuction(address(token), 0, 1 days, 1 ether, address(sellerFundsRecipient), 0, 1000, address(listingFeeRecipient), 0, 0); + + vm.warp(1 hours); + + vm.prank(address(bidder)); + auctions.createBid{value: 1 ether}(address(token), 0); + + vm.warp(10 hours); + + vm.expectRevert("AUCTION_NOT_OVER"); + auctions.settleAuction(address(token), 0); + } +}