From 7a7a31562d188e64dcece018bb2538479b06fdb5 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Wed, 6 Apr 2022 08:59:56 +0800 Subject: [PATCH 1/5] extensions: draft dutch auction --- .../extensions/DutchAuctionExtension.sol | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 contracts/extensions/DutchAuctionExtension.sol diff --git a/contracts/extensions/DutchAuctionExtension.sol b/contracts/extensions/DutchAuctionExtension.sol new file mode 100644 index 00000000..80896c9d --- /dev/null +++ b/contracts/extensions/DutchAuctionExtension.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/security/Pausable.sol"; +import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; + +import "./NFTExtension.sol"; +import "./SaleControl.sol"; + +contract DutchAuctionExtension is NFTExtension, Ownable, SaleControl { + + uint256 public startingPrice; + uint256 public endTimestamp; + uint256 public maxPerAddress; + + mapping (address => uint256) public claimedByAddress; + + constructor(address _nft, uint256 _price, uint256 _endTimestamp) NFTExtension(_nft) SaleControl() { + stopSale(); + + startingPrice = _price; + endTimestamp = _endTimestamp; + } + + function updatePrice(uint256 _price) public onlyOwner { + startingPrice = _price; + } + + function updateMaxPerAddress(uint256 _maxPerAddress) public onlyOwner { + maxPerAddress = _maxPerAddress; + } + + function updateEndTimestamp(uint256 _timestamp) public onlyOwner { + endTimestamp = _timestamp; + } + + function mint(uint256 nTokens) external whenSaleStarted payable { + + require(nTokens <= maxPerAddress, "Cannot claim more per transaction"); + + require(msg.value >= nTokens * price(), "Not enough ETH to mint"); + + nft.mintExternal{ value: msg.value }(nTokens, msg.sender, bytes32(0x0)); + + // TODO: refund unused? + + } + + + function price() public view returns (uint256 currentPrice) { + // start at startTimestamp at startingPrice, gradually falls at reducePriceSpeed per second + // currentPrice = startingPrice - (block.timestamp - startTimestamp) * + currentPrice = startingPrice - (block.timestamp - startTimestamp) * (endTimestamp - startTimestamp); + } + +} From 9eccacfe0465df1159741dcbe918e99fbc7c7e73 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Mon, 8 May 2023 03:42:32 -0400 Subject: [PATCH 2/5] research extension deploy costs --- contracts/extensions/DutchAuction.sol | 118 +++++++++ .../extensions/DutchAuctionExtension.sol | 4 +- .../DutchAuctionExtensionSingleton.sol | 138 ++++++++++ test/foundry/DutchAuction.t.sol | 236 ++++++++++++++++++ 4 files changed, 494 insertions(+), 2 deletions(-) create mode 100644 contracts/extensions/DutchAuction.sol create mode 100644 contracts/extensions/DutchAuctionExtensionSingleton.sol create mode 100644 test/foundry/DutchAuction.t.sol diff --git a/contracts/extensions/DutchAuction.sol b/contracts/extensions/DutchAuction.sol new file mode 100644 index 00000000..edcbb418 --- /dev/null +++ b/contracts/extensions/DutchAuction.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/proxy/Clones.sol"; + +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; + +import "./allowlist-factory/base/NFTExtensionUpgradeable.sol"; +import "./allowlist-factory/base/SaleControlUpgradeable.sol"; + +import "./base/NFTExtension.sol"; +import "./base/SaleControl.sol"; + +contract DutchAuctionFactory { + DutchAuction public implementation; + + constructor() { + implementation = new DutchAuction(); + } + + function createAuction( + address _nft, + uint256 _price, + uint256 _maxPerAddress, + uint256 _startTimestamp, + uint256 _endTimestamp + ) public returns (DutchAuction) { + address clone = Clones.clone(address(implementation)); + + DutchAuction(clone).initialize( + _nft, + _price, + _maxPerAddress, + _startTimestamp, + _endTimestamp, + msg.sender + ); + + // if (startSale) { + // DutchAuction(clone).startSale(); + // } + + // DutchAuction(clone).transferOwnership(msg.sender); + + return DutchAuction(clone); + } +} + +contract DutchAuction is + NFTExtensionUpgradeable, + OwnableUpgradeable + // SaleControlUpgradeable +{ + uint256 public startingPrice; + uint256 public startTimestamp; + uint256 public endTimestamp; + uint256 public maxPerAddress; + + mapping(address => uint256) public claimedByAddress; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() initializer {} + + function initialize( + address _nft, + uint256 _price, + uint256 _maxPerAddress, + uint256 _startTimestamp, + uint256 _endTimestamp, + address owner + ) public initializer { + NFTExtensionUpgradeable.initialize(_nft); + // SaleControlUpgradeable.initialize(); + __Ownable_init(); + transferOwnership(owner); + + startingPrice = _price; + maxPerAddress = _maxPerAddress; + startTimestamp = _startTimestamp; + endTimestamp = _endTimestamp; + + } + + function updatePrice(uint256 _price) public onlyOwner { + startingPrice = _price; + } + + function updateMaxPerAddress(uint256 _maxPerAddress) public onlyOwner { + maxPerAddress = _maxPerAddress; + } + + function updateEndTimestamp(uint256 _timestamp) public onlyOwner { + endTimestamp = _timestamp; + } + + function mint(uint256 nTokens) external payable { + require(block.timestamp <= endTimestamp, "Sale has ended"); + require(block.timestamp >= startTimestamp, "Sale has not started"); + + require(nTokens <= maxPerAddress, "Cannot claim more per transaction"); + + require(msg.value >= nTokens * price(), "Not enough ETH to mint"); + + nft.mintExternal{value: msg.value}(nTokens, msg.sender, bytes32(0x0)); + + // TODO: refund unused? + } + + function price() public view returns (uint256 currentPrice) { + // start at startTimestamp at startingPrice, gradually falls at reducePriceSpeed per second + // currentPrice = startingPrice - (block.timestamp - startTimestamp) * + currentPrice = + startingPrice - + (block.timestamp - startTimestamp) * + (endTimestamp - startTimestamp); + } +} diff --git a/contracts/extensions/DutchAuctionExtension.sol b/contracts/extensions/DutchAuctionExtension.sol index 80896c9d..fe1dbf45 100644 --- a/contracts/extensions/DutchAuctionExtension.sol +++ b/contracts/extensions/DutchAuctionExtension.sol @@ -5,8 +5,8 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/security/Pausable.sol"; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; -import "./NFTExtension.sol"; -import "./SaleControl.sol"; +import "./base/NFTExtension.sol"; +import "./base/SaleControl.sol"; contract DutchAuctionExtension is NFTExtension, Ownable, SaleControl { diff --git a/contracts/extensions/DutchAuctionExtensionSingleton.sol b/contracts/extensions/DutchAuctionExtensionSingleton.sol new file mode 100644 index 00000000..7489f7d1 --- /dev/null +++ b/contracts/extensions/DutchAuctionExtensionSingleton.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/security/Pausable.sol"; +import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; + +import "./base/NFTExtension.sol"; +// import "./base/SaleControl.sol"; + +uint256 constant __SALE_NEVER_STARTS = 2**256 - 1; + + +contract DutchAuctionExtensionSingleton { + modifier onlyNFTOwner(IERC721Community nft) { + require( + Ownable(address(nft)).owner() == msg.sender, + "MintBatchExtension: Not NFT owner" + ); + _; + } + + mapping(IERC721Community => uint256) public startingPrice; + mapping(IERC721Community => uint256) public startTimestamp; + mapping(IERC721Community => uint256) public endTimestamp; + mapping(IERC721Community => uint256) public maxPerAddress; + + mapping(IERC721Community => bool) public saleStarted; + + mapping(address => mapping(IERC721Community => uint256)) + public claimedByAddress; + + constructor() {} + + function configureSale( + IERC721Community collection, + uint256 _price, + uint256 _maxPerAddress, + uint256 _endTimestamp, + bool _saleStarted + ) public onlyNFTOwner(collection) { + + startTimestamp[collection] = __SALE_NEVER_STARTS; + endTimestamp[collection] = _endTimestamp; + startingPrice[collection] = _price; + maxPerAddress[collection] = _maxPerAddress; + + // endTimestamp[collection] = __SALE_NEVER_STARTS; + + saleStarted[collection] = _saleStarted; + + if (_saleStarted) { + startTimestamp[collection] = block.timestamp; + } + } + + function updatePrice( + IERC721Community collection, + uint256 _price + ) public onlyNFTOwner(collection) { + startingPrice[collection] = _price; + } + + function updateMaxPerAddress( + IERC721Community collection, + uint256 _maxPerAddress + ) public onlyNFTOwner(collection) { + maxPerAddress[collection] = _maxPerAddress; + } + + function updateEndTimestamp( + IERC721Community collection, + uint256 _timestamp + ) public onlyNFTOwner(collection) { + endTimestamp[collection] = _timestamp; + } + + function startSale( + IERC721Community collection + ) public onlyNFTOwner(collection) { + startTimestamp[collection] = block.timestamp; + endTimestamp[collection] = __SALE_NEVER_STARTS; + + saleStarted[collection] = true; + } + + function stopSale( + IERC721Community collection + ) public onlyNFTOwner(collection) { + startTimestamp[collection] = __SALE_NEVER_STARTS; + endTimestamp[collection] = __SALE_NEVER_STARTS; + + saleStarted[collection] = false; + } + + // function saleStarted(IERC721Community collection) public view returns (bool) { + // return block.timestamp >= startTimestamp[collection] && block.timestamp <= endTimestamp[collection]; + // } + + function mint( + IERC721Community collection, + uint256 nTokens + ) external payable { + require(saleStarted[collection], "Sale not started"); + + // require(block.timestamp >= startTimestamp[collection], "Sale not started"); + // require(block.timestamp <= endTimestamp[collection], "Sale ended"); + + require( + nTokens <= maxPerAddress[collection], + "Cannot claim more per transaction" + ); + + require( + msg.value >= nTokens * price(collection), + "Not enough ETH to mint" + ); + + IERC721Community(collection).mintExternal{value: msg.value}( + nTokens, + msg.sender, + bytes32(0x0) + ); + + // TODO: refund unused? + } + + function price( + IERC721Community collection + ) public view returns (uint256 currentPrice) { + // start at startTimestamp at startingPrice, gradually falls at reducePriceSpeed per second + // currentPrice = startingPrice - (block.timestamp - startTimestamp) * + currentPrice = + startingPrice[collection] - + (block.timestamp - startTimestamp[collection]) * + (endTimestamp[collection] - startTimestamp[collection]); + } +} diff --git a/test/foundry/DutchAuction.t.sol b/test/foundry/DutchAuction.t.sol new file mode 100644 index 00000000..c0992830 --- /dev/null +++ b/test/foundry/DutchAuction.t.sol @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import "contracts/extensions/DutchAuctionExtension.sol"; +import "contracts/extensions/DutchAuctionExtensionSingleton.sol"; +import "contracts/extensions/DutchAuction.sol"; + +import "contracts/standards/ERC721CommunityBase.sol"; + +contract DutchAuctionTest is Test { + DutchAuctionExtensionSingleton dutchAuctionExtensionSingleton; + DutchAuctionExtension dutchAuctionExtension; + + DutchAuction dutchAuction; + + DutchAuctionFactory dutchAuctionFactory; + + IERC721Community nft; + + address admin; + + uint amount = 1; + + function setUp() public { + admin = makeAddr("Admin"); + vm.deal(admin, 100 ether); + vm.startPrank(admin); + + nft = new ERC721CommunityBase( + "Test", + "NFT", + 10000, + 15, // reserved + false, + "ipfs://factory-test/", + MintConfig(0.03 ether, 20, 20, 500, msg.sender, false, false, false) + ); + + dutchAuctionFactory = new DutchAuctionFactory(); + + dutchAuctionExtensionSingleton = new DutchAuctionExtensionSingleton(); + } + + function OFFtestSingleton() public { + amount = 1; // bound(amount, 1, 100); + + nft.addExtension(address(dutchAuctionExtensionSingleton)); + + dutchAuctionExtensionSingleton.startSale(nft); + dutchAuctionExtensionSingleton.updatePrice(nft, 100); + dutchAuctionExtensionSingleton.updateMaxPerAddress(nft, 200); + dutchAuctionExtensionSingleton.updateEndTimestamp( + nft, + block.timestamp + 1000 + ); + + assertEq(dutchAuctionExtensionSingleton.startingPrice(nft), 100); + assertEq(dutchAuctionExtensionSingleton.maxPerAddress(nft), 200); + assertEq( + dutchAuctionExtensionSingleton.endTimestamp(nft), + block.timestamp + 1000 + ); + assertTrue(dutchAuctionExtensionSingleton.saleStarted(nft)); + + // test mint gas cost + uint256 gasCost = gasleft(); + + dutchAuctionExtensionSingleton.mint{value: 100 * amount}(nft, amount); + + gasCost = gasCost - gasleft(); + + console.log("Gas cost per mint singleton: ", gasCost / amount); + } + + function OFFtestExtension() public { + amount = 1; // bound(amount, 1, 100); + + dutchAuctionExtension = new DutchAuctionExtension( + (address(nft)), + 100, + 10_000_000_000 + ); + + nft.addExtension(address(dutchAuctionExtension)); + dutchAuctionExtension.startSale(); + + dutchAuctionExtension.updatePrice(100); + dutchAuctionExtension.updateMaxPerAddress(200); + dutchAuctionExtension.updateEndTimestamp(block.timestamp + 1000); + + assertEq(dutchAuctionExtension.startingPrice(), 100); + assertEq(dutchAuctionExtension.maxPerAddress(), 200); + assertEq(dutchAuctionExtension.endTimestamp(), block.timestamp + 1000); + assertTrue(dutchAuctionExtension.saleStarted()); + + // test mint gas cost + + uint256 gasCost = gasleft(); + + dutchAuctionExtension.mint{value: 100 * amount}(amount); + + gasCost = gasCost - gasleft(); + + console.log("Gas cost per mint extension: ", gasCost / amount); + } + + function OFFtestCrosscompare(uint amount) public { + amount = bound(amount, 5, 100); + + dutchAuctionExtensionSingleton = new DutchAuctionExtensionSingleton(); + + nft.addExtension(address(dutchAuctionExtensionSingleton)); + + dutchAuctionExtensionSingleton.startSale(nft); + dutchAuctionExtensionSingleton.updatePrice(nft, 100); + dutchAuctionExtensionSingleton.updateMaxPerAddress(nft, 200); + dutchAuctionExtensionSingleton.updateEndTimestamp( + nft, + block.timestamp + 1000 + ); + + assertEq(dutchAuctionExtensionSingleton.startingPrice(nft), 100); + assertEq(dutchAuctionExtensionSingleton.maxPerAddress(nft), 200); + assertEq( + dutchAuctionExtensionSingleton.endTimestamp(nft), + block.timestamp + 1000 + ); + assertTrue(dutchAuctionExtensionSingleton.saleStarted(nft)); + + // test mint gas cost + uint256 gasCost = gasleft(); + + dutchAuctionExtensionSingleton.mint{value: 100 * amount}(nft, amount); + + gasCost = gasCost - gasleft(); + + console.log("Gas cost per mint singleton: ", gasCost / amount); + + dutchAuctionExtension = new DutchAuctionExtension( + (address(nft)), + 100, + 10_000_000_000 + ); + + nft.addExtension(address(dutchAuctionExtension)); + dutchAuctionExtension.startSale(); + + dutchAuctionExtension.updatePrice(100); + dutchAuctionExtension.updateMaxPerAddress(200); + dutchAuctionExtension.updateEndTimestamp(block.timestamp + 1000); + + assertEq(dutchAuctionExtension.startingPrice(), 100); + assertEq(dutchAuctionExtension.maxPerAddress(), 200); + assertEq(dutchAuctionExtension.endTimestamp(), block.timestamp + 1000); + assertTrue(dutchAuctionExtension.saleStarted()); + + // test mint gas cost + + gasCost = gasleft(); + + dutchAuctionExtension.mint{value: 100 * amount}(amount); + + gasCost = gasCost - gasleft(); + + console.log("Gas cost per mint extension: ", gasCost / amount); + + assertTrue(false); + } + + function testDeployExtension() public { + // we already have nft configured, we need to deploy the extension and add it to the nft + + uint gasCost = gasleft(); + + dutchAuction = dutchAuctionFactory.createAuction( + address(nft), + 100, + 10, + block.timestamp, + block.timestamp + 1000 + ); + + nft.addExtension(address(dutchAuction)); + + gasCost = gasCost - gasleft(); + + console.log("Gas cost to deploy extension: ", gasCost); + + // test mint + + gasCost = gasleft(); + + dutchAuction.mint{value: 100 * amount}(amount); + + gasCost = gasCost - gasleft(); + + console.log("Gas cost to mint via extension: ", gasCost / amount); + } + + function testConfigSingleton() public { + + // we already have nft configured, we need to deploy the extension and add it to the nft + + uint gasCost = gasleft(); + + dutchAuctionExtensionSingleton.configureSale( + (nft), + 100, + 10, + 10_000_000_000, + true + ); + + nft.addExtension(address(dutchAuctionExtensionSingleton)); + + gasCost = gasCost - gasleft(); + + console.log("Gas cost to configure singleton: ", gasCost); + + // test mint + + gasCost = gasleft(); + + dutchAuctionExtensionSingleton.mint{value: 100 * amount}(nft, amount); + + gasCost = gasCost - gasleft(); + + console.log( + "Gas cost to mint via singleton: ", + gasCost / amount + ); + } +} From a5a9d747e5c5dcd12020edfa32125d360f5f4508 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 9 May 2023 19:43:09 -0400 Subject: [PATCH 3/5] test erc721 .deployExtension --- contracts/extensions/DutchAuction.sol | 2 +- contracts/standards/ERC721CommunityBase.sol | 21 +++++++++++ test/foundry/DutchAuction.t.sol | 40 ++++++++++++++------- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/contracts/extensions/DutchAuction.sol b/contracts/extensions/DutchAuction.sol index edcbb418..4cb68cb3 100644 --- a/contracts/extensions/DutchAuction.sol +++ b/contracts/extensions/DutchAuction.sol @@ -19,7 +19,7 @@ contract DutchAuctionFactory { implementation = new DutchAuction(); } - function createAuction( + function createExtension( address _nft, uint256 _price, uint256 _maxPerAddress, diff --git a/contracts/standards/ERC721CommunityBase.sol b/contracts/standards/ERC721CommunityBase.sol index 5d8e29a3..7bc44b50 100644 --- a/contracts/standards/ERC721CommunityBase.sol +++ b/contracts/standards/ERC721CommunityBase.sol @@ -11,6 +11,8 @@ import "erc721a/contracts/ERC721A.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/proxy/Clones.sol"; + import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; @@ -282,6 +284,25 @@ contract ERC721CommunityBase is return extensions.length; } + function deployExtension( + address implementation, + bytes memory initData + ) public onlyOwner returns (address extension) { + require(implementation != address(0), "Factory cannot be zero address"); + + extension = Clones.clone(implementation); + + // call extension.initialize encoded with selector and data + + (bool success, ) = extension.call(initData); + + require(success, "Extension initialization failed"); + + // TODO: transfer ownership? + + addExtension(extension); + } + // Extensions are allowed to mint function addExtension(address _extension) public onlyOwner { require(_extension != address(this), "Cannot add self as extension"); diff --git a/test/foundry/DutchAuction.t.sol b/test/foundry/DutchAuction.t.sol index c0992830..2483a63c 100644 --- a/test/foundry/DutchAuction.t.sol +++ b/test/foundry/DutchAuction.t.sol @@ -171,19 +171,37 @@ contract DutchAuctionTest is Test { } function testDeployExtension() public { + + DutchAuction implementation = new DutchAuction(); + // we already have nft configured, we need to deploy the extension and add it to the nft uint gasCost = gasleft(); - dutchAuction = dutchAuctionFactory.createAuction( - address(nft), - 100, - 10, - block.timestamp, - block.timestamp + 1000 + // dutchAuction = dutchAuctionFactory.createExtension( + // address(nft), + // 100, + // 10, + // block.timestamp, + // block.timestamp + 1000 + // ); + + // nft.addExtension(address(dutchAuction)); + + address ext = ERC721CommunityBase(payable(address(nft))).deployExtension( + address(implementation), + abi.encodeWithSelector( + DutchAuction.initialize.selector, + address(nft), + 100, + 10, + block.timestamp, + block.timestamp + 1000, + admin + ) ); - nft.addExtension(address(dutchAuction)); + // console.log("Deploy result: ", string(result)); gasCost = gasCost - gasleft(); @@ -193,7 +211,7 @@ contract DutchAuctionTest is Test { gasCost = gasleft(); - dutchAuction.mint{value: 100 * amount}(amount); + DutchAuction(ext).mint{value: 100 * amount}(amount); gasCost = gasCost - gasleft(); @@ -201,7 +219,6 @@ contract DutchAuctionTest is Test { } function testConfigSingleton() public { - // we already have nft configured, we need to deploy the extension and add it to the nft uint gasCost = gasleft(); @@ -228,9 +245,6 @@ contract DutchAuctionTest is Test { gasCost = gasCost - gasleft(); - console.log( - "Gas cost to mint via singleton: ", - gasCost / amount - ); + console.log("Gas cost to mint via singleton: ", gasCost / amount); } } From 8c55172a178f29c2e2130595334e9363c4103ff5 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 9 May 2023 20:00:31 -0400 Subject: [PATCH 4/5] rename error --- contracts/standards/ERC721CommunityBase.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/standards/ERC721CommunityBase.sol b/contracts/standards/ERC721CommunityBase.sol index 7bc44b50..9b89a533 100644 --- a/contracts/standards/ERC721CommunityBase.sol +++ b/contracts/standards/ERC721CommunityBase.sol @@ -288,7 +288,7 @@ contract ERC721CommunityBase is address implementation, bytes memory initData ) public onlyOwner returns (address extension) { - require(implementation != address(0), "Factory cannot be zero address"); + require(implementation != address(0), "Extension implementation cannot be zero address"); extension = Clones.clone(implementation); From b79a869df03d91889322c5e3c4d67e9bfa88f644 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Tue, 9 May 2023 20:00:43 -0400 Subject: [PATCH 5/5] dutch auction singleton edit --- .../DutchAuctionExtensionSingleton.sol | 95 ++++++++++++------- test/foundry/DutchAuction.t.sol | 13 +-- 2 files changed, 66 insertions(+), 42 deletions(-) diff --git a/contracts/extensions/DutchAuctionExtensionSingleton.sol b/contracts/extensions/DutchAuctionExtensionSingleton.sol index 7489f7d1..9cfdd03b 100644 --- a/contracts/extensions/DutchAuctionExtensionSingleton.sol +++ b/contracts/extensions/DutchAuctionExtensionSingleton.sol @@ -8,8 +8,7 @@ import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import "./base/NFTExtension.sol"; // import "./base/SaleControl.sol"; -uint256 constant __SALE_NEVER_STARTS = 2**256 - 1; - +uint256 constant __SALE_NEVER_STARTS = 2 ** 256 - 1; contract DutchAuctionExtensionSingleton { modifier onlyNFTOwner(IERC721Community nft) { @@ -20,45 +19,57 @@ contract DutchAuctionExtensionSingleton { _; } - mapping(IERC721Community => uint256) public startingPrice; + mapping(IERC721Community => uint256) public startPrice; + mapping(IERC721Community => uint256) public endPrice; + mapping(IERC721Community => uint256) public startTimestamp; mapping(IERC721Community => uint256) public endTimestamp; - mapping(IERC721Community => uint256) public maxPerAddress; - mapping(IERC721Community => bool) public saleStarted; + mapping(IERC721Community => uint256) public maxPerAddress; - mapping(address => mapping(IERC721Community => uint256)) + mapping(IERC721Community => mapping(address => uint256)) public claimedByAddress; constructor() {} function configureSale( IERC721Community collection, - uint256 _price, - uint256 _maxPerAddress, + uint256 _startPrice, + uint256 _endPrice, + uint256 _startTimestamp, uint256 _endTimestamp, - bool _saleStarted - ) public onlyNFTOwner(collection) { + uint256 _maxPerAddress + ) public onlyNFTOwner(collection) returns (address) { + require( + _startTimestamp >= block.timestamp, + "Start time must be in the future" + ); + require( + _endTimestamp > _startTimestamp, + "End time must be after start time" + ); - startTimestamp[collection] = __SALE_NEVER_STARTS; + require( + _startPrice > _endPrice, + "Start price must be greater than end price" + ); + + startTimestamp[collection] = _startTimestamp; endTimestamp[collection] = _endTimestamp; - startingPrice[collection] = _price; - maxPerAddress[collection] = _maxPerAddress; - // endTimestamp[collection] = __SALE_NEVER_STARTS; + startPrice[collection] = _startPrice; + endPrice[collection] = _endPrice; - saleStarted[collection] = _saleStarted; + maxPerAddress[collection] = _maxPerAddress; - if (_saleStarted) { - startTimestamp[collection] = block.timestamp; - } + return address(this); } function updatePrice( IERC721Community collection, uint256 _price ) public onlyNFTOwner(collection) { - startingPrice[collection] = _price; + startPrice[collection] = _price; } function updateMaxPerAddress( @@ -80,8 +91,6 @@ contract DutchAuctionExtensionSingleton { ) public onlyNFTOwner(collection) { startTimestamp[collection] = block.timestamp; endTimestamp[collection] = __SALE_NEVER_STARTS; - - saleStarted[collection] = true; } function stopSale( @@ -89,8 +98,6 @@ contract DutchAuctionExtensionSingleton { ) public onlyNFTOwner(collection) { startTimestamp[collection] = __SALE_NEVER_STARTS; endTimestamp[collection] = __SALE_NEVER_STARTS; - - saleStarted[collection] = false; } // function saleStarted(IERC721Community collection) public view returns (bool) { @@ -101,14 +108,16 @@ contract DutchAuctionExtensionSingleton { IERC721Community collection, uint256 nTokens ) external payable { - require(saleStarted[collection], "Sale not started"); - - // require(block.timestamp >= startTimestamp[collection], "Sale not started"); - // require(block.timestamp <= endTimestamp[collection], "Sale ended"); + require( + block.timestamp >= startTimestamp[collection], + "Sale not started" + ); + require(block.timestamp <= endTimestamp[collection], "Sale ended"); require( - nTokens <= maxPerAddress[collection], - "Cannot claim more per transaction" + nTokens + claimedByAddress[collection][msg.sender] <= + maxPerAddress[collection], + "Cannot claim more than maxPerAddress" ); require( @@ -116,7 +125,7 @@ contract DutchAuctionExtensionSingleton { "Not enough ETH to mint" ); - IERC721Community(collection).mintExternal{value: msg.value}( + collection.mintExternal{value: msg.value}( nTokens, msg.sender, bytes32(0x0) @@ -128,11 +137,25 @@ contract DutchAuctionExtensionSingleton { function price( IERC721Community collection ) public view returns (uint256 currentPrice) { - // start at startTimestamp at startingPrice, gradually falls at reducePriceSpeed per second - // currentPrice = startingPrice - (block.timestamp - startTimestamp) * - currentPrice = - startingPrice[collection] - - (block.timestamp - startTimestamp[collection]) * - (endTimestamp[collection] - startTimestamp[collection]); + // start at startTimestamp at startPrice, gradually falls until endTimestamp at endPrice + + if (block.timestamp <= startTimestamp[collection]) { + return startPrice[collection]; + } + + if (block.timestamp >= endTimestamp[collection]) { + return endPrice[collection]; + } + + uint256 timeDelta = endTimestamp[collection] - + startTimestamp[collection]; + + uint256 priceDelta = startPrice[collection] - endPrice[collection]; + + uint256 timeElapsed = block.timestamp - startTimestamp[collection]; + + uint256 priceChange = (timeElapsed * priceDelta) / timeDelta; + + return startPrice[collection] - priceChange; } } diff --git a/test/foundry/DutchAuction.t.sol b/test/foundry/DutchAuction.t.sol index 2483a63c..cbb70fc0 100644 --- a/test/foundry/DutchAuction.t.sol +++ b/test/foundry/DutchAuction.t.sol @@ -57,13 +57,13 @@ contract DutchAuctionTest is Test { block.timestamp + 1000 ); - assertEq(dutchAuctionExtensionSingleton.startingPrice(nft), 100); + assertEq(dutchAuctionExtensionSingleton.startPrice(nft), 100); assertEq(dutchAuctionExtensionSingleton.maxPerAddress(nft), 200); assertEq( dutchAuctionExtensionSingleton.endTimestamp(nft), block.timestamp + 1000 ); - assertTrue(dutchAuctionExtensionSingleton.saleStarted(nft)); + // assertTrue(dutchAuctionExtensionSingleton.saleStarted(nft)); // test mint gas cost uint256 gasCost = gasleft(); @@ -122,13 +122,13 @@ contract DutchAuctionTest is Test { block.timestamp + 1000 ); - assertEq(dutchAuctionExtensionSingleton.startingPrice(nft), 100); + assertEq(dutchAuctionExtensionSingleton.startPrice(nft), 100); assertEq(dutchAuctionExtensionSingleton.maxPerAddress(nft), 200); assertEq( dutchAuctionExtensionSingleton.endTimestamp(nft), block.timestamp + 1000 ); - assertTrue(dutchAuctionExtensionSingleton.saleStarted(nft)); + // assertTrue(dutchAuctionExtensionSingleton.saleStarted(nft)); // test mint gas cost uint256 gasCost = gasleft(); @@ -227,8 +227,9 @@ contract DutchAuctionTest is Test { (nft), 100, 10, - 10_000_000_000, - true + block.timestamp, + block.timestamp + 1000, + 5 // max per address ); nft.addExtension(address(dutchAuctionExtensionSingleton));