From b7df81487453d7590583d004a9c0a822df00aa03 Mon Sep 17 00:00:00 2001 From: Michael Yu Date: Fri, 28 Oct 2022 20:42:28 -0700 Subject: [PATCH] Make timestamp expiry configurable (#45) --- contracts/BucketAuction.sol | 54 ++--- contracts/DutchAuction.sol | 4 +- contracts/ERC721M.sol | 58 +++-- contracts/ERC721MCallback.sol | 6 +- contracts/IBucketAuction.sol | 1 - contracts/IERC721M.sol | 5 + hardhat.config.ts | 28 +-- package-lock.json | 4 +- package.json | 2 +- scripts/common/constants.ts | 6 - scripts/deploy.ts | 14 +- scripts/deployBA.ts | 55 ----- scripts/mint.ts | 11 +- scripts/ownerMint.ts | 3 +- scripts/setActiveStage.ts | 24 ++ scripts/setBaseURI.ts | 3 +- scripts/setCrossmintAddress.ts | 3 +- scripts/setGlobalWalletLimit.ts | 3 +- scripts/setMaxMintableSupply.ts | 7 +- scripts/setMintable.ts | 3 +- scripts/setStages.ts | 3 +- scripts/transferOwnership.ts | 3 +- test/BucketAuction.test.ts | 309 ++++++------------------- test/ERC721MCallback.test.ts | 55 +---- test/erc721m.test.ts | 387 +++++++++----------------------- 25 files changed, 300 insertions(+), 751 deletions(-) delete mode 100644 scripts/common/constants.ts delete mode 100644 scripts/deployBA.ts create mode 100644 scripts/setActiveStage.ts diff --git a/contracts/BucketAuction.sol b/contracts/BucketAuction.sol index 415fa4c..854282b 100644 --- a/contracts/BucketAuction.sol +++ b/contracts/BucketAuction.sol @@ -12,11 +12,9 @@ import "./ERC721M.sol"; contract BucketAuction is IBucketAuction, ERC721M { bool private _claimable; - bool private _firstTokenSent; - uint64 private _startTimeUnixSeconds; - uint64 private _endTimeUnixSeconds; uint256 private _minimumContributionInWei; uint256 private _price; + bool private _auctionActive; mapping(address => User) private _userData; constructor( @@ -26,10 +24,7 @@ contract BucketAuction is IBucketAuction, ERC721M { uint256 maxMintableSupply, uint256 globalWalletLimit, address cosigner, - uint256 minimumContributionInWei, - uint64 startTimeUnixSeconds, - uint64 endTimeUnixSeconds - + uint256 minimumContributionInWei ) ERC721M( collectionName, @@ -37,14 +32,13 @@ contract BucketAuction is IBucketAuction, ERC721M { tokenURISuffix, maxMintableSupply, globalWalletLimit, - cosigner + cosigner, + /* timestampExpirySeconds= */ + 300 ) { _claimable = false; _minimumContributionInWei = minimumContributionInWei; - _startTimeUnixSeconds = startTimeUnixSeconds; - _endTimeUnixSeconds = endTimeUnixSeconds ; - _firstTokenSent = false; } modifier isClaimable() { @@ -53,12 +47,12 @@ contract BucketAuction is IBucketAuction, ERC721M { } modifier isAuctionActive() { - if (_startTimeUnixSeconds > block.timestamp || _endTimeUnixSeconds <= block.timestamp) revert BucketAuctionNotActive(); + if (!_auctionActive) revert BucketAuctionNotActive(); _; } modifier isAuctionInactive() { - if (_startTimeUnixSeconds <= block.timestamp && block.timestamp < _endTimeUnixSeconds) revert BucketAuctionActive(); + if (_auctionActive) revert BucketAuctionActive(); _; } @@ -70,16 +64,8 @@ contract BucketAuction is IBucketAuction, ERC721M { return _price; } - function getStartTimeUnixSecods() external view returns (uint64) { - return _startTimeUnixSeconds; - } - - function getEndTimeUnixSecods() external view returns (uint64) { - return _endTimeUnixSeconds; - } - function getAuctionActive() external view returns (bool) { - return _startTimeUnixSeconds <= block.timestamp && block.timestamp < _endTimeUnixSeconds; + return _auctionActive; } function getUserData(address user) external view returns (User memory) { @@ -96,17 +82,13 @@ contract BucketAuction is IBucketAuction, ERC721M { } /** - * @notice set the start and end times in unix seconds for the bucket auction. + * @notice begin the auction. * @dev cannot be reactivated after price has been set. - * @param startTime set to unix timestamp for the auction start time. - * @param endTime set to unix timestamp for the auction end time. + * @param b set 'true' to start auction, set 'false' to stop auction. */ - function setStartAndEndTimeUnixSeconds(uint64 startTime, uint64 endTime) external onlyOwner { + function setAuctionActive(bool b) external onlyOwner cannotMint { if (_price != 0) revert PriceHasBeenSet(); - if (endTime <= startTime) revert InvalidStartAndEndTimestamp(); - - _startTimeUnixSeconds = startTime; - _endTimeUnixSeconds = endTime; + _auctionActive = b; } /** @@ -114,12 +96,7 @@ contract BucketAuction is IBucketAuction, ERC721M { * multiple times will increase your bid amount. All bids placed are final * and cannot be reversed. */ - function bid() - external - payable - isAuctionActive - nonReentrant - { + function bid() external payable isAuctionActive nonReentrant cannotMint { User storage bidder = _userData[msg.sender]; // get user's current bid total uint256 contribution_ = bidder.contribution; // bidder.contribution is uint216 unchecked { @@ -152,11 +129,11 @@ contract BucketAuction is IBucketAuction, ERC721M { */ function setPrice(uint256 priceInWei) external + isAuctionInactive onlyOwner + cannotMint { if (_claimable) revert CannotSetPriceIfClaimable(); - if (block.timestamp <= _endTimeUnixSeconds) revert BucketAuctionActive(); - if (_firstTokenSent) revert CannotSetPriceIfFirstTokenSent(); _price = priceInWei; emit SetPrice(priceInWei); @@ -172,7 +149,6 @@ contract BucketAuction is IBucketAuction, ERC721M { hasSupply(numberOfTokens) { _safeMint(to, numberOfTokens); - if (!_firstTokenSent && numberOfTokens > 0) _firstTokenSent = true; } /** diff --git a/contracts/DutchAuction.sol b/contracts/DutchAuction.sol index 50ca0f8..515a155 100644 --- a/contracts/DutchAuction.sol +++ b/contracts/DutchAuction.sol @@ -33,7 +33,9 @@ contract DutchAuction is IDutchAuction, ERC721M { tokenURISuffix, maxMintableSupply, globalWalletLimit, - cosigner + cosigner, + /* timestampExpirySeconds= */ + 300 ) { _refundable = refundable; diff --git a/contracts/ERC721M.sol b/contracts/ERC721M.sol index 9d538f2..3572208 100644 --- a/contracts/ERC721M.sol +++ b/contracts/ERC721M.sol @@ -12,21 +12,20 @@ import "./IERC721M.sol"; contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard { using ECDSA for bytes32; - uint64 public constant MIN_STAGE_INTERVAL_SECONDS = 60; - uint64 public constant CROSSMINT_TIMESTAMP_EXPIRY_SECONDS = 300; - bool private _mintable; - string private _currentBaseURI; - uint256 private _maxMintableSupply; - uint256 private _globalWalletLimit; - string private _tokenURISuffix; bool private _baseURIPermanent; + // @notice Specify how long a signature from cosigner is valid for recommend 300 seconds + uint64 private _timestampExpirySeconds; address private _cosigner; address private _crossmintAddress; + uint256 private _activeStage; + uint256 private _maxMintableSupply; + uint256 private _globalWalletLimit; + string private _currentBaseURI; + string private _tokenURISuffix; MintStageInfo[] private _mintStages; - // Need this because struct cannot have nested mapping mapping(uint256 => mapping(address => uint32)) private _stageMintedCountsPerWallet; mapping(uint256 => uint256) private _stageMintedCounts; @@ -37,7 +36,8 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard { string memory tokenURISuffix, uint256 maxMintableSupply, uint256 globalWalletLimit, - address cosigner + address cosigner, + uint64 timestampExpirySeconds ) ERC721A(collectionName, collectionSymbol) { if (globalWalletLimit > maxMintableSupply) revert GlobalWalletLimitOverflow(); @@ -47,6 +47,7 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard { _globalWalletLimit = globalWalletLimit; _tokenURISuffix = tokenURISuffix; _cosigner = cosigner; // ethers.constants.AddressZero for no cosigning + _timestampExpirySeconds = timestampExpirySeconds; } modifier canMint() { @@ -77,6 +78,11 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard { emit SetCosigner(cosigner); } + function setTimestampExpirySeconds(uint64 expiry) external onlyOwner { + _timestampExpirySeconds = expiry; + emit SetTimestampExpirySeconds(expiry); + } + function getCrossmintAddress() external view override returns (address) { return _crossmintAddress; } @@ -92,12 +98,12 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard { _mintStages.pop(); } + uint64 timestampExpirySeconds = getTimestampExpirySeconds(); for (uint256 i = 0; i < newStages.length; i++) { if (i >= 1) { if ( newStages[i].startTimeUnixSeconds < - newStages[i - 1].endTimeUnixSeconds + - MIN_STAGE_INTERVAL_SECONDS + newStages[i - 1].endTimeUnixSeconds + timestampExpirySeconds ) { revert InsufficientStageTimeGap(); } @@ -170,6 +176,16 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard { emit SetGlobalWalletLimit(globalWalletLimit); } + function getActiveStage() external view override returns (uint256) { + return _activeStage; + } + + function setActiveStage(uint256 activeStage) external onlyOwner { + if (activeStage >= _mintStages.length) revert InvalidStage(); + _activeStage = activeStage; + emit SetActiveStage(activeStage); + } + function totalMintedByAddress(address a) external view @@ -211,7 +227,7 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard { if ( startTimeUnixSeconds < _mintStages[index - 1].endTimeUnixSeconds + - MIN_STAGE_INTERVAL_SECONDS + getTimestampExpirySeconds() ) { revert InsufficientStageTimeGap(); } @@ -269,17 +285,17 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard { uint64 timestamp, bytes calldata signature ) internal canMint hasSupply(qty) { - uint64 stageTimestamp = uint64(block.timestamp); + uint256 activeStage = _activeStage; + + if (activeStage >= _mintStages.length) revert InvalidStage(); MintStageInfo memory stage; if (_cosigner != address(0)) { assertValidCosign(msg.sender, qty, timestamp, signature); _assertValidTimestamp(timestamp); - stageTimestamp = timestamp; + activeStage = getActiveStageFromTimestamp(timestamp); } - uint256 activeStage = getActiveStageFromTimestamp(stageTimestamp); - stage = _mintStages[activeStage]; // Check value @@ -432,11 +448,13 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard { revert InvalidStage(); } + function getTimestampExpirySeconds() public view override returns (uint64) { + return _timestampExpirySeconds; + } + function _assertValidTimestamp(uint64 timestamp) internal view { - uint64 threshold = msg.sender == _crossmintAddress - ? CROSSMINT_TIMESTAMP_EXPIRY_SECONDS - : MIN_STAGE_INTERVAL_SECONDS; - if (timestamp < block.timestamp - threshold) revert TimestampExpired(); + if (timestamp < block.timestamp - getTimestampExpirySeconds()) + revert TimestampExpired(); } function _assertValidStartAndEndTimestamp(uint64 start, uint64 end) diff --git a/contracts/ERC721MCallback.sol b/contracts/ERC721MCallback.sol index 638a5cc..c151f13 100644 --- a/contracts/ERC721MCallback.sol +++ b/contracts/ERC721MCallback.sol @@ -14,7 +14,8 @@ contract ERC721MCallback is ERC721M, IERC721MCallback { string memory tokenURISuffix, uint256 maxMintableSupply, uint256 globalWalletLimit, - address cosigner + address cosigner, + uint64 timestampExpirySeconds ) ERC721M( collectionName, @@ -22,7 +23,8 @@ contract ERC721MCallback is ERC721M, IERC721MCallback { tokenURISuffix, maxMintableSupply, globalWalletLimit, - cosigner + cosigner, + timestampExpirySeconds ) {} diff --git a/contracts/IBucketAuction.sol b/contracts/IBucketAuction.sol index 5da7c63..f1029f5 100644 --- a/contracts/IBucketAuction.sol +++ b/contracts/IBucketAuction.sol @@ -7,7 +7,6 @@ interface IBucketAuction { error BucketAuctionNotActive(); error CannotSendMoreThanUserPurchased(); error CannotSetPriceIfClaimable(); - error CannotSetPriceIfFirstTokenSent(); error LowerThanMinBidAmount(); error NotClaimable(); error PriceHasBeenSet(); diff --git a/contracts/IERC721M.sol b/contracts/IERC721M.sol index 7e8c553..4a83c61 100644 --- a/contracts/IERC721M.sol +++ b/contracts/IERC721M.sol @@ -52,6 +52,7 @@ interface IERC721M is IERC721AQueryable { event SetGlobalWalletLimit(uint256 globalWalletLimit); event SetActiveStage(uint256 activeStage); event SetBaseURI(string baseURI); + event SetTimestampExpirySeconds(uint64 expiry); event PermanentBaseURI(string baseURI); event Withdraw(uint256 value); @@ -80,6 +81,10 @@ interface IERC721M is IERC721AQueryable { uint256 ); + function getActiveStage() external view returns (uint256); + + function getTimestampExpirySeconds() external view returns (uint64); + function getActiveStageFromTimestamp(uint64 timestamp) external view diff --git a/hardhat.config.ts b/hardhat.config.ts index 8addb87..678e1e0 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -8,11 +8,11 @@ import 'hardhat-watcher'; import { HardhatUserConfig, task, types } from 'hardhat/config'; import 'solidity-coverage'; +import { setActiveStage } from './scripts/setActiveStage'; import { setCrossmintAddress } from './scripts/setCrossmintAddress'; import { setStages } from './scripts/setStages'; import { setMintable } from './scripts/setMintable'; import { deploy } from './scripts/deploy'; -import { deployBA } from './scripts/deployBA'; import { setBaseURI } from './scripts/setBaseURI'; import { mint } from './scripts/mint'; import { ownerMint } from './scripts/ownerMint'; @@ -66,6 +66,11 @@ task('setStages', 'Set stages for ERC721M') .addParam('stages', 'stages json file') .setAction(setStages); +task('setActiveStage', 'Set active stage for ERC721M') + .addParam('contract', 'contract address') + .addParam('stage', 'stage index to set to active') + .setAction(setActiveStage); + task('setMintable', 'Set mintable state for ERC721M') .addParam('contract', 'contract address') .addParam('mintable', 'mintable state', 'true', types.boolean) @@ -97,7 +102,6 @@ task('setCrossmintAddress', 'Set crossmint address') task('mint', 'Mint token(s)') .addParam('contract', 'contract address') .addParam('qty', 'quantity to mint', '1') - .addParam('minttime', 'time of the mint') .setAction(mint); task('ownerMint', 'Mint token(s) as owner') @@ -115,24 +119,4 @@ task('setMaxMintableSupply', 'set max mintable supply') .addParam('contract', 'contract address') .addParam('supply', 'new supply') .setAction(setMaxMintableSupply); - -task('deployBA', 'Deploy BucketAuction') - .addParam('name', 'name') - .addParam('symbol', 'symbol') - .addParam('maxsupply', 'max supply') - .addParam('tokenurisuffix', 'token uri suffix', '.json') - .addParam('globalwalletlimit', 'global wallet limit') - .addOptionalParam( - 'cosigner', - 'cosigner address (0x00...000 if not using cosign)', - '0x0000000000000000000000000000000000000000', - ) - .addParam( - 'mincontributioninwei', - 'The minimum contribution in wei required only for the AcutionBucket', - ) - .addParam('auctionstarttime', 'The start time of the bucket auction') - .addParam('auctionendtime', 'The end time of the bucket auction') - .setAction(deployBA); - export default config; diff --git a/package-lock.json b/package-lock.json index 53b358c..a6eb7f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@magiceden-oss/erc721m", - "version": "0.0.5", + "version": "0.0.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@magiceden-oss/erc721m", - "version": "0.0.5", + "version": "0.0.8", "dependencies": { "@openzeppelin/contracts": "^4.7.3", "erc721a": "^4.2.3" diff --git a/package.json b/package.json index 07b6359..254c544 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@magiceden-oss/erc721m", - "version": "0.0.7", + "version": "0.0.8", "description": "erc721m contract for Solidity", "files": [ "/contracts/**/*.sol", diff --git a/scripts/common/constants.ts b/scripts/common/constants.ts deleted file mode 100644 index 81e3041..0000000 --- a/scripts/common/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This module wraps up the constants which can be used by any script - -export const ContractDetails = { - ERC721M: { name: 'ERC721M' }, // The contract of direct sales - BucketAuction: { name: 'BucketAuction' }, // The contract of bucket auctions -}; diff --git a/scripts/deploy.ts b/scripts/deploy.ts index a9f5fa6..880e9b4 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -4,7 +4,6 @@ // When running the script with `npx hardhat run