Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

extensions: dutch auction #105

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions contracts/extensions/DutchAuction.sol
Original file line number Diff line number Diff line change
@@ -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 createExtension(
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);
}
}
57 changes: 57 additions & 0 deletions contracts/extensions/DutchAuctionExtension.sol
Original file line number Diff line number Diff line change
@@ -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 "./base/NFTExtension.sol";
import "./base/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);
}

}
161 changes: 161 additions & 0 deletions contracts/extensions/DutchAuctionExtensionSingleton.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// 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 startPrice;
mapping(IERC721Community => uint256) public endPrice;

mapping(IERC721Community => uint256) public startTimestamp;
mapping(IERC721Community => uint256) public endTimestamp;

mapping(IERC721Community => uint256) public maxPerAddress;

mapping(IERC721Community => mapping(address => uint256))
public claimedByAddress;

constructor() {}

function configureSale(
IERC721Community collection,
uint256 _startPrice,
uint256 _endPrice,
uint256 _startTimestamp,
uint256 _endTimestamp,
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"
);

require(
_startPrice > _endPrice,
"Start price must be greater than end price"
);

startTimestamp[collection] = _startTimestamp;
endTimestamp[collection] = _endTimestamp;

startPrice[collection] = _startPrice;
endPrice[collection] = _endPrice;

maxPerAddress[collection] = _maxPerAddress;

return address(this);
}

function updatePrice(
IERC721Community collection,
uint256 _price
) public onlyNFTOwner(collection) {
startPrice[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;
}

function stopSale(
IERC721Community collection
) public onlyNFTOwner(collection) {
startTimestamp[collection] = __SALE_NEVER_STARTS;
endTimestamp[collection] = __SALE_NEVER_STARTS;
}

// 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(
block.timestamp >= startTimestamp[collection],
"Sale not started"
);
require(block.timestamp <= endTimestamp[collection], "Sale ended");

require(
nTokens + claimedByAddress[collection][msg.sender] <=
maxPerAddress[collection],
"Cannot claim more than maxPerAddress"
);

require(
msg.value >= nTokens * price(collection),
"Not enough ETH to mint"
);

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 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;
}
}
21 changes: 21 additions & 0 deletions contracts/standards/ERC721CommunityBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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), "Extension implementation 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");
Expand Down
Loading