diff --git a/.gitmodules b/.gitmodules index db5ccba93..160854ceb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,9 +4,6 @@ [submodule "lib/ds-test"] path = lib/ds-test url = https://github.com/dapphub/ds-test -[submodule "lib/chainlink"] - path = lib/chainlink - url = https://github.com/smartcontractkit/chainlink [submodule "lib/ERC721A-Upgradeable"] path = lib/ERC721A-Upgradeable url = https://github.com/chiru-labs/ERC721A-Upgradeable diff --git a/contracts/external-deps/chainlink/LinkTokenInterface.sol b/contracts/external-deps/chainlink/LinkTokenInterface.sol deleted file mode 100644 index 203f8684c..000000000 --- a/contracts/external-deps/chainlink/LinkTokenInterface.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -interface LinkTokenInterface { - function allowance(address owner, address spender) external view returns (uint256 remaining); - - function approve(address spender, uint256 value) external returns (bool success); - - function balanceOf(address owner) external view returns (uint256 balance); - - function decimals() external view returns (uint8 decimalPlaces); - - function decreaseApproval(address spender, uint256 addedValue) external returns (bool success); - - function increaseApproval(address spender, uint256 subtractedValue) external; - - function name() external view returns (string memory tokenName); - - function symbol() external view returns (string memory tokenSymbol); - - function totalSupply() external view returns (uint256 totalTokensIssued); - - function transfer(address to, uint256 value) external returns (bool success); - - function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool success); - - function transferFrom(address from, address to, uint256 value) external returns (bool success); -} diff --git a/contracts/external-deps/chainlink/VRFV2WrapperConsumerBase.sol b/contracts/external-deps/chainlink/VRFV2WrapperConsumerBase.sol deleted file mode 100644 index 48a62ee60..000000000 --- a/contracts/external-deps/chainlink/VRFV2WrapperConsumerBase.sol +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "./LinkTokenInterface.sol"; -import "./VRFV2WrapperInterface.sol"; - -/** ******************************************************************************* - * @notice Interface for contracts using VRF randomness through the VRF V2 wrapper - * ******************************************************************************** - * @dev PURPOSE - * - * @dev Create VRF V2 requests without the need for subscription management. Rather than creating - * @dev and funding a VRF V2 subscription, a user can use this wrapper to create one off requests, - * @dev paying up front rather than at fulfillment. - * - * @dev Since the price is determined using the gas price of the request transaction rather than - * @dev the fulfillment transaction, the wrapper charges an additional premium on callback gas - * @dev usage, in addition to some extra overhead costs associated with the VRFV2Wrapper contract. - * ***************************************************************************** - * @dev USAGE - * - * @dev Calling contracts must inherit from VRFV2WrapperConsumerBase. The consumer must be funded - * @dev with enough LINK to make the request, otherwise requests will revert. To request randomness, - * @dev call the 'requestRandomness' function with the desired VRF parameters. This function handles - * @dev paying for the request based on the current pricing. - * - * @dev Consumers must implement the fullfillRandomWords function, which will be called during - * @dev fulfillment with the randomness result. - */ -abstract contract VRFV2WrapperConsumerBase { - // solhint-disable-next-line var-name-mixedcase - LinkTokenInterface internal immutable LINK; - // solhint-disable-next-line var-name-mixedcase - VRFV2WrapperInterface internal immutable VRF_V2_WRAPPER; - - /** - * @param _link is the address of LinkToken - * @param _vrfV2Wrapper is the address of the VRFV2Wrapper contract - */ - constructor(address _link, address _vrfV2Wrapper) { - LINK = LinkTokenInterface(_link); - VRF_V2_WRAPPER = VRFV2WrapperInterface(_vrfV2Wrapper); - } - - /** - * @dev Requests randomness from the VRF V2 wrapper. - * - * @param _callbackGasLimit is the gas limit that should be used when calling the consumer's - * fulfillRandomWords function. - * @param _requestConfirmations is the number of confirmations to wait before fulfilling the - * request. A higher number of confirmations increases security by reducing the likelihood - * that a chain re-org changes a published randomness outcome. - * @param _numWords is the number of random words to request. - * - * @return requestId is the VRF V2 request ID of the newly created randomness request. - */ - function requestRandomness( - uint32 _callbackGasLimit, - uint16 _requestConfirmations, - uint32 _numWords - ) internal returns (uint256 requestId) { - LINK.transferAndCall( - address(VRF_V2_WRAPPER), - VRF_V2_WRAPPER.calculateRequestPrice(_callbackGasLimit), - abi.encode(_callbackGasLimit, _requestConfirmations, _numWords) - ); - return VRF_V2_WRAPPER.lastRequestId(); - } - - /** - * @notice fulfillRandomWords handles the VRF V2 wrapper response. The consuming contract must - * @notice implement it. - * - * @param _requestId is the VRF V2 request ID. - * @param _randomWords is the randomness result. - */ - function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal virtual; - - function rawFulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) external { - require(msg.sender == address(VRF_V2_WRAPPER), "only VRF V2 wrapper can fulfill"); - fulfillRandomWords(_requestId, _randomWords); - } -} diff --git a/contracts/external-deps/chainlink/VRFV2WrapperInterface.sol b/contracts/external-deps/chainlink/VRFV2WrapperInterface.sol deleted file mode 100644 index b636940bb..000000000 --- a/contracts/external-deps/chainlink/VRFV2WrapperInterface.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -interface VRFV2WrapperInterface { - /** - * @return the request ID of the most recent VRF V2 request made by this wrapper. This should only - * be relied option within the same transaction that the request was made. - */ - function lastRequestId() external view returns (uint256); - - /** - * @notice Calculates the price of a VRF request with the given callbackGasLimit at the current - * @notice block. - * - * @dev This function relies on the transaction gas price which is not automatically set during - * @dev simulation. To estimate the price at a specific gas price, use the estimatePrice function. - * - * @param _callbackGasLimit is the gas limit used to estimate the price. - */ - function calculateRequestPrice(uint32 _callbackGasLimit) external view returns (uint256); - - /** - * @notice Estimates the price of a VRF request with a specific gas limit and gas price. - * - * @dev This is a convenience function that can be called in simulation to better understand - * @dev pricing. - * - * @param _callbackGasLimit is the gas limit used to estimate the price. - * @param _requestGasPriceWei is the gas price in wei used for the estimation. - */ - function estimateRequestPrice( - uint32 _callbackGasLimit, - uint256 _requestGasPriceWei - ) external view returns (uint256); -} diff --git a/contracts/external-deps/openzeppelin/metatx/MinimalForwarderEOAOnly.sol b/contracts/external-deps/openzeppelin/metatx/MinimalForwarderEOAOnly.sol deleted file mode 100644 index 6a5c2ef61..000000000 --- a/contracts/external-deps/openzeppelin/metatx/MinimalForwarderEOAOnly.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (metatx/MinimalForwarder.sol) - -pragma solidity ^0.8.0; - -import "../utils/cryptography/ECDSA.sol"; -import "../utils/cryptography/EIP712.sol"; - -/** - * @dev Simple minimal forwarder to be used together with an ERC2771 compatible contract. See {ERC2771Context}. - */ -contract MinimalForwarderEOAOnly is EIP712 { - using ECDSA for bytes32; - - struct ForwardRequest { - address from; - address to; - uint256 value; - uint256 gas; - uint256 nonce; - bytes data; - } - - bytes32 private constant _TYPEHASH = - keccak256("ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data)"); - - mapping(address => uint256) private _nonces; - - constructor() EIP712("GSNv2 Forwarder", "0.0.1") {} - - function getNonce(address from) public view returns (uint256) { - return _nonces[from]; - } - - function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) { - address signer = _hashTypedDataV4( - keccak256(abi.encode(_TYPEHASH, req.from, req.to, req.value, req.gas, req.nonce, keccak256(req.data))) - ).recover(signature); - return _nonces[req.from] == req.nonce && signer == req.from; - } - - function execute( - ForwardRequest calldata req, - bytes calldata signature - ) public payable returns (bool, bytes memory) { - require(msg.sender == tx.origin, "not EOA"); - require(verify(req, signature), "MinimalForwarder: signature does not match request"); - _nonces[req.from] = req.nonce + 1; - - (bool success, bytes memory returndata) = req.to.call{ gas: req.gas, value: req.value }( - abi.encodePacked(req.data, req.from) - ); - - // Validate that the relayer has sent enough gas for the call. - // See https://ronan.eth.link/blog/ethereum-gas-dangers/ - if (gasleft() <= req.gas / 63) { - // We explicitly trigger invalid opcode to consume all gas and bubble-up the effects, since - // neither revert or assert consume all gas since Solidity 0.8.0 - // https://docs.soliditylang.org/en/v0.8.0/control-structures.html#panic-via-assert-and-error-via-require - assembly { - invalid() - } - } - - return (success, returndata); - } -} diff --git a/contracts/infra/forwarder/ForwarderEOAOnly.sol b/contracts/infra/forwarder/ForwarderEOAOnly.sol deleted file mode 100644 index b0612339f..000000000 --- a/contracts/infra/forwarder/ForwarderEOAOnly.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -/// @author thirdweb - -// $$\ $$\ $$\ $$\ $$\ -// $$ | $$ | \__| $$ | $$ | -// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ -// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ -// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | -// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | -// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | -// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ - -import "../../external-deps/openzeppelin/metatx/MinimalForwarderEOAOnly.sol"; - -/* - * @dev Minimal forwarder for GSNv2 - */ -contract ForwarderEOAOnly is MinimalForwarderEOAOnly { - // solhint-disable-next-line no-empty-blocks - constructor() MinimalForwarderEOAOnly() {} -} diff --git a/contracts/legacy-contracts/extension/BatchMintMetadata_V1.sol b/contracts/legacy-contracts/extension/BatchMintMetadata_V1.sol deleted file mode 100644 index 7158488dd..000000000 --- a/contracts/legacy-contracts/extension/BatchMintMetadata_V1.sol +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/// @author thirdweb - -/** - * @title Batch-mint Metadata - * @notice The `BatchMintMetadata` is a contract extension for any base NFT contract. It lets the smart contract - * using this extension set metadata for `n` number of NFTs all at once. This is enabled by storing a single - * base URI for a batch of `n` NFTs, where the metadata for each NFT in a relevant batch is `baseURI/tokenId`. - */ - -contract BatchMintMetadata_V1 { - /// @dev Largest tokenId of each batch of tokens with the same baseURI. - uint256[] private batchIds; - - /// @dev Mapping from id of a batch of tokens => to base URI for the respective batch of tokens. - mapping(uint256 => string) private baseURI; - - /** - * @notice Returns the count of batches of NFTs. - * @dev Each batch of tokens has an in ID and an associated `baseURI`. - * See {batchIds}. - */ - function getBaseURICount() public view returns (uint256) { - return batchIds.length; - } - - /** - * @notice Returns the ID for the batch of tokens at the given index. - * @dev See {getBaseURICount}. - * @param _index Index of the desired batch in batchIds array. - */ - function getBatchIdAtIndex(uint256 _index) public view returns (uint256) { - if (_index >= getBaseURICount()) { - revert("Invalid index"); - } - return batchIds[_index]; - } - - /// @dev Returns the id for the batch of tokens the given tokenId belongs to. - function _getBatchId(uint256 _tokenId) internal view returns (uint256 batchId, uint256 index) { - uint256 numOfTokenBatches = getBaseURICount(); - uint256[] memory indices = batchIds; - - for (uint256 i = 0; i < numOfTokenBatches; i += 1) { - if (_tokenId < indices[i]) { - index = i; - batchId = indices[i]; - - return (batchId, index); - } - } - - revert("Invalid tokenId"); - } - - /// @dev Returns the baseURI for a token. The intended metadata URI for the token is baseURI + tokenId. - function _getBaseURI(uint256 _tokenId) internal view returns (string memory) { - uint256 numOfTokenBatches = getBaseURICount(); - uint256[] memory indices = batchIds; - - for (uint256 i = 0; i < numOfTokenBatches; i += 1) { - if (_tokenId < indices[i]) { - return baseURI[indices[i]]; - } - } - revert("Invalid tokenId"); - } - - /// @dev Sets the base URI for the batch of tokens with the given batchId. - function _setBaseURI(uint256 _batchId, string memory _baseURI) internal { - baseURI[_batchId] = _baseURI; - } - - /// @dev Mints a batch of tokenIds and associates a common baseURI to all those Ids. - function _batchMintMetadata( - uint256 _startId, - uint256 _amountToMint, - string memory _baseURIForTokens - ) internal returns (uint256 nextTokenIdToMint, uint256 batchId) { - batchId = _startId + _amountToMint; - nextTokenIdToMint = batchId; - - batchIds.push(batchId); - - baseURI[batchId] = _baseURIForTokens; - } -} diff --git a/contracts/legacy-contracts/extension/DropSinglePhase1155_V1.sol b/contracts/legacy-contracts/extension/DropSinglePhase1155_V1.sol deleted file mode 100644 index 003906bd7..000000000 --- a/contracts/legacy-contracts/extension/DropSinglePhase1155_V1.sol +++ /dev/null @@ -1,268 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/// @author thirdweb - -import "./interface/IDropSinglePhase1155_V1.sol"; -import "../../lib/MerkleProof.sol"; -import "../../lib/BitMaps.sol"; - -abstract contract DropSinglePhase1155_V1 is IDropSinglePhase1155_V1 { - using BitMaps for BitMaps.BitMap; - - /*/////////////////////////////////////////////////////////////// - Mappings - //////////////////////////////////////////////////////////////*/ - - /// @dev Mapping from tokenId => active claim condition for the tokenId. - mapping(uint256 => ClaimCondition) public claimCondition; - - /// @dev Mapping from tokenId => active claim condition's UID. - mapping(uint256 => bytes32) private conditionId; - - /** - * @dev Map from an account and uid for a claim condition, to the last timestamp - * at which the account claimed tokens under that claim condition. - */ - mapping(bytes32 => mapping(address => uint256)) private lastClaimTimestamp; - - /** - * @dev Map from a claim condition uid to whether an address in an allowlist - * has already claimed tokens i.e. used their place in the allowlist. - */ - mapping(bytes32 => BitMaps.BitMap) private usedAllowlistSpot; - - /*/////////////////////////////////////////////////////////////// - Drop logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets an account claim tokens. - function claim( - address _receiver, - uint256 _tokenId, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - AllowlistProof calldata _allowlistProof, - bytes memory _data - ) public payable virtual override { - _beforeClaim(_tokenId, _receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); - - ClaimCondition memory condition = claimCondition[_tokenId]; - bytes32 activeConditionId = conditionId[_tokenId]; - - /** - * We make allowlist checks (i.e. verifyClaimMerkleProof) before verifying the claim's general - * validity (i.e. verifyClaim) because we give precedence to the check of allow list quantity - * restriction over the check of the general claim condition's quantityLimitPerTransaction - * restriction. - */ - - // Verify inclusion in allowlist. - (bool validMerkleProof, ) = verifyClaimMerkleProof(_tokenId, _dropMsgSender(), _quantity, _allowlistProof); - - // Verify claim validity. If not valid, revert. - // when there's allowlist present --> verifyClaimMerkleProof will verify the maxQuantityInAllowlist value with hashed leaf in the allowlist - // when there's no allowlist, this check is true --> verifyClaim will check for _quantity being equal/less than the limit - bool toVerifyMaxQuantityPerTransaction = _allowlistProof.maxQuantityInAllowlist == 0 || - condition.merkleRoot == bytes32(0); - - verifyClaim( - _tokenId, - _dropMsgSender(), - _quantity, - _currency, - _pricePerToken, - toVerifyMaxQuantityPerTransaction - ); - - if (validMerkleProof && _allowlistProof.maxQuantityInAllowlist > 0) { - /** - * Mark the claimer's use of their position in the allowlist. A spot in an allowlist - * can be used only once. - */ - usedAllowlistSpot[activeConditionId].set(uint256(uint160(_dropMsgSender()))); - } - - // Update contract state. - condition.supplyClaimed += _quantity; - lastClaimTimestamp[activeConditionId][_dropMsgSender()] = block.timestamp; - claimCondition[_tokenId] = condition; - - // If there's a price, collect price. - _collectPriceOnClaim(address(0), _quantity, _currency, _pricePerToken); - - // Mint the relevant NFTs to claimer. - _transferTokensOnClaim(_receiver, _tokenId, _quantity); - - emit TokensClaimed(_dropMsgSender(), _receiver, _tokenId, _quantity); - - _afterClaim(_tokenId, _receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); - } - - /// @dev Lets a contract admin set claim conditions. - function setClaimConditions( - uint256 _tokenId, - ClaimCondition calldata _condition, - bool _resetClaimEligibility - ) external override { - if (!_canSetClaimConditions()) { - revert("Not authorized"); - } - - ClaimCondition memory condition = claimCondition[_tokenId]; - bytes32 targetConditionId = conditionId[_tokenId]; - - uint256 supplyClaimedAlready = condition.supplyClaimed; - - if (targetConditionId == bytes32(0) || _resetClaimEligibility) { - supplyClaimedAlready = 0; - targetConditionId = keccak256(abi.encodePacked(_dropMsgSender(), block.number, _tokenId)); - } - - if (supplyClaimedAlready > _condition.maxClaimableSupply) { - revert("max supply claimed"); - } - - ClaimCondition memory updatedCondition = ClaimCondition({ - startTimestamp: _condition.startTimestamp, - maxClaimableSupply: _condition.maxClaimableSupply, - supplyClaimed: supplyClaimedAlready, - quantityLimitPerTransaction: _condition.quantityLimitPerTransaction, - waitTimeInSecondsBetweenClaims: _condition.waitTimeInSecondsBetweenClaims, - merkleRoot: _condition.merkleRoot, - pricePerToken: _condition.pricePerToken, - currency: _condition.currency - }); - - claimCondition[_tokenId] = updatedCondition; - conditionId[_tokenId] = targetConditionId; - - emit ClaimConditionUpdated(_tokenId, _condition, _resetClaimEligibility); - } - - /// @dev Checks a request to claim NFTs against the active claim condition's criteria. - function verifyClaim( - uint256 _tokenId, - address _claimer, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - bool verifyMaxQuantityPerTransaction - ) public view { - ClaimCondition memory currentClaimPhase = claimCondition[_tokenId]; - - if (_currency != currentClaimPhase.currency || _pricePerToken != currentClaimPhase.pricePerToken) { - revert("Invalid price or currency"); - } - - // If we're checking for an allowlist quantity restriction, ignore the general quantity restriction. - if ( - _quantity == 0 || - (verifyMaxQuantityPerTransaction && _quantity > currentClaimPhase.quantityLimitPerTransaction) - ) { - revert("Invalid quantity"); - } - - if (currentClaimPhase.supplyClaimed + _quantity > currentClaimPhase.maxClaimableSupply) { - revert("exceeds max supply"); - } - - (uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) = getClaimTimestamp(_tokenId, _claimer); - if ( - currentClaimPhase.startTimestamp > block.timestamp || - (lastClaimedAt != 0 && block.timestamp < nextValidClaimTimestamp) - ) { - revert("cant claim yet"); - } - } - - /// @dev Checks whether a claimer meets the claim condition's allowlist criteria. - function verifyClaimMerkleProof( - uint256 _tokenId, - address _claimer, - uint256 _quantity, - AllowlistProof calldata _allowlistProof - ) public view returns (bool validMerkleProof, uint256 merkleProofIndex) { - ClaimCondition memory currentClaimPhase = claimCondition[_tokenId]; - - if (currentClaimPhase.merkleRoot != bytes32(0)) { - (validMerkleProof, merkleProofIndex) = MerkleProof.verify( - _allowlistProof.proof, - currentClaimPhase.merkleRoot, - keccak256(abi.encodePacked(_claimer, _allowlistProof.maxQuantityInAllowlist)) - ); - if (!validMerkleProof) { - revert("not in allowlist"); - } - - if (usedAllowlistSpot[conditionId[_tokenId]].get(uint256(uint160(_claimer)))) { - revert("proof claimed"); - } - - if (_allowlistProof.maxQuantityInAllowlist != 0 && _quantity > _allowlistProof.maxQuantityInAllowlist) { - revert("Invalid qty proof"); - } - } - } - - /// @dev Returns the timestamp for when a claimer is eligible for claiming NFTs again. - function getClaimTimestamp( - uint256 _tokenId, - address _claimer - ) public view returns (uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) { - lastClaimedAt = lastClaimTimestamp[conditionId[_tokenId]][_claimer]; - - unchecked { - nextValidClaimTimestamp = lastClaimedAt + claimCondition[_tokenId].waitTimeInSecondsBetweenClaims; - - if (nextValidClaimTimestamp < lastClaimedAt) { - nextValidClaimTimestamp = type(uint256).max; - } - } - } - - /*//////////////////////////////////////////////////////////////////// - Optional hooks that can be implemented in the derived contract - ///////////////////////////////////////////////////////////////////*/ - - /// @dev Exposes the ability to override the msg sender. - function _dropMsgSender() internal virtual returns (address) { - return msg.sender; - } - - /// @dev Runs before every `claim` function call. - function _beforeClaim( - uint256 _tokenId, - address _receiver, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - AllowlistProof calldata _allowlistProof, - bytes memory _data - ) internal virtual {} - - /// @dev Runs after every `claim` function call. - function _afterClaim( - uint256 _tokenId, - address _receiver, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - AllowlistProof calldata _allowlistProof, - bytes memory _data - ) internal virtual {} - - /// @dev Collects and distributes the primary sale value of NFTs being claimed. - function _collectPriceOnClaim( - address _primarySaleRecipient, - uint256 _quantityToClaim, - address _currency, - uint256 _pricePerToken - ) internal virtual; - - /// @dev Transfers the NFTs being claimed. - function _transferTokensOnClaim(address _to, uint256 _tokenId, uint256 _quantityBeingClaimed) internal virtual; - - function _canSetClaimConditions() internal view virtual returns (bool); -} diff --git a/contracts/legacy-contracts/extension/DropSinglePhase_V1.sol b/contracts/legacy-contracts/extension/DropSinglePhase_V1.sol deleted file mode 100644 index 571a0150b..000000000 --- a/contracts/legacy-contracts/extension/DropSinglePhase_V1.sol +++ /dev/null @@ -1,252 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/// @author thirdweb - -import "./interface/IDropSinglePhase_V1.sol"; -import "../../lib/MerkleProof.sol"; -import "../../lib/BitMaps.sol"; - -abstract contract DropSinglePhase_V1 is IDropSinglePhase_V1 { - using BitMaps for BitMaps.BitMap; - - /*/////////////////////////////////////////////////////////////// - State variables - //////////////////////////////////////////////////////////////*/ - - /// @dev The active conditions for claiming tokens. - ClaimCondition public claimCondition; - - /// @dev The ID for the active claim condition. - bytes32 private conditionId; - - /*/////////////////////////////////////////////////////////////// - Mappings - //////////////////////////////////////////////////////////////*/ - - /** - * @dev Map from an account and uid for a claim condition, to the last timestamp - * at which the account claimed tokens under that claim condition. - */ - mapping(bytes32 => mapping(address => uint256)) private lastClaimTimestamp; - - /** - * @dev Map from a claim condition uid to whether an address in an allowlist - * has already claimed tokens i.e. used their place in the allowlist. - */ - mapping(bytes32 => BitMaps.BitMap) private usedAllowlistSpot; - - /*/////////////////////////////////////////////////////////////// - Drop logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets an account claim tokens. - function claim( - address _receiver, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - AllowlistProof calldata _allowlistProof, - bytes memory _data - ) public payable virtual override { - _beforeClaim(_receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); - - bytes32 activeConditionId = conditionId; - - /** - * We make allowlist checks (i.e. verifyClaimMerkleProof) before verifying the claim's general - * validity (i.e. verifyClaim) because we give precedence to the check of allow list quantity - * restriction over the check of the general claim condition's quantityLimitPerTransaction - * restriction. - */ - - // Verify inclusion in allowlist. - (bool validMerkleProof, ) = verifyClaimMerkleProof(_dropMsgSender(), _quantity, _allowlistProof); - - // Verify claim validity. If not valid, revert. - // when there's allowlist present --> verifyClaimMerkleProof will verify the maxQuantityInAllowlist value with hashed leaf in the allowlist - // when there's no allowlist, this check is true --> verifyClaim will check for _quantity being equal/less than the limit - bool toVerifyMaxQuantityPerTransaction = _allowlistProof.maxQuantityInAllowlist == 0 || - claimCondition.merkleRoot == bytes32(0); - - verifyClaim(_dropMsgSender(), _quantity, _currency, _pricePerToken, toVerifyMaxQuantityPerTransaction); - - if (validMerkleProof && _allowlistProof.maxQuantityInAllowlist > 0) { - /** - * Mark the claimer's use of their position in the allowlist. A spot in an allowlist - * can be used only once. - */ - usedAllowlistSpot[activeConditionId].set(uint256(uint160(_dropMsgSender()))); - } - - // Update contract state. - claimCondition.supplyClaimed += _quantity; - lastClaimTimestamp[activeConditionId][_dropMsgSender()] = block.timestamp; - - // If there's a price, collect price. - _collectPriceOnClaim(address(0), _quantity, _currency, _pricePerToken); - - // Mint the relevant NFTs to claimer. - uint256 startTokenId = _transferTokensOnClaim(_receiver, _quantity); - - emit TokensClaimed(_dropMsgSender(), _receiver, startTokenId, _quantity); - - _afterClaim(_receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); - } - - /// @dev Lets a contract admin set claim conditions. - function setClaimConditions(ClaimCondition calldata _condition, bool _resetClaimEligibility) external override { - if (!_canSetClaimConditions()) { - revert("Not authorized"); - } - - bytes32 targetConditionId = conditionId; - uint256 supplyClaimedAlready = claimCondition.supplyClaimed; - - if (_resetClaimEligibility) { - supplyClaimedAlready = 0; - targetConditionId = keccak256(abi.encodePacked(_dropMsgSender(), block.number)); - } - - if (supplyClaimedAlready > _condition.maxClaimableSupply) { - revert("max supply claimed"); - } - - claimCondition = ClaimCondition({ - startTimestamp: _condition.startTimestamp, - maxClaimableSupply: _condition.maxClaimableSupply, - supplyClaimed: supplyClaimedAlready, - quantityLimitPerTransaction: _condition.quantityLimitPerTransaction, - waitTimeInSecondsBetweenClaims: _condition.waitTimeInSecondsBetweenClaims, - merkleRoot: _condition.merkleRoot, - pricePerToken: _condition.pricePerToken, - currency: _condition.currency - }); - conditionId = targetConditionId; - - emit ClaimConditionUpdated(_condition, _resetClaimEligibility); - } - - /// @dev Checks a request to claim NFTs against the active claim condition's criteria. - function verifyClaim( - address _claimer, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - bool verifyMaxQuantityPerTransaction - ) public view { - ClaimCondition memory currentClaimPhase = claimCondition; - - if (_currency != currentClaimPhase.currency || _pricePerToken != currentClaimPhase.pricePerToken) { - revert("Invalid price or currency"); - } - - // If we're checking for an allowlist quantity restriction, ignore the general quantity restriction. - if ( - _quantity == 0 || - (verifyMaxQuantityPerTransaction && _quantity > currentClaimPhase.quantityLimitPerTransaction) - ) { - revert("Invalid quantity"); - } - - if (currentClaimPhase.supplyClaimed + _quantity > currentClaimPhase.maxClaimableSupply) { - revert("exceeds max supply"); - } - - (uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) = getClaimTimestamp(_claimer); - if ( - currentClaimPhase.startTimestamp > block.timestamp || - (lastClaimedAt != 0 && block.timestamp < nextValidClaimTimestamp) - ) { - revert("cant claim yet"); - } - } - - /// @dev Checks whether a claimer meets the claim condition's allowlist criteria. - function verifyClaimMerkleProof( - address _claimer, - uint256 _quantity, - AllowlistProof calldata _allowlistProof - ) public view returns (bool validMerkleProof, uint256 merkleProofIndex) { - ClaimCondition memory currentClaimPhase = claimCondition; - - if (currentClaimPhase.merkleRoot != bytes32(0)) { - (validMerkleProof, merkleProofIndex) = MerkleProof.verify( - _allowlistProof.proof, - currentClaimPhase.merkleRoot, - keccak256(abi.encodePacked(_claimer, _allowlistProof.maxQuantityInAllowlist)) - ); - if (!validMerkleProof) { - revert("not in allowlist"); - } - - if (usedAllowlistSpot[conditionId].get(uint256(uint160(_claimer)))) { - revert("proof claimed"); - } - - if (_allowlistProof.maxQuantityInAllowlist != 0 && _quantity > _allowlistProof.maxQuantityInAllowlist) { - revert("Invalid qty proof"); - } - } - } - - /// @dev Returns the timestamp for when a claimer is eligible for claiming NFTs again. - function getClaimTimestamp( - address _claimer - ) public view returns (uint256 lastClaimedAt, uint256 nextValidClaimTimestamp) { - lastClaimedAt = lastClaimTimestamp[conditionId][_claimer]; - - unchecked { - nextValidClaimTimestamp = lastClaimedAt + claimCondition.waitTimeInSecondsBetweenClaims; - - if (nextValidClaimTimestamp < lastClaimedAt) { - nextValidClaimTimestamp = type(uint256).max; - } - } - } - - /*//////////////////////////////////////////////////////////////////// - Optional hooks that can be implemented in the derived contract - ///////////////////////////////////////////////////////////////////*/ - - /// @dev Exposes the ability to override the msg sender. - function _dropMsgSender() internal virtual returns (address) { - return msg.sender; - } - - /// @dev Runs before every `claim` function call. - function _beforeClaim( - address _receiver, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - AllowlistProof calldata _allowlistProof, - bytes memory _data - ) internal virtual {} - - /// @dev Runs after every `claim` function call. - function _afterClaim( - address _receiver, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - AllowlistProof calldata _allowlistProof, - bytes memory _data - ) internal virtual {} - - /// @dev Collects and distributes the primary sale value of NFTs being claimed. - function _collectPriceOnClaim( - address _primarySaleRecipient, - uint256 _quantityToClaim, - address _currency, - uint256 _pricePerToken - ) internal virtual; - - /// @dev Transfers the NFTs being claimed. - function _transferTokensOnClaim( - address _to, - uint256 _quantityBeingClaimed - ) internal virtual returns (uint256 startTokenId); - - function _canSetClaimConditions() internal view virtual returns (bool); -} diff --git a/contracts/legacy-contracts/extension/LazyMintWithTier_V1.sol b/contracts/legacy-contracts/extension/LazyMintWithTier_V1.sol deleted file mode 100644 index 2acda2134..000000000 --- a/contracts/legacy-contracts/extension/LazyMintWithTier_V1.sol +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/// @author thirdweb - -import "../../extension/interface/ILazyMintWithTier.sol"; -import "./BatchMintMetadata_V1.sol"; - -/** - * The `LazyMint` is a contract extension for any base NFT contract. It lets you 'lazy mint' any number of NFTs - * at once. Here, 'lazy mint' means defining the metadata for particular tokenIds of your NFT contract, without actually - * minting a non-zero balance of NFTs of those tokenIds. - */ - -abstract contract LazyMintWithTier_V1 is ILazyMintWithTier, BatchMintMetadata_V1 { - struct TokenRange { - uint256 startIdInclusive; - uint256 endIdNonInclusive; - } - - struct TierMetadata { - string tier; - TokenRange[] ranges; - string[] baseURIs; - } - - /// @notice The tokenId assigned to the next new NFT to be lazy minted. - uint256 internal nextTokenIdToLazyMint; - - /// @notice Mapping from a tier -> the token IDs grouped under that tier. - mapping(string => TokenRange[]) internal tokensInTier; - - /// @notice A list of tiers used in this contract. - string[] private tiers; - - /** - * @notice Lets an authorized address lazy mint a given amount of NFTs. - * - * @param _amount The number of NFTs to lazy mint. - * @param _baseURIForTokens The base URI for the 'n' number of NFTs being lazy minted, where the metadata for each - * of those NFTs is `${baseURIForTokens}/${tokenId}`. - * @param _data Additional bytes data to be used at the discretion of the consumer of the contract. - * @return batchId A unique integer identifier for the batch of NFTs lazy minted together. - */ - function lazyMint( - uint256 _amount, - string calldata _baseURIForTokens, - string calldata _tier, - bytes calldata _data - ) public virtual override returns (uint256 batchId) { - if (!_canLazyMint()) { - revert("Not authorized"); - } - - if (_amount == 0) { - revert("0 amt"); - } - - uint256 startId = nextTokenIdToLazyMint; - - (nextTokenIdToLazyMint, batchId) = _batchMintMetadata(startId, _amount, _baseURIForTokens); - - // Handle tier info. - if (!(tokensInTier[_tier].length > 0)) { - tiers.push(_tier); - } - tokensInTier[_tier].push(TokenRange(startId, batchId)); - - emit TokensLazyMinted(_tier, startId, startId + _amount - 1, _baseURIForTokens, _data); - - return batchId; - } - - /// @notice Returns all metadata lazy minted for the given tier. - function _getMetadataInTier( - string memory _tier - ) private view returns (TokenRange[] memory tokens, string[] memory baseURIs) { - tokens = tokensInTier[_tier]; - - uint256 len = tokens.length; - baseURIs = new string[](len); - - for (uint256 i = 0; i < len; i += 1) { - baseURIs[i] = _getBaseURI(tokens[i].startIdInclusive); - } - } - - /// @notice Returns all metadata for all tiers created on the contract. - function getMetadataForAllTiers() external view returns (TierMetadata[] memory metadataForAllTiers) { - string[] memory allTiers = tiers; - uint256 len = allTiers.length; - - metadataForAllTiers = new TierMetadata[](len); - - for (uint256 i = 0; i < len; i += 1) { - (TokenRange[] memory tokens, string[] memory baseURIs) = _getMetadataInTier(allTiers[i]); - metadataForAllTiers[i] = TierMetadata(allTiers[i], tokens, baseURIs); - } - } - - /** - * @notice Returns whether any metadata is lazy minted for the given tier. - * - * @param _tier We check whether this given tier is empty. - */ - function isTierEmpty(string memory _tier) internal view returns (bool) { - return tokensInTier[_tier].length == 0; - } - - /// @dev Returns whether lazy minting can be performed in the given execution context. - function _canLazyMint() internal view virtual returns (bool); -} diff --git a/contracts/legacy-contracts/extension/LazyMint_V1.sol b/contracts/legacy-contracts/extension/LazyMint_V1.sol deleted file mode 100644 index 820ea4eed..000000000 --- a/contracts/legacy-contracts/extension/LazyMint_V1.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/// @author thirdweb - -import "../../extension/interface/ILazyMint.sol"; -import "./BatchMintMetadata_V1.sol"; - -/** - * The `LazyMint` is a contract extension for any base NFT contract. It lets you 'lazy mint' any number of NFTs - * at once. Here, 'lazy mint' means defining the metadata for particular tokenIds of your NFT contract, without actually - * minting a non-zero balance of NFTs of those tokenIds. - */ - -abstract contract LazyMint_V1 is ILazyMint, BatchMintMetadata_V1 { - /// @notice The tokenId assigned to the next new NFT to be lazy minted. - uint256 internal nextTokenIdToLazyMint; - - /** - * @notice Lets an authorized address lazy mint a given amount of NFTs. - * - * @param _amount The number of NFTs to lazy mint. - * @param _baseURIForTokens The base URI for the 'n' number of NFTs being lazy minted, where the metadata for each - * of those NFTs is `${baseURIForTokens}/${tokenId}`. - * @param _data Additional bytes data to be used at the discretion of the consumer of the contract. - * @return batchId A unique integer identifier for the batch of NFTs lazy minted together. - */ - function lazyMint( - uint256 _amount, - string calldata _baseURIForTokens, - bytes calldata _data - ) public virtual override returns (uint256 batchId) { - if (!_canLazyMint()) { - revert("Not authorized"); - } - - if (_amount == 0) { - revert("0 amt"); - } - - uint256 startId = nextTokenIdToLazyMint; - - (nextTokenIdToLazyMint, batchId) = _batchMintMetadata(startId, _amount, _baseURIForTokens); - - emit TokensLazyMinted(startId, startId + _amount - 1, _baseURIForTokens, _data); - - return batchId; - } - - /// @dev Returns whether lazy minting can be performed in the given execution context. - function _canLazyMint() internal view virtual returns (bool); -} diff --git a/contracts/legacy-contracts/extension/PlatformFee_V1.sol b/contracts/legacy-contracts/extension/PlatformFee_V1.sol deleted file mode 100644 index 9e3af425f..000000000 --- a/contracts/legacy-contracts/extension/PlatformFee_V1.sol +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/// @author thirdweb - -import "./interface/IPlatformFee_V1.sol"; - -/** - * @title Platform Fee - * @notice Thirdweb's `PlatformFee` is a contract extension to be used with any base contract. It exposes functions for setting and reading - * the recipient of platform fee and the platform fee basis points, and lets the inheriting contract perform conditional logic - * that uses information about platform fees, if desired. - */ - -abstract contract PlatformFee is IPlatformFee { - /// @dev The sender is not authorized to perform the action - error PlatformFeeUnauthorized(); - - /// @dev The recipient is invalid - error PlatformFeeInvalidRecipient(address recipient); - - /// @dev The fee bps exceeded the max value - error PlatformFeeExceededMaxFeeBps(uint256 max, uint256 actual); - - /// @dev The address that receives all platform fees from all sales. - address private platformFeeRecipient; - - /// @dev The % of primary sales collected as platform fees. - uint16 private platformFeeBps; - - /// @dev Returns the platform fee recipient and bps. - function getPlatformFeeInfo() public view override returns (address, uint16) { - return (platformFeeRecipient, uint16(platformFeeBps)); - } - - /** - * @notice Updates the platform fee recipient and bps. - * @dev Caller should be authorized to set platform fee info. - * See {_canSetPlatformFeeInfo}. - * Emits {PlatformFeeInfoUpdated Event}; See {_setupPlatformFeeInfo}. - - * @param _platformFeeRecipient Address to be set as new platformFeeRecipient. - * @param _platformFeeBps Updated platformFeeBps. - */ - function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external override { - if (!_canSetPlatformFeeInfo()) { - revert PlatformFeeUnauthorized(); - } - _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); - } - - /// @dev Sets the platform fee recipient and bps - function _setupPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) internal { - if (_platformFeeBps > 10_000) { - revert PlatformFeeExceededMaxFeeBps(10_000, _platformFeeBps); - } - if (_platformFeeRecipient == address(0)) { - revert PlatformFeeInvalidRecipient(_platformFeeRecipient); - } - - platformFeeBps = uint16(_platformFeeBps); - platformFeeRecipient = _platformFeeRecipient; - - emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); - } - - /// @dev Returns whether platform fee info can be set in the given execution context. - function _canSetPlatformFeeInfo() internal view virtual returns (bool); -} diff --git a/contracts/legacy-contracts/extension/PrimarySale_V1.sol b/contracts/legacy-contracts/extension/PrimarySale_V1.sol deleted file mode 100644 index 165501754..000000000 --- a/contracts/legacy-contracts/extension/PrimarySale_V1.sol +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/// @author thirdweb - -import "./interface/IPrimarySale_V1.sol"; - -/** - * @title Primary Sale - * @notice Thirdweb's `PrimarySale` is a contract extension to be used with any base contract. It exposes functions for setting and reading - * the recipient of primary sales, and lets the inheriting contract perform conditional logic that uses information about - * primary sales, if desired. - */ - -abstract contract PrimarySale is IPrimarySale { - /// @dev The sender is not authorized to perform the action - error PrimarySaleUnauthorized(); - - /// @dev The recipient is invalid - error PrimarySaleInvalidRecipient(address recipient); - - /// @dev The address that receives all primary sales value. - address private recipient; - - /// @dev Returns primary sale recipient address. - function primarySaleRecipient() public view override returns (address) { - return recipient; - } - - /** - * @notice Updates primary sale recipient. - * @dev Caller should be authorized to set primary sales info. - * See {_canSetPrimarySaleRecipient}. - * Emits {PrimarySaleRecipientUpdated Event}; See {_setupPrimarySaleRecipient}. - * - * @param _saleRecipient Address to be set as new recipient of primary sales. - */ - function setPrimarySaleRecipient(address _saleRecipient) external override { - if (!_canSetPrimarySaleRecipient()) { - revert PrimarySaleUnauthorized(); - } - _setupPrimarySaleRecipient(_saleRecipient); - } - - /// @dev Lets a contract admin set the recipient for all primary sales. - function _setupPrimarySaleRecipient(address _saleRecipient) internal { - recipient = _saleRecipient; - emit PrimarySaleRecipientUpdated(_saleRecipient); - } - - /// @dev Returns whether primary sale recipient can be set in the given execution context. - function _canSetPrimarySaleRecipient() internal view virtual returns (bool); -} diff --git a/contracts/legacy-contracts/extension/interface/IClaimCondition_V1.sol b/contracts/legacy-contracts/extension/interface/IClaimCondition_V1.sol deleted file mode 100644 index 7615ded78..000000000 --- a/contracts/legacy-contracts/extension/interface/IClaimCondition_V1.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/// @author thirdweb - -import "../../../lib/BitMaps.sol"; - -/** - * Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. - * - * A contract admin (i.e. a holder of `DEFAULT_ADMIN_ROLE`) can set a series of claim conditions, - * ordered by their respective `startTimestamp`. A claim condition defines criteria under which - * accounts can mint tokens. Claim conditions can be overwritten or added to by the contract admin. - * At any moment, there is only one active claim condition. - */ - -interface IClaimCondition_V1 { - /** - * @notice The criteria that make up a claim condition. - * - * @param startTimestamp The unix timestamp after which the claim condition applies. - * The same claim condition applies until the `startTimestamp` - * of the next claim condition. - * - * @param maxClaimableSupply The maximum total number of tokens that can be claimed under - * the claim condition. - * - * @param supplyClaimed At any given point, the number of tokens that have been claimed - * under the claim condition. - * - * @param quantityLimitPerTransaction The maximum number of tokens that can be claimed in a single - * transaction. - * - * @param waitTimeInSecondsBetweenClaims The least number of seconds an account must wait after claiming - * tokens, to be able to claim tokens again. - * - * @param merkleRoot The allowlist of addresses that can claim tokens under the claim - * condition. - * - * @param pricePerToken The price required to pay per token claimed. - * - * @param currency The currency in which the `pricePerToken` must be paid. - */ - struct ClaimCondition { - uint256 startTimestamp; - uint256 maxClaimableSupply; - uint256 supplyClaimed; - uint256 quantityLimitPerTransaction; - uint256 waitTimeInSecondsBetweenClaims; - bytes32 merkleRoot; - uint256 pricePerToken; - address currency; - } -} diff --git a/contracts/legacy-contracts/extension/interface/IDropSinglePhase1155_V1.sol b/contracts/legacy-contracts/extension/interface/IDropSinglePhase1155_V1.sol deleted file mode 100644 index 0cf271b46..000000000 --- a/contracts/legacy-contracts/extension/interface/IDropSinglePhase1155_V1.sol +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/// @author thirdweb - -import "./IClaimCondition_V1.sol"; - -interface IDropSinglePhase1155_V1 is IClaimCondition_V1 { - struct AllowlistProof { - bytes32[] proof; - uint256 maxQuantityInAllowlist; - } - - /// @dev Emitted when tokens are claimed via `claim`. - event TokensClaimed( - address indexed claimer, - address indexed receiver, - uint256 indexed tokenId, - uint256 quantityClaimed - ); - - /// @dev Emitted when the contract's claim conditions are updated. - event ClaimConditionUpdated(uint256 indexed tokenId, ClaimCondition condition, bool resetEligibility); - - /** - * @notice Lets an account claim a given quantity of NFTs. - * - * @param tokenId The tokenId of the NFT to claim. - * @param receiver The receiver of the NFT to claim. - * @param quantity The quantity of the NFT to claim. - * @param currency The currency in which to pay for the claim. - * @param pricePerToken The price per token to pay for the claim. - * @param allowlistProof The proof of the claimer's inclusion in the merkle root allowlist - * of the claim conditions that apply. - * @param data Arbitrary bytes data that can be leveraged in the implementation of this interface. - */ - function claim( - address receiver, - uint256 tokenId, - uint256 quantity, - address currency, - uint256 pricePerToken, - AllowlistProof calldata allowlistProof, - bytes memory data - ) external payable; - - /** - * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - * - * @param phase Claim condition to set. - * - * @param resetClaimEligibility Whether to reset `limitLastClaimTimestamp` and `limitMerkleProofClaim` values when setting new - * claim conditions. - * - * @param tokenId The tokenId for which to set the relevant claim condition. - */ - function setClaimConditions(uint256 tokenId, ClaimCondition calldata phase, bool resetClaimEligibility) external; -} diff --git a/contracts/legacy-contracts/extension/interface/IDropSinglePhase_V1.sol b/contracts/legacy-contracts/extension/interface/IDropSinglePhase_V1.sol deleted file mode 100644 index 753455b84..000000000 --- a/contracts/legacy-contracts/extension/interface/IDropSinglePhase_V1.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/// @author thirdweb - -import "./IClaimCondition_V1.sol"; - -interface IDropSinglePhase_V1 is IClaimCondition_V1 { - struct AllowlistProof { - bytes32[] proof; - uint256 maxQuantityInAllowlist; - } - - /// @dev Emitted when tokens are claimed via `claim`. - event TokensClaimed( - address indexed claimer, - address indexed receiver, - uint256 indexed startTokenId, - uint256 quantityClaimed - ); - - /// @dev Emitted when the contract's claim conditions are updated. - event ClaimConditionUpdated(ClaimCondition condition, bool resetEligibility); - - /** - * @notice Lets an account claim a given quantity of NFTs. - * - * @param receiver The receiver of the NFTs to claim. - * @param quantity The quantity of NFTs to claim. - * @param currency The currency in which to pay for the claim. - * @param pricePerToken The price per token to pay for the claim. - * @param allowlistProof The proof of the claimer's inclusion in the merkle root allowlist - * of the claim conditions that apply. - * @param data Arbitrary bytes data that can be leveraged in the implementation of this interface. - */ - function claim( - address receiver, - uint256 quantity, - address currency, - uint256 pricePerToken, - AllowlistProof calldata allowlistProof, - bytes memory data - ) external payable; - - /** - * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - * - * @param phase Claim condition to set. - * - * @param resetClaimEligibility Whether to reset `limitLastClaimTimestamp` and `limitMerkleProofClaim` values when setting new - * claim conditions. - */ - function setClaimConditions(ClaimCondition calldata phase, bool resetClaimEligibility) external; -} diff --git a/contracts/legacy-contracts/extension/interface/IPlatformFee_V1.sol b/contracts/legacy-contracts/extension/interface/IPlatformFee_V1.sol deleted file mode 100644 index 28932effa..000000000 --- a/contracts/legacy-contracts/extension/interface/IPlatformFee_V1.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/// @author thirdweb - -/** - * Thirdweb's `PlatformFee` is a contract extension to be used with any base contract. It exposes functions for setting and reading - * the recipient of platform fee and the platform fee basis points, and lets the inheriting contract perform conditional logic - * that uses information about platform fees, if desired. - */ - -interface IPlatformFee { - /// @dev Returns the platform fee bps and recipient. - function getPlatformFeeInfo() external view returns (address, uint16); - - /// @dev Lets a module admin update the fees on primary sales. - function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external; - - /// @dev Emitted when fee on primary sales is updated. - event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); -} diff --git a/contracts/legacy-contracts/extension/interface/IPrimarySale_V1.sol b/contracts/legacy-contracts/extension/interface/IPrimarySale_V1.sol deleted file mode 100644 index 6ca726842..000000000 --- a/contracts/legacy-contracts/extension/interface/IPrimarySale_V1.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/// @author thirdweb - -/** - * Thirdweb's `Primary` is a contract extension to be used with any base contract. It exposes functions for setting and reading - * the recipient of primary sales, and lets the inheriting contract perform conditional logic that uses information about - * primary sales, if desired. - */ - -interface IPrimarySale { - /// @dev The adress that receives all primary sales value. - function primarySaleRecipient() external view returns (address); - - /// @dev Lets a module admin set the default recipient of all primary sales. - function setPrimarySaleRecipient(address _saleRecipient) external; - - /// @dev Emitted when a new sale recipient is set. - event PrimarySaleRecipientUpdated(address indexed recipient); -} diff --git a/contracts/legacy-contracts/interface/ISignatureMintERC721_V1.sol b/contracts/legacy-contracts/interface/ISignatureMintERC721_V1.sol deleted file mode 100644 index e22061e86..000000000 --- a/contracts/legacy-contracts/interface/ISignatureMintERC721_V1.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -/// @author thirdweb - -import "../../prebuilts/interface/token/ITokenERC721.sol"; - -interface ISignatureMintERC721_V1 { - function mintWithSignature( - ITokenERC721.MintRequest calldata _req, - bytes calldata _signature - ) external payable returns (uint256 tokenIdMinted); - - function verify( - ITokenERC721.MintRequest calldata _req, - bytes calldata _signature - ) external view returns (bool, address); -} diff --git a/contracts/legacy-contracts/interface/drop/IDropClaimCondition_V2.sol b/contracts/legacy-contracts/interface/drop/IDropClaimCondition_V2.sol deleted file mode 100644 index 240465a0c..000000000 --- a/contracts/legacy-contracts/interface/drop/IDropClaimCondition_V2.sol +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/// @author thirdweb - -import "@openzeppelin/contracts-upgradeable/utils/structs/BitMapsUpgradeable.sol"; - -/** - * Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. - * - * A contract admin (i.e. a holder of `DEFAULT_ADMIN_ROLE`) can set a series of claim conditions, - * ordered by their respective `startTimestamp`. A claim condition defines criteria under which - * accounts can mint tokens. Claim conditions can be overwritten or added to by the contract admin. - * At any moment, there is only one active claim condition. - */ - -interface IDropClaimCondition_V2 { - /** - * @notice The criteria that make up a claim condition. - * - * @param startTimestamp The unix timestamp after which the claim condition applies. - * The same claim condition applies until the `startTimestamp` - * of the next claim condition. - * - * @param maxClaimableSupply The maximum total number of tokens that can be claimed under - * the claim condition. - * - * @param supplyClaimed At any given point, the number of tokens that have been claimed - * under the claim condition. - * - * @param quantityLimitPerTransaction The maximum number of tokens that can be claimed in a single - * transaction. - * - * @param waitTimeInSecondsBetweenClaims The least number of seconds an account must wait after claiming - * tokens, to be able to claim tokens again. - * - * @param merkleRoot The allowlist of addresses that can claim tokens under the claim - * condition. - * - * @param pricePerToken The price required to pay per token claimed. - * - * @param currency The currency in which the `pricePerToken` must be paid. - */ - struct ClaimCondition { - uint256 startTimestamp; - uint256 maxClaimableSupply; - uint256 supplyClaimed; - uint256 quantityLimitPerTransaction; - uint256 waitTimeInSecondsBetweenClaims; - bytes32 merkleRoot; - uint256 pricePerToken; - address currency; - } - - /** - * @notice The set of all claim conditions, at any given moment. - * Claim Phase ID = [currentStartId, currentStartId + length - 1]; - * - * @param currentStartId The uid for the first claim condition amongst the current set of - * claim conditions. The uid for each next claim condition is one - * more than the previous claim condition's uid. - * - * @param count The total number of phases / claim conditions in the list - * of claim conditions. - * - * @param phases The claim conditions at a given uid. Claim conditions - * are ordered in an ascending order by their `startTimestamp`. - * - * @param limitLastClaimTimestamp Map from an account and uid for a claim condition, to the last timestamp - * at which the account claimed tokens under that claim condition. - * - * @param limitMerkleProofClaim Map from a claim condition uid to whether an address in an allowlist - * has already claimed tokens i.e. used their place in the allowlist. - */ - struct ClaimConditionList { - uint256 currentStartId; - uint256 count; - mapping(uint256 => ClaimCondition) phases; - mapping(uint256 => mapping(address => uint256)) limitLastClaimTimestamp; - mapping(uint256 => BitMapsUpgradeable.BitMap) limitMerkleProofClaim; - } -} diff --git a/contracts/legacy-contracts/interface/drop/IDropERC1155_V2.sol b/contracts/legacy-contracts/interface/drop/IDropERC1155_V2.sol deleted file mode 100644 index 9184580db..000000000 --- a/contracts/legacy-contracts/interface/drop/IDropERC1155_V2.sol +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -/// @author thirdweb - -import "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol"; -import "./IDropClaimCondition_V2.sol"; - -/** - * Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. The - * `DropERC721` contract is a distribution mechanism for ERC721 tokens. - * - * A minter wallet (i.e. holder of `MINTER_ROLE`) can (lazy)mint 'n' tokens - * at once by providing a single base URI for all tokens being lazy minted. - * The URI for each of the 'n' tokens lazy minted is the provided base URI + - * `{tokenId}` of the respective token. (e.g. "ipsf://Qmece.../1"). - * - * A minter can choose to lazy mint 'delayed-reveal' tokens. More on 'delayed-reveal' - * tokens in [this article](https://blog.thirdweb.com/delayed-reveal-nfts). - * - * A contract admin (i.e. holder of `DEFAULT_ADMIN_ROLE`) can create claim conditions - * with non-overlapping time windows, and accounts can claim the tokens according to - * restrictions defined in the claim condition that is active at the time of the transaction. - */ - -interface IDropERC1155_V2 is IERC1155Upgradeable, IDropClaimCondition_V2 { - /// @dev Emitted when tokens are claimed. - event TokensClaimed( - uint256 indexed claimConditionIndex, - uint256 indexed tokenId, - address indexed claimer, - address receiver, - uint256 quantityClaimed - ); - - /// @dev Emitted when tokens are lazy minted. - event TokensLazyMinted(uint256 startTokenId, uint256 endTokenId, string baseURI); - - /// @dev Emitted when new claim conditions are set for a token. - event ClaimConditionsUpdated(uint256 indexed tokenId, ClaimCondition[] claimConditions); - - /// @dev Emitted when the global max supply of a token is updated. - event MaxTotalSupplyUpdated(uint256 tokenId, uint256 maxTotalSupply); - - /// @dev Emitted when the wallet claim count for a given tokenId and address is updated. - event WalletClaimCountUpdated(uint256 tokenId, address indexed wallet, uint256 count); - - /// @dev Emitted when the max wallet claim count for a given tokenId is updated. - event MaxWalletClaimCountUpdated(uint256 tokenId, uint256 count); - - /// @dev Emitted when the sale recipient for a particular tokenId is updated. - event SaleRecipientForTokenUpdated(uint256 indexed tokenId, address saleRecipient); - - /** - * @notice Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. - * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. - * - * @param amount The amount of NFTs to lazy mint. - * @param baseURIForTokens The URI for the NFTs to lazy mint. - */ - function lazyMint(uint256 amount, string calldata baseURIForTokens) external; - - /** - * @notice Lets an account claim a given quantity of NFTs. - * - * @param receiver The receiver of the NFTs to claim. - * @param tokenId The unique ID of the token to claim. - * @param quantity The quantity of NFTs to claim. - * @param currency The currency in which to pay for the claim. - * @param pricePerToken The price per token to pay for the claim. - * @param proofs The proof of the claimer's inclusion in the merkle root allowlist - * of the claim conditions that apply. - * @param proofMaxQuantityPerTransaction (Optional) The maximum number of NFTs an address included in an - * allowlist can claim. - */ - function claim( - address receiver, - uint256 tokenId, - uint256 quantity, - address currency, - uint256 pricePerToken, - bytes32[] calldata proofs, - uint256 proofMaxQuantityPerTransaction - ) external payable; - - /** - * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - * - * @param tokenId The token ID for which to set mint conditions. - * @param phases Claim conditions in ascending order by `startTimestamp`. - * @param resetClaimEligibility Whether to reset `limitLastClaimTimestamp` and - * `limitMerkleProofClaim` values when setting new - * claim conditions. - */ - function setClaimConditions(uint256 tokenId, ClaimCondition[] calldata phases, bool resetClaimEligibility) external; -} diff --git a/contracts/legacy-contracts/interface/drop/IDropERC20_V2.sol b/contracts/legacy-contracts/interface/drop/IDropERC20_V2.sol deleted file mode 100644 index d0b40e526..000000000 --- a/contracts/legacy-contracts/interface/drop/IDropERC20_V2.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -/// @author thirdweb - -import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import "./IDropClaimCondition_V2.sol"; - -/** - * Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. The - * `DropERC20` contract is a distribution mechanism for ERC20 tokens. - * - * A contract admin (i.e. holder of `DEFAULT_ADMIN_ROLE`) can create claim conditions - * with non-overlapping time windows, and accounts can claim the tokens according to - * restrictions defined in the claim condition that is active at the time of the transaction. - */ - -interface IDropERC20_V2 is IERC20Upgradeable, IDropClaimCondition_V2 { - /// @dev Emitted when tokens are claimed. - event TokensClaimed( - uint256 indexed claimConditionIndex, - address indexed claimer, - address indexed receiver, - uint256 quantityClaimed - ); - - /// @dev Emitted when new claim conditions are set. - event ClaimConditionsUpdated(ClaimCondition[] claimConditions); - - /// @dev Emitted when the global max supply of tokens is updated. - event MaxTotalSupplyUpdated(uint256 maxTotalSupply); - - /// @dev Emitted when the wallet claim count for an address is updated. - event WalletClaimCountUpdated(address indexed wallet, uint256 count); - - /// @dev Emitted when the global max wallet claim count is updated. - event MaxWalletClaimCountUpdated(uint256 count); - - /// @dev Emitted when the contract URI is updated. - event ContractURIUpdated(string prevURI, string newURI); - - /** - * @notice Lets an account claim a given quantity of tokens. - * - * @param receiver The receiver of the tokens to claim. - * @param quantity The quantity of tokens to claim. - * @param currency The currency in which to pay for the claim. - * @param pricePerToken The price per token (i.e. price per 1 ether unit of the token) - * to pay for the claim. - * @param proofs The proof of the claimer's inclusion in the merkle root allowlist - * of the claim conditions that apply. - * @param proofMaxQuantityPerTransaction (Optional) The maximum number of tokens an address included in an - * allowlist can claim. - */ - function claim( - address receiver, - uint256 quantity, - address currency, - uint256 pricePerToken, - bytes32[] calldata proofs, - uint256 proofMaxQuantityPerTransaction - ) external payable; - - /** - * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - * - * @param phases Claim conditions in ascending order by `startTimestamp`. - * @param resetClaimEligibility Whether to reset `limitLastClaimTimestamp` and - * `limitMerkleProofClaim` values when setting new - * claim conditions. - */ - function setClaimConditions(ClaimCondition[] calldata phases, bool resetClaimEligibility) external; -} diff --git a/contracts/legacy-contracts/interface/drop/IDropERC721_V3.sol b/contracts/legacy-contracts/interface/drop/IDropERC721_V3.sol deleted file mode 100644 index 7148c2b4d..000000000 --- a/contracts/legacy-contracts/interface/drop/IDropERC721_V3.sol +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -/// @author thirdweb - -import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; -import "./IDropClaimCondition_V2.sol"; - -/** - * Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. The - * `DropERC721` contract is a distribution mechanism for ERC721 tokens. - * - * A minter wallet (i.e. holder of `MINTER_ROLE`) can (lazy)mint 'n' tokens - * at once by providing a single base URI for all tokens being lazy minted. - * The URI for each of the 'n' tokens lazy minted is the provided base URI + - * `{tokenId}` of the respective token. (e.g. "ipsf://Qmece.../1"). - * - * A minter can choose to lazy mint 'delayed-reveal' tokens. More on 'delayed-reveal' - * tokens in [this article](https://blog.thirdweb.com/delayed-reveal-nfts). - * - * A contract admin (i.e. holder of `DEFAULT_ADMIN_ROLE`) can create claim conditions - * with non-overlapping time windows, and accounts can claim the tokens according to - * restrictions defined in the claim condition that is active at the time of the transaction. - */ - -interface IDropERC721_V3 is IERC721Upgradeable, IDropClaimCondition_V2 { - /// @dev Emitted when tokens are claimed. - event TokensClaimed( - uint256 indexed claimConditionIndex, - address indexed claimer, - address indexed receiver, - uint256 startTokenId, - uint256 quantityClaimed - ); - - /// @dev Emitted when tokens are lazy minted. - event TokensLazyMinted(uint256 startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); - - /// @dev Emitted when the URI for a batch of 'delayed-reveal' NFTs is revealed. - event NFTRevealed(uint256 endTokenId, string revealedURI); - - /// @dev Emitted when new claim conditions are set. - event ClaimConditionsUpdated(ClaimCondition[] claimConditions); - - /// @dev Emitted when the global max supply of tokens is updated. - event MaxTotalSupplyUpdated(uint256 maxTotalSupply); - - /// @dev Emitted when the wallet claim count for an address is updated. - event WalletClaimCountUpdated(address indexed wallet, uint256 count); - - /// @dev Emitted when the global max wallet claim count is updated. - event MaxWalletClaimCountUpdated(uint256 count); - - /** - * @notice Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. - * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. - * - * @param amount The amount of NFTs to lazy mint. - * @param baseURIForTokens The URI for the NFTs to lazy mint. If lazy minting - * 'delayed-reveal' NFTs, the is a URI for NFTs in the - * un-revealed state. - * @param encryptedBaseURI If lazy minting 'delayed-reveal' NFTs, this is the - * result of encrypting the URI of the NFTs in the revealed - * state. - */ - function lazyMint(uint256 amount, string calldata baseURIForTokens, bytes calldata encryptedBaseURI) external; - - /** - * @notice Lets an account claim a given quantity of NFTs. - * - * @param receiver The receiver of the NFTs to claim. - * @param quantity The quantity of NFTs to claim. - * @param currency The currency in which to pay for the claim. - * @param pricePerToken The price per token to pay for the claim. - * @param proofs The proof of the claimer's inclusion in the merkle root allowlist - * of the claim conditions that apply. - * @param proofMaxQuantityPerTransaction (Optional) The maximum number of NFTs an address included in an - * allowlist can claim. - */ - function claim( - address receiver, - uint256 quantity, - address currency, - uint256 pricePerToken, - bytes32[] calldata proofs, - uint256 proofMaxQuantityPerTransaction - ) external payable; - - /** - * @notice Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - * - * @param phases Claim conditions in ascending order by `startTimestamp`. - * @param resetClaimEligibility Whether to reset `limitLastClaimTimestamp` and - * `limitMerkleProofClaim` values when setting new - * claim conditions. - */ - function setClaimConditions(ClaimCondition[] calldata phases, bool resetClaimEligibility) external; -} diff --git a/contracts/legacy-contracts/pre-builts/DropERC1155_V2.sol b/contracts/legacy-contracts/pre-builts/DropERC1155_V2.sol deleted file mode 100644 index 2d8f04971..000000000 --- a/contracts/legacy-contracts/pre-builts/DropERC1155_V2.sol +++ /dev/null @@ -1,731 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -/// @author thirdweb - -// ========== External imports ========== - -import "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; - -import "@openzeppelin/contracts-upgradeable/utils/structs/BitMapsUpgradeable.sol"; -import "../../extension/Multicall.sol"; -import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; - -import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; - -// ========== Internal imports ========== - -import "../../infra/interface/IThirdwebContract.sol"; - -// ========== Features ========== - -import "../../extension/interface/IPlatformFee.sol"; -import "../../extension/interface/IPrimarySale.sol"; -import "../../extension/interface/IRoyalty.sol"; -import "../../extension/interface/IOwnable.sol"; - -import { IDropERC1155_V2 } from "../interface/drop/IDropERC1155_V2.sol"; - -import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; - -import "../../lib/CurrencyTransferLib.sol"; -import "../../lib/FeeType.sol"; -import "../../lib/MerkleProof.sol"; - -contract DropERC1155_V2 is - Initializable, - IThirdwebContract, - IOwnable, - IRoyalty, - IPrimarySale, - IPlatformFee, - ReentrancyGuardUpgradeable, - ERC2771ContextUpgradeable, - Multicall, - AccessControlEnumerableUpgradeable, - ERC1155Upgradeable, - IDropERC1155_V2 -{ - using BitMapsUpgradeable for BitMapsUpgradeable.BitMap; - using StringsUpgradeable for uint256; - - /*/////////////////////////////////////////////////////////////// - State variables - //////////////////////////////////////////////////////////////*/ - - bytes32 private constant MODULE_TYPE = bytes32("DropERC1155"); - uint256 private constant VERSION = 2; - - // Token name - string public name; - - // Token symbol - string public symbol; - - /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. - bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); - /// @dev Only MINTER_ROLE holders can lazy mint NFTs. - bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); - - /// @dev Max bps in the thirdweb system - uint256 private constant MAX_BPS = 10_000; - - /// @dev Owner of the contract (purpose: OpenSea compatibility) - address private _owner; - - // @dev The next token ID of the NFT to "lazy mint". - uint256 public nextTokenIdToMint; - - /// @dev The address that receives all primary sales value. - address public primarySaleRecipient; - - /// @dev The address that receives all platform fees from all sales. - address private platformFeeRecipient; - - /// @dev The % of primary sales collected as platform fees. - uint16 private platformFeeBps; - - /// @dev The recipient of who gets the royalty. - address private royaltyRecipient; - - /// @dev The (default) address that receives all royalty value. - uint16 private royaltyBps; - - /// @dev Contract level metadata. - string public contractURI; - - /// @dev Largest tokenId of each batch of tokens with the same baseURI - uint256[] private baseURIIndices; - - /*/////////////////////////////////////////////////////////////// - Mappings - //////////////////////////////////////////////////////////////*/ - - /** - * @dev Mapping from 'Largest tokenId of a batch of tokens with the same baseURI' - * to base URI for the respective batch of tokens. - **/ - mapping(uint256 => string) private baseURI; - - /// @dev Mapping from token ID => total circulating supply of tokens with that ID. - mapping(uint256 => uint256) public totalSupply; - - /// @dev Mapping from token ID => maximum possible total circulating supply of tokens with that ID. - mapping(uint256 => uint256) public maxTotalSupply; - - /// @dev Mapping from token ID => the set of all claim conditions, at any given moment, for tokens of the token ID. - mapping(uint256 => ClaimConditionList) public claimCondition; - - /// @dev Mapping from token ID => the address of the recipient of primary sales. - mapping(uint256 => address) public saleRecipient; - - /// @dev Mapping from token ID => royalty recipient and bps for tokens of the token ID. - mapping(uint256 => RoyaltyInfo) private royaltyInfoForToken; - - /// @dev Mapping from token ID => claimer wallet address => total number of NFTs of the token ID a wallet has claimed. - mapping(uint256 => mapping(address => uint256)) public walletClaimCount; - - /// @dev Mapping from token ID => the max number of NFTs of the token ID a wallet can claim. - mapping(uint256 => uint256) public maxWalletClaimCount; - - /*/////////////////////////////////////////////////////////////// - Constructor + initializer logic - //////////////////////////////////////////////////////////////*/ - - constructor() initializer {} - - /// @dev Initializes the contract, like a constructor. - function initialize( - address _defaultAdmin, - string memory _name, - string memory _symbol, - string memory _contractURI, - address[] memory _trustedForwarders, - address _saleRecipient, - address _royaltyRecipient, - uint128 _royaltyBps, - uint128 _platformFeeBps, - address _platformFeeRecipient - ) external initializer { - // Initialize inherited contracts, most base-like -> most derived. - __ReentrancyGuard_init(); - __ERC2771Context_init_unchained(_trustedForwarders); - __ERC1155_init_unchained(""); - - // Initialize this contract's state. - name = _name; - symbol = _symbol; - royaltyRecipient = _royaltyRecipient; - royaltyBps = uint16(_royaltyBps); - platformFeeRecipient = _platformFeeRecipient; - primarySaleRecipient = _saleRecipient; - contractURI = _contractURI; - platformFeeBps = uint16(_platformFeeBps); - _owner = _defaultAdmin; - - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setupRole(MINTER_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, address(0)); - } - - /*/////////////////////////////////////////////////////////////// - Generic contract logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the type of the contract. - function contractType() external pure returns (bytes32) { - return MODULE_TYPE; - } - - /// @dev Returns the version of the contract. - function contractVersion() external pure returns (uint8) { - return uint8(VERSION); - } - - /** - * @dev Returns the address of the current owner. - */ - function owner() public view returns (address) { - return hasRole(DEFAULT_ADMIN_ROLE, _owner) ? _owner : address(0); - } - - /*/////////////////////////////////////////////////////////////// - ERC 165 / 1155 / 2981 logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the URI for a given tokenId. - function uri(uint256 _tokenId) public view override returns (string memory _tokenURI) { - for (uint256 i = 0; i < baseURIIndices.length; i += 1) { - if (_tokenId < baseURIIndices[i]) { - return string(abi.encodePacked(baseURI[baseURIIndices[i]], _tokenId.toString())); - } - } - - return ""; - } - - /// @dev See ERC 165 - function supportsInterface( - bytes4 interfaceId - ) - public - view - virtual - override(ERC1155Upgradeable, AccessControlEnumerableUpgradeable, IERC165Upgradeable, IERC165) - returns (bool) - { - return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; - } - - /// @dev Returns the royalty recipient and amount, given a tokenId and sale price. - function royaltyInfo( - uint256 tokenId, - uint256 salePrice - ) external view virtual returns (address receiver, uint256 royaltyAmount) { - (address recipient, uint256 bps) = getRoyaltyInfoForToken(tokenId); - receiver = recipient; - royaltyAmount = (salePrice * bps) / MAX_BPS; - } - - /*/////////////////////////////////////////////////////////////// - Minting logic - //////////////////////////////////////////////////////////////*/ - - /** - * @dev Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. - * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. - */ - function lazyMint(uint256 _amount, string calldata _baseURIForTokens) external onlyRole(MINTER_ROLE) { - uint256 startId = nextTokenIdToMint; - uint256 baseURIIndex = startId + _amount; - - nextTokenIdToMint = baseURIIndex; - baseURI[baseURIIndex] = _baseURIForTokens; - baseURIIndices.push(baseURIIndex); - - emit TokensLazyMinted(startId, startId + _amount - 1, _baseURIForTokens); - } - - /*/////////////////////////////////////////////////////////////// - Claim logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets an account claim a given quantity of NFTs, of a single tokenId. - function claim( - address _receiver, - uint256 _tokenId, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - bytes32[] calldata _proofs, - uint256 _proofMaxQuantityPerTransaction - ) external payable nonReentrant { - require(isTrustedForwarder(msg.sender) || _msgSender() == tx.origin, "BOT"); - - // Get the active claim condition index. - uint256 activeConditionId = getActiveClaimConditionId(_tokenId); - - /** - * We make allowlist checks (i.e. verifyClaimMerkleProof) before verifying the claim's general - * validity (i.e. verifyClaim) because we give precedence to the check of allow list quantity - * restriction over the check of the general claim condition's quantityLimitPerTransaction - * restriction. - */ - - // Verify inclusion in allowlist. - (bool validMerkleProof, ) = verifyClaimMerkleProof( - activeConditionId, - _msgSender(), - _tokenId, - _quantity, - _proofs, - _proofMaxQuantityPerTransaction - ); - - // Verify claim validity. If not valid, revert. - // when there's allowlist present --> verifyClaimMerkleProof will verify the _proofMaxQuantityPerTransaction value with hashed leaf in the allowlist - // when there's no allowlist, this check is true --> verifyClaim will check for _quantity being less/equal than the limit - bool toVerifyMaxQuantityPerTransaction = _proofMaxQuantityPerTransaction == 0 || - claimCondition[_tokenId].phases[activeConditionId].merkleRoot == bytes32(0); - verifyClaim( - activeConditionId, - _msgSender(), - _tokenId, - _quantity, - _currency, - _pricePerToken, - toVerifyMaxQuantityPerTransaction - ); - - if (validMerkleProof && _proofMaxQuantityPerTransaction > 0) { - /** - * Mark the claimer's use of their position in the allowlist. A spot in an allowlist - * can be used only once. - */ - claimCondition[_tokenId].limitMerkleProofClaim[activeConditionId].set(uint256(uint160(_msgSender()))); - } - - // If there's a price, collect price. - collectClaimPrice(_quantity, _currency, _pricePerToken, _tokenId); - - // Mint the relevant tokens to claimer. - transferClaimedTokens(_receiver, activeConditionId, _tokenId, _quantity); - - emit TokensClaimed(activeConditionId, _tokenId, _msgSender(), _receiver, _quantity); - } - - /// @dev Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions, for a tokenId. - function setClaimConditions( - uint256 _tokenId, - ClaimCondition[] calldata _phases, - bool _resetClaimEligibility - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - ClaimConditionList storage condition = claimCondition[_tokenId]; - uint256 existingStartIndex = condition.currentStartId; - uint256 existingPhaseCount = condition.count; - - /** - * `limitLastClaimTimestamp` and `limitMerkleProofClaim` are mappings that use a - * claim condition's UID as a key. - * - * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim - * conditions in `_phases`, effectively resetting the restrictions on claims expressed - * by `limitLastClaimTimestamp` and `limitMerkleProofClaim`. - */ - uint256 newStartIndex = existingStartIndex; - if (_resetClaimEligibility) { - newStartIndex = existingStartIndex + existingPhaseCount; - } - - condition.count = _phases.length; - condition.currentStartId = newStartIndex; - - uint256 lastConditionStartTimestamp; - for (uint256 i = 0; i < _phases.length; i++) { - require( - i == 0 || lastConditionStartTimestamp < _phases[i].startTimestamp, - "startTimestamp must be in ascending order." - ); - - uint256 supplyClaimedAlready = condition.phases[newStartIndex + i].supplyClaimed; - require(supplyClaimedAlready <= _phases[i].maxClaimableSupply, "max supply claimed already"); - - condition.phases[newStartIndex + i] = _phases[i]; - condition.phases[newStartIndex + i].supplyClaimed = supplyClaimedAlready; - - lastConditionStartTimestamp = _phases[i].startTimestamp; - } - - /** - * Gas refunds (as much as possible) - * - * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim - * conditions in `_phases`. So, we delete claim conditions with UID < `newStartIndex`. - * - * If `_resetClaimEligibility == false`, and there are more existing claim conditions - * than in `_phases`, we delete the existing claim conditions that don't get replaced - * by the conditions in `_phases`. - */ - if (_resetClaimEligibility) { - for (uint256 i = existingStartIndex; i < newStartIndex; i++) { - delete condition.phases[i]; - delete condition.limitMerkleProofClaim[i]; - } - } else { - if (existingPhaseCount > _phases.length) { - for (uint256 i = _phases.length; i < existingPhaseCount; i++) { - delete condition.phases[newStartIndex + i]; - delete condition.limitMerkleProofClaim[newStartIndex + i]; - } - } - } - - emit ClaimConditionsUpdated(_tokenId, _phases); - } - - /// @dev Collects and distributes the primary sale value of NFTs being claimed. - function collectClaimPrice( - uint256 _quantityToClaim, - address _currency, - uint256 _pricePerToken, - uint256 _tokenId - ) internal { - if (_pricePerToken == 0) { - return; - } - - uint256 totalPrice = _quantityToClaim * _pricePerToken; - uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; - - if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { - require(msg.value == totalPrice, "must send total price."); - } - - address recipient = saleRecipient[_tokenId] == address(0) ? primarySaleRecipient : saleRecipient[_tokenId]; - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), recipient, totalPrice - platformFees); - } - - /// @dev Transfers the NFTs being claimed. - function transferClaimedTokens( - address _to, - uint256 _conditionId, - uint256 _tokenId, - uint256 _quantityBeingClaimed - ) internal { - // Update the supply minted under mint condition. - claimCondition[_tokenId].phases[_conditionId].supplyClaimed += _quantityBeingClaimed; - - // if transfer claimed tokens is called when to != msg.sender, it'd use msg.sender's limits. - // behavior would be similar to msg.sender mint for itself, then transfer to `to`. - claimCondition[_tokenId].limitLastClaimTimestamp[_conditionId][_msgSender()] = block.timestamp; - - walletClaimCount[_tokenId][_msgSender()] += _quantityBeingClaimed; - - _mint(_to, _tokenId, _quantityBeingClaimed, ""); - } - - /// @dev Checks a request to claim NFTs against the active claim condition's criteria. - function verifyClaim( - uint256 _conditionId, - address _claimer, - uint256 _tokenId, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - bool verifyMaxQuantityPerTransaction - ) public view { - ClaimCondition memory currentClaimPhase = claimCondition[_tokenId].phases[_conditionId]; - - require( - _currency == currentClaimPhase.currency && _pricePerToken == currentClaimPhase.pricePerToken, - "invalid currency or price specified." - ); - // If we're checking for an allowlist quantity restriction, ignore the general quantity restriction. - require( - _quantity > 0 && - (!verifyMaxQuantityPerTransaction || _quantity <= currentClaimPhase.quantityLimitPerTransaction), - "invalid quantity claimed." - ); - require( - currentClaimPhase.supplyClaimed + _quantity <= currentClaimPhase.maxClaimableSupply, - "exceed max mint supply." - ); - require( - maxTotalSupply[_tokenId] == 0 || totalSupply[_tokenId] + _quantity <= maxTotalSupply[_tokenId], - "exceed max total supply" - ); - require( - maxWalletClaimCount[_tokenId] == 0 || - walletClaimCount[_tokenId][_claimer] + _quantity <= maxWalletClaimCount[_tokenId], - "exceed claim limit for wallet" - ); - - (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) = getClaimTimestamp( - _tokenId, - _conditionId, - _claimer - ); - require(lastClaimTimestamp == 0 || block.timestamp >= nextValidClaimTimestamp, "cannot claim yet."); - } - - /// @dev Checks whether a claimer meets the claim condition's allowlist criteria. - function verifyClaimMerkleProof( - uint256 _conditionId, - address _claimer, - uint256 _tokenId, - uint256 _quantity, - bytes32[] calldata _proofs, - uint256 _proofMaxQuantityPerTransaction - ) public view returns (bool validMerkleProof, uint256 merkleProofIndex) { - ClaimCondition memory currentClaimPhase = claimCondition[_tokenId].phases[_conditionId]; - - if (currentClaimPhase.merkleRoot != bytes32(0)) { - (validMerkleProof, merkleProofIndex) = MerkleProof.verify( - _proofs, - currentClaimPhase.merkleRoot, - keccak256(abi.encodePacked(_claimer, _proofMaxQuantityPerTransaction)) - ); - require(validMerkleProof, "not in whitelist."); - require( - !claimCondition[_tokenId].limitMerkleProofClaim[_conditionId].get(uint256(uint160(_claimer))), - "proof claimed." - ); - require( - _proofMaxQuantityPerTransaction == 0 || _quantity <= _proofMaxQuantityPerTransaction, - "invalid quantity proof." - ); - } - } - - /*/////////////////////////////////////////////////////////////// - Getter functions - //////////////////////////////////////////////////////////////*/ - - /// @dev At any given moment, returns the uid for the active claim condition, for a given tokenId. - function getActiveClaimConditionId(uint256 _tokenId) public view returns (uint256) { - ClaimConditionList storage conditionList = claimCondition[_tokenId]; - for (uint256 i = conditionList.currentStartId + conditionList.count; i > conditionList.currentStartId; i--) { - if (block.timestamp >= conditionList.phases[i - 1].startTimestamp) { - return i - 1; - } - } - - revert("no active mint condition."); - } - - /// @dev Returns the platform fee recipient and bps. - function getPlatformFeeInfo() external view returns (address, uint16) { - return (platformFeeRecipient, uint16(platformFeeBps)); - } - - /// @dev Returns the default royalty recipient and bps. - function getDefaultRoyaltyInfo() external view returns (address, uint16) { - return (royaltyRecipient, uint16(royaltyBps)); - } - - /// @dev Returns the royalty recipient and bps for a particular token Id. - function getRoyaltyInfoForToken(uint256 _tokenId) public view returns (address, uint16) { - RoyaltyInfo memory royaltyForToken = royaltyInfoForToken[_tokenId]; - - return - royaltyForToken.recipient == address(0) - ? (royaltyRecipient, uint16(royaltyBps)) - : (royaltyForToken.recipient, uint16(royaltyForToken.bps)); - } - - /// @dev Returns the timestamp for when a claimer is eligible for claiming NFTs again. - function getClaimTimestamp( - uint256 _tokenId, - uint256 _conditionId, - address _claimer - ) public view returns (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) { - lastClaimTimestamp = claimCondition[_tokenId].limitLastClaimTimestamp[_conditionId][_claimer]; - - unchecked { - nextValidClaimTimestamp = - lastClaimTimestamp + - claimCondition[_tokenId].phases[_conditionId].waitTimeInSecondsBetweenClaims; - - if (nextValidClaimTimestamp < lastClaimTimestamp) { - nextValidClaimTimestamp = type(uint256).max; - } - } - } - - /// @dev Returns the claim condition at the given uid. - function getClaimConditionById( - uint256 _tokenId, - uint256 _conditionId - ) external view returns (ClaimCondition memory condition) { - condition = claimCondition[_tokenId].phases[_conditionId]; - } - - /*/////////////////////////////////////////////////////////////// - Setter functions - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets a contract admin set a claim count for a wallet. - function setWalletClaimCount( - uint256 _tokenId, - address _claimer, - uint256 _count - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - walletClaimCount[_tokenId][_claimer] = _count; - emit WalletClaimCountUpdated(_tokenId, _claimer, _count); - } - - /// @dev Lets a contract admin set a maximum number of NFTs of a tokenId that can be claimed by any wallet. - function setMaxWalletClaimCount(uint256 _tokenId, uint256 _count) external onlyRole(DEFAULT_ADMIN_ROLE) { - maxWalletClaimCount[_tokenId] = _count; - emit MaxWalletClaimCountUpdated(_tokenId, _count); - } - - /// @dev Lets a module admin set a max total supply for token. - function setMaxTotalSupply(uint256 _tokenId, uint256 _maxTotalSupply) external onlyRole(DEFAULT_ADMIN_ROLE) { - maxTotalSupply[_tokenId] = _maxTotalSupply; - emit MaxTotalSupplyUpdated(_tokenId, _maxTotalSupply); - } - - /// @dev Lets a contract admin set the recipient for all primary sales. - function setPrimarySaleRecipient(address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { - primarySaleRecipient = _saleRecipient; - emit PrimarySaleRecipientUpdated(_saleRecipient); - } - - /// @dev Lets a contract admin set the recipient for all primary sales. - function setSaleRecipientForToken(uint256 _tokenId, address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { - saleRecipient[_tokenId] = _saleRecipient; - emit SaleRecipientForTokenUpdated(_tokenId, _saleRecipient); - } - - /// @dev Lets a contract admin update the default royalty recipient and bps. - function setDefaultRoyaltyInfo( - address _royaltyRecipient, - uint256 _royaltyBps - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(_royaltyBps <= MAX_BPS, "exceed royalty bps"); - - royaltyRecipient = _royaltyRecipient; - royaltyBps = uint16(_royaltyBps); - - emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); - } - - /// @dev Lets a contract admin set the royalty recipient and bps for a particular token Id. - function setRoyaltyInfoForToken( - uint256 _tokenId, - address _recipient, - uint256 _bps - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(_bps <= MAX_BPS, "exceed royalty bps"); - - royaltyInfoForToken[_tokenId] = RoyaltyInfo({ recipient: _recipient, bps: _bps }); - - emit RoyaltyForToken(_tokenId, _recipient, _bps); - } - - /// @dev Lets a contract admin update the platform fee recipient and bps - function setPlatformFeeInfo( - address _platformFeeRecipient, - uint256 _platformFeeBps - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(_platformFeeBps <= MAX_BPS, "bps <= 10000."); - - platformFeeBps = uint16(_platformFeeBps); - platformFeeRecipient = _platformFeeRecipient; - - emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); - } - - /// @dev Lets a contract admin set a new owner for the contract. The new owner must be a contract admin. - function setOwner(address _newOwner) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(hasRole(DEFAULT_ADMIN_ROLE, _newOwner), "new owner not module admin."); - emit OwnerUpdated(_owner, _newOwner); - _owner = _newOwner; - } - - /// @dev Lets a contract admin set the URI for contract-level metadata. - function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { - contractURI = _uri; - } - - /*/////////////////////////////////////////////////////////////// - Miscellaneous - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets a token owner burn the tokens they own (i.e. destroy for good) - function burn(address account, uint256 id, uint256 value) public virtual { - require( - account == _msgSender() || isApprovedForAll(account, _msgSender()), - "ERC1155: caller is not owner nor approved." - ); - - _burn(account, id, value); - } - - /// @dev Lets a token owner burn multiple tokens they own at once (i.e. destroy for good) - function burnBatch(address account, uint256[] memory ids, uint256[] memory values) public virtual { - require( - account == _msgSender() || isApprovedForAll(account, _msgSender()), - "ERC1155: caller is not owner nor approved." - ); - - _burnBatch(account, ids, values); - } - - /** - * @dev See {ERC1155-_beforeTokenTransfer}. - */ - function _beforeTokenTransfer( - address operator, - address from, - address to, - uint256[] memory ids, - uint256[] memory amounts, - bytes memory data - ) internal virtual override { - super._beforeTokenTransfer(operator, from, to, ids, amounts, data); - - // if transfer is restricted on the contract, we still want to allow burning and minting - if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { - require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "restricted to TRANSFER_ROLE holders."); - } - - if (from == address(0)) { - for (uint256 i = 0; i < ids.length; ++i) { - totalSupply[ids[i]] += amounts[i]; - } - } - - if (to == address(0)) { - for (uint256 i = 0; i < ids.length; ++i) { - totalSupply[ids[i]] -= amounts[i]; - } - } - } - - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } -} diff --git a/contracts/legacy-contracts/pre-builts/DropERC20_V2.sol b/contracts/legacy-contracts/pre-builts/DropERC20_V2.sol deleted file mode 100644 index 72310658a..000000000 --- a/contracts/legacy-contracts/pre-builts/DropERC20_V2.sol +++ /dev/null @@ -1,521 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -/// @author thirdweb - -// ========== External imports ========== - -import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; - -import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; - -import "@openzeppelin/contracts-upgradeable/utils/structs/BitMapsUpgradeable.sol"; -import "../../extension/Multicall.sol"; - -// ========== Internal imports ========== - -import "../../infra/interface/IThirdwebContract.sol"; - -// ========== Features ========== - -import "../../extension/interface/IPlatformFee.sol"; -import "../../extension/interface/IPrimarySale.sol"; - -import { IDropERC20_V2 } from "../interface/drop/IDropERC20_V2.sol"; - -import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; - -import "../../lib/MerkleProof.sol"; -import "../../lib/CurrencyTransferLib.sol"; -import "../../lib/FeeType.sol"; - -contract DropERC20_V2 is - Initializable, - IThirdwebContract, - IPrimarySale, - IPlatformFee, - ReentrancyGuardUpgradeable, - ERC2771ContextUpgradeable, - Multicall, - AccessControlEnumerableUpgradeable, - ERC20BurnableUpgradeable, - ERC20VotesUpgradeable, - IDropERC20_V2 -{ - using BitMapsUpgradeable for BitMapsUpgradeable.BitMap; - - /*/////////////////////////////////////////////////////////////// - State variables - //////////////////////////////////////////////////////////////*/ - - bytes32 private constant MODULE_TYPE = bytes32("DropERC20"); - uint128 private constant VERSION = 2; - - /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. - bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); - - /// @dev Contract level metadata. - string public contractURI; - - /// @dev Max bps in the thirdweb system. - uint128 internal constant MAX_BPS = 10_000; - - /// @dev The % of primary sales collected as platform fees. - uint128 internal platformFeeBps; - - /// @dev The address that receives all platform fees from all sales. - address internal platformFeeRecipient; - - /// @dev The address that receives all primary sales value. - address public primarySaleRecipient; - - /// @dev The max number of tokens a wallet can claim. - uint256 public maxWalletClaimCount; - - /// @dev Global max total supply of tokens. - uint256 public maxTotalSupply; - - /// @dev The set of all claim conditions, at any given moment. - ClaimConditionList public claimCondition; - - /*/////////////////////////////////////////////////////////////// - Mappings - //////////////////////////////////////////////////////////////*/ - - /// @dev Mapping from address => number of tokens a wallet has claimed. - mapping(address => uint256) public walletClaimCount; - - /*/////////////////////////////////////////////////////////////// - Constructor + initializer logic - //////////////////////////////////////////////////////////////*/ - - constructor() initializer {} - - /// @dev Initializes the contract, like a constructor. - function initialize( - address _defaultAdmin, - string memory _name, - string memory _symbol, - string memory _contractURI, - address[] memory _trustedForwarders, - address _primarySaleRecipient, - address _platformFeeRecipient, - uint256 _platformFeeBps - ) external initializer { - // Initialize inherited contracts, most base-like -> most derived. - __ERC2771Context_init_unchained(_trustedForwarders); - __ERC20Permit_init(_name); - __ERC20_init_unchained(_name, _symbol); - - // Initialize this contract's state. - contractURI = _contractURI; - primarySaleRecipient = _primarySaleRecipient; - platformFeeRecipient = _platformFeeRecipient; - platformFeeBps = uint128(_platformFeeBps); - - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, address(0)); - } - - /*/////////////////////////////////////////////////////////////// - Generic contract logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the type of the contract. - function contractType() external pure returns (bytes32) { - return MODULE_TYPE; - } - - /// @dev Returns the version of the contract. - function contractVersion() external pure returns (uint8) { - return uint8(VERSION); - } - - /*/////////////////////////////////////////////////////////////// - ERC 165 + ERC20 transfer hooks - //////////////////////////////////////////////////////////////*/ - - /// @dev See ERC 165 - function supportsInterface( - bytes4 interfaceId - ) public view virtual override(AccessControlEnumerableUpgradeable) returns (bool) { - return super.supportsInterface(interfaceId); - } - - function _afterTokenTransfer( - address from, - address to, - uint256 amount - ) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { - super._afterTokenTransfer(from, to, amount); - } - - /// @dev Runs on every transfer. - function _beforeTokenTransfer(address from, address to, uint256 amount) internal override(ERC20Upgradeable) { - super._beforeTokenTransfer(from, to, amount); - - if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { - require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "transfers restricted."); - } - } - - /*/////////////////////////////////////////////////////////////// - Claim logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets an account claim tokens. - function claim( - address _receiver, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - bytes32[] calldata _proofs, - uint256 _proofMaxQuantityPerTransaction - ) external payable nonReentrant { - require(isTrustedForwarder(msg.sender) || _msgSender() == tx.origin, "BOT"); - - // Get the claim conditions. - uint256 activeConditionId = getActiveClaimConditionId(); - - /** - * We make allowlist checks (i.e. verifyClaimMerkleProof) before verifying the claim's general - * validity (i.e. verifyClaim) because we give precedence to the check of allow list quantity - * restriction over the check of the general claim condition's quantityLimitPerTransaction - * restriction. - */ - - // Verify inclusion in allowlist. - (bool validMerkleProof, ) = verifyClaimMerkleProof( - activeConditionId, - _msgSender(), - _quantity, - _proofs, - _proofMaxQuantityPerTransaction - ); - - // Verify claim validity. If not valid, revert. - // when there's allowlist present --> verifyClaimMerkleProof will verify the _proofMaxQuantityPerTransaction value with hashed leaf in the allowlist - // when there's no allowlist, this check is true --> verifyClaim will check for _quantity being less/equal than the limit - bool toVerifyMaxQuantityPerTransaction = _proofMaxQuantityPerTransaction == 0 || - claimCondition.phases[activeConditionId].merkleRoot == bytes32(0); - verifyClaim( - activeConditionId, - _msgSender(), - _quantity, - _currency, - _pricePerToken, - toVerifyMaxQuantityPerTransaction - ); - - if (validMerkleProof && _proofMaxQuantityPerTransaction > 0) { - /** - * Mark the claimer's use of their position in the allowlist. A spot in an allowlist - * can be used only once. - */ - claimCondition.limitMerkleProofClaim[activeConditionId].set(uint256(uint160(_msgSender()))); - } - - // If there's a price, collect price. - collectClaimPrice(_quantity, _currency, _pricePerToken); - - // Mint the relevant NFTs to claimer. - transferClaimedTokens(_receiver, activeConditionId, _quantity); - - emit TokensClaimed(activeConditionId, _msgSender(), _receiver, _quantity); - } - - /// @dev Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - function setClaimConditions( - ClaimCondition[] calldata _phases, - bool _resetClaimEligibility - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - uint256 existingStartIndex = claimCondition.currentStartId; - uint256 existingPhaseCount = claimCondition.count; - - /** - * `limitLastClaimTimestamp` and `limitMerkleProofClaim` are mappings that use a - * claim condition's UID as a key. - * - * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim - * conditions in `_phases`, effectively resetting the restrictions on claims expressed - * by `limitLastClaimTimestamp` and `limitMerkleProofClaim`. - */ - uint256 newStartIndex = existingStartIndex; - if (_resetClaimEligibility) { - newStartIndex = existingStartIndex + existingPhaseCount; - } - - claimCondition.count = _phases.length; - claimCondition.currentStartId = newStartIndex; - - uint256 lastConditionStartTimestamp; - for (uint256 i = 0; i < _phases.length; i++) { - require( - i == 0 || lastConditionStartTimestamp < _phases[i].startTimestamp, - "startTimestamp must be in ascending order." - ); - - uint256 supplyClaimedAlready = claimCondition.phases[newStartIndex + i].supplyClaimed; - require(supplyClaimedAlready <= _phases[i].maxClaimableSupply, "max supply claimed already"); - - claimCondition.phases[newStartIndex + i] = _phases[i]; - claimCondition.phases[newStartIndex + i].supplyClaimed = supplyClaimedAlready; - - lastConditionStartTimestamp = _phases[i].startTimestamp; - } - - /** - * Gas refunds (as much as possible) - * - * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim - * conditions in `_phases`. So, we delete claim conditions with UID < `newStartIndex`. - * - * If `_resetClaimEligibility == false`, and there are more existing claim conditions - * than in `_phases`, we delete the existing claim conditions that don't get replaced - * by the conditions in `_phases`. - */ - if (_resetClaimEligibility) { - for (uint256 i = existingStartIndex; i < newStartIndex; i++) { - delete claimCondition.phases[i]; - delete claimCondition.limitMerkleProofClaim[i]; - } - } else { - if (existingPhaseCount > _phases.length) { - for (uint256 i = _phases.length; i < existingPhaseCount; i++) { - delete claimCondition.phases[newStartIndex + i]; - delete claimCondition.limitMerkleProofClaim[newStartIndex + i]; - } - } - } - - emit ClaimConditionsUpdated(_phases); - } - - /// @dev Collects and distributes the primary sale value of tokens being claimed. - function collectClaimPrice(uint256 _quantityToClaim, address _currency, uint256 _pricePerToken) internal { - if (_pricePerToken == 0) { - return; - } - - // `_pricePerToken` is interpreted as price per 1 ether unit of the ERC20 tokens. - uint256 totalPrice = (_quantityToClaim * _pricePerToken) / 1 ether; - require(totalPrice > 0, "quantity too low"); - - uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; - - if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { - require(msg.value == totalPrice, "must send total price."); - } - - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), primarySaleRecipient, totalPrice - platformFees); - } - - /// @dev Transfers the tokens being claimed. - function transferClaimedTokens(address _to, uint256 _conditionId, uint256 _quantityBeingClaimed) internal { - // Update the supply minted under mint condition. - claimCondition.phases[_conditionId].supplyClaimed += _quantityBeingClaimed; - - // if transfer claimed tokens is called when to != msg.sender, it'd use msg.sender's limits. - // behavior would be similar to msg.sender mint for itself, then transfer to `to`. - claimCondition.limitLastClaimTimestamp[_conditionId][_msgSender()] = block.timestamp; - walletClaimCount[_msgSender()] += _quantityBeingClaimed; - - _mint(_to, _quantityBeingClaimed); - } - - /// @dev Checks a request to claim tokens against the active claim condition's criteria. - function verifyClaim( - uint256 _conditionId, - address _claimer, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - bool verifyMaxQuantityPerTransaction - ) public view { - ClaimCondition memory currentClaimPhase = claimCondition.phases[_conditionId]; - - require( - _currency == currentClaimPhase.currency && _pricePerToken == currentClaimPhase.pricePerToken, - "invalid currency or price specified." - ); - // If we're checking for an allowlist quantity restriction, ignore the general quantity restriction. - require( - _quantity > 0 && - (!verifyMaxQuantityPerTransaction || _quantity <= currentClaimPhase.quantityLimitPerTransaction), - "invalid quantity claimed." - ); - require( - currentClaimPhase.supplyClaimed + _quantity <= currentClaimPhase.maxClaimableSupply, - "exceed max mint supply." - ); - - uint256 _maxTotalSupply = maxTotalSupply; - uint256 _maxWalletClaimCount = maxWalletClaimCount; - require(_maxTotalSupply == 0 || totalSupply() + _quantity <= _maxTotalSupply, "exceed max total supply."); - require( - _maxWalletClaimCount == 0 || walletClaimCount[_claimer] + _quantity <= _maxWalletClaimCount, - "exceed claim limit for wallet" - ); - - (, uint256 nextValidClaimTimestamp) = getClaimTimestamp(_conditionId, _claimer); - require(block.timestamp >= nextValidClaimTimestamp, "cannot claim yet."); - } - - /// @dev Checks whether a claimer meets the claim condition's allowlist criteria. - function verifyClaimMerkleProof( - uint256 _conditionId, - address _claimer, - uint256 _quantity, - bytes32[] calldata _proofs, - uint256 _proofMaxQuantityPerTransaction - ) public view returns (bool validMerkleProof, uint256 merkleProofIndex) { - ClaimCondition memory currentClaimPhase = claimCondition.phases[_conditionId]; - - if (currentClaimPhase.merkleRoot != bytes32(0)) { - (validMerkleProof, merkleProofIndex) = MerkleProof.verify( - _proofs, - currentClaimPhase.merkleRoot, - keccak256(abi.encodePacked(_claimer, _proofMaxQuantityPerTransaction)) - ); - require(validMerkleProof, "not in whitelist."); - require( - !claimCondition.limitMerkleProofClaim[_conditionId].get(uint256(uint160(_claimer))), - "proof claimed." - ); - require( - _proofMaxQuantityPerTransaction == 0 || _quantity <= _proofMaxQuantityPerTransaction, - "invalid quantity proof." - ); - } - } - - /*/////////////////////////////////////////////////////////////// - Getter functions - //////////////////////////////////////////////////////////////*/ - - /// @dev At any given moment, returns the uid for the active claim condition. - function getActiveClaimConditionId() public view returns (uint256) { - for (uint256 i = claimCondition.currentStartId + claimCondition.count; i > claimCondition.currentStartId; i--) { - if (block.timestamp >= claimCondition.phases[i - 1].startTimestamp) { - return i - 1; - } - } - - revert("no active mint condition."); - } - - /// @dev Returns the timestamp for when a claimer is eligible for claiming tokens again. - function getClaimTimestamp( - uint256 _conditionId, - address _claimer - ) public view returns (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) { - lastClaimTimestamp = claimCondition.limitLastClaimTimestamp[_conditionId][_claimer]; - - if (lastClaimTimestamp != 0) { - unchecked { - nextValidClaimTimestamp = - lastClaimTimestamp + - claimCondition.phases[_conditionId].waitTimeInSecondsBetweenClaims; - - if (nextValidClaimTimestamp < lastClaimTimestamp) { - nextValidClaimTimestamp = type(uint256).max; - } - } - } - } - - /// @dev Returns the claim condition at the given uid. - function getClaimConditionById(uint256 _conditionId) external view returns (ClaimCondition memory condition) { - condition = claimCondition.phases[_conditionId]; - } - - /// @dev Returns the platform fee recipient and bps. - function getPlatformFeeInfo() external view returns (address, uint16) { - return (platformFeeRecipient, uint16(platformFeeBps)); - } - - /*/////////////////////////////////////////////////////////////// - Setter functions - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets a contract admin set a claim count for a wallet. - function setWalletClaimCount(address _claimer, uint256 _count) external onlyRole(DEFAULT_ADMIN_ROLE) { - walletClaimCount[_claimer] = _count; - emit WalletClaimCountUpdated(_claimer, _count); - } - - /// @dev Set a maximum number of tokens that can be claimed by any wallet. Must be parsed to 18 decimals when setting, by adding 18 zeros after the desired value. - function setMaxWalletClaimCount(uint256 _count) external onlyRole(DEFAULT_ADMIN_ROLE) { - maxWalletClaimCount = _count; - emit MaxWalletClaimCountUpdated(_count); - } - - /// @dev Set global maximum supply. Must be parsed to 18 decimals when setting, by adding 18 zeros after the desired value. - function setMaxTotalSupply(uint256 _maxTotalSupply) external onlyRole(DEFAULT_ADMIN_ROLE) { - maxTotalSupply = _maxTotalSupply; - emit MaxTotalSupplyUpdated(_maxTotalSupply); - } - - /// @dev Lets a contract admin set the recipient for all primary sales. - function setPrimarySaleRecipient(address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { - primarySaleRecipient = _saleRecipient; - emit PrimarySaleRecipientUpdated(_saleRecipient); - } - - /// @dev Lets a contract admin update the platform fee recipient and bps - function setPlatformFeeInfo( - address _platformFeeRecipient, - uint256 _platformFeeBps - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(_platformFeeBps <= MAX_BPS, "bps <= 10000."); - - platformFeeBps = uint64(_platformFeeBps); - platformFeeRecipient = _platformFeeRecipient; - - emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); - } - - /// @dev Lets a contract admin set the URI for contract-level metadata. - function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { - string memory prevURI = contractURI; - contractURI = _uri; - - emit ContractURIUpdated(prevURI, _uri); - } - - /*/////////////////////////////////////////////////////////////// - Miscellaneous - //////////////////////////////////////////////////////////////*/ - - function _mint(address account, uint256 amount) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { - super._mint(account, amount); - } - - function _burn(address account, uint256 amount) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { - super._burn(account, amount); - } - - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } -} diff --git a/contracts/legacy-contracts/pre-builts/DropERC721_V3.sol b/contracts/legacy-contracts/pre-builts/DropERC721_V3.sol deleted file mode 100644 index 2c7534d6e..000000000 --- a/contracts/legacy-contracts/pre-builts/DropERC721_V3.sol +++ /dev/null @@ -1,745 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -/// @author thirdweb - -// ========== External imports ========== - -import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; - -import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; - -import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/structs/BitMapsUpgradeable.sol"; -import "../../extension/Multicall.sol"; -import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; - -// ========== Internal imports ========== - -import { IDropERC721_V3 } from "../interface/drop/IDropERC721_V3.sol"; -import "../../infra/interface/IThirdwebContract.sol"; - -// ========== Features ========== - -import "../../extension/interface/IPlatformFee.sol"; -import "../../extension/interface/IPrimarySale.sol"; -import "../../extension/interface/IRoyalty.sol"; -import "../../extension/interface/IOwnable.sol"; - -import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; - -import "../../lib/CurrencyTransferLib.sol"; -import "../../lib/FeeType.sol"; -import "../../lib/MerkleProof.sol"; - -contract DropERC721_V3 is - Initializable, - IThirdwebContract, - IOwnable, - IRoyalty, - IPrimarySale, - IPlatformFee, - ReentrancyGuardUpgradeable, - ERC2771ContextUpgradeable, - Multicall, - AccessControlEnumerableUpgradeable, - ERC721EnumerableUpgradeable, - IDropERC721_V3 -{ - using BitMapsUpgradeable for BitMapsUpgradeable.BitMap; - using StringsUpgradeable for uint256; - - /*/////////////////////////////////////////////////////////////// - State variables - //////////////////////////////////////////////////////////////*/ - - bytes32 private constant MODULE_TYPE = bytes32("DropERC721"); - uint256 private constant VERSION = 3; - - /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. - bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); - /// @dev Only MINTER_ROLE holders can lazy mint NFTs. - bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); - - /// @dev Max bps in the thirdweb system. - uint256 private constant MAX_BPS = 10_000; - - /// @dev Owner of the contract (purpose: OpenSea compatibility) - address private _owner; - - /// @dev The next token ID of the NFT to "lazy mint". - uint256 public nextTokenIdToMint; - - /// @dev The next token ID of the NFT that can be claimed. - uint256 public nextTokenIdToClaim; - - /// @dev The address that receives all primary sales value. - address public primarySaleRecipient; - - /// @dev The max number of NFTs a wallet can claim. - uint256 public maxWalletClaimCount; - - /// @dev Global max total supply of NFTs. - uint256 public maxTotalSupply; - - /// @dev The address that receives all platform fees from all sales. - address private platformFeeRecipient; - - /// @dev The % of primary sales collected as platform fees. - uint16 private platformFeeBps; - - /// @dev The (default) address that receives all royalty value. - address private royaltyRecipient; - - /// @dev The (default) % of a sale to take as royalty (in basis points). - uint16 private royaltyBps; - - /// @dev Contract level metadata. - string public contractURI; - - /// @dev Largest tokenId of each batch of tokens with the same baseURI - uint256[] public baseURIIndices; - - /// @dev The set of all claim conditions, at any given moment. - ClaimConditionList public claimCondition; - - /*/////////////////////////////////////////////////////////////// - Mappings - //////////////////////////////////////////////////////////////*/ - - /** - * @dev Mapping from 'Largest tokenId of a batch of tokens with the same baseURI' - * to base URI for the respective batch of tokens. - **/ - mapping(uint256 => string) private baseURI; - - /** - * @dev Mapping from 'Largest tokenId of a batch of 'delayed-reveal' tokens with - * the same baseURI' to encrypted base URI for the respective batch of tokens. - **/ - mapping(uint256 => bytes) public encryptedData; - - /// @dev Mapping from address => total number of NFTs a wallet has claimed. - mapping(address => uint256) public walletClaimCount; - - /// @dev Token ID => royalty recipient and bps for token - mapping(uint256 => RoyaltyInfo) private royaltyInfoForToken; - - /*/////////////////////////////////////////////////////////////// - Constructor + initializer logic - //////////////////////////////////////////////////////////////*/ - - constructor() initializer {} - - /// @dev Initializes the contract, like a constructor. - function initialize( - address _defaultAdmin, - string memory _name, - string memory _symbol, - string memory _contractURI, - address[] memory _trustedForwarders, - address _saleRecipient, - address _royaltyRecipient, - uint128 _royaltyBps, - uint128 _platformFeeBps, - address _platformFeeRecipient - ) external initializer { - // Initialize inherited contracts, most base-like -> most derived. - __ReentrancyGuard_init(); - __ERC2771Context_init(_trustedForwarders); - __ERC721_init(_name, _symbol); - - // Initialize this contract's state. - royaltyRecipient = _royaltyRecipient; - royaltyBps = uint16(_royaltyBps); - platformFeeRecipient = _platformFeeRecipient; - platformFeeBps = uint16(_platformFeeBps); - primarySaleRecipient = _saleRecipient; - contractURI = _contractURI; - _owner = _defaultAdmin; - - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setupRole(MINTER_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, address(0)); - } - - /*/////////////////////////////////////////////////////////////// - Generic contract logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the type of the contract. - function contractType() external pure returns (bytes32) { - return MODULE_TYPE; - } - - /// @dev Returns the version of the contract. - function contractVersion() external pure returns (uint8) { - return uint8(VERSION); - } - - /** - * @dev Returns the address of the current owner. - */ - function owner() public view returns (address) { - return hasRole(DEFAULT_ADMIN_ROLE, _owner) ? _owner : address(0); - } - - /*/////////////////////////////////////////////////////////////// - ERC 165 / 721 / 2981 logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the URI for a given tokenId. - function tokenURI(uint256 _tokenId) public view override returns (string memory) { - for (uint256 i = 0; i < baseURIIndices.length; i += 1) { - if (_tokenId < baseURIIndices[i]) { - if (encryptedData[baseURIIndices[i]].length != 0) { - return string(abi.encodePacked(baseURI[baseURIIndices[i]], "0")); - } else { - return string(abi.encodePacked(baseURI[baseURIIndices[i]], _tokenId.toString())); - } - } - } - - return ""; - } - - /// @dev See ERC 165 - function supportsInterface( - bytes4 interfaceId - ) - public - view - virtual - override(ERC721EnumerableUpgradeable, AccessControlEnumerableUpgradeable, IERC165Upgradeable, IERC165) - returns (bool) - { - return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; - } - - /// @dev Returns the royalty recipient and amount, given a tokenId and sale price. - function royaltyInfo( - uint256 tokenId, - uint256 salePrice - ) external view virtual returns (address receiver, uint256 royaltyAmount) { - (address recipient, uint256 bps) = getRoyaltyInfoForToken(tokenId); - receiver = recipient; - royaltyAmount = (salePrice * bps) / MAX_BPS; - } - - /*/////////////////////////////////////////////////////////////// - Minting + delayed-reveal logic - //////////////////////////////////////////////////////////////*/ - - /** - * @dev Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. - * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. - */ - function lazyMint( - uint256 _amount, - string calldata _baseURIForTokens, - bytes calldata _data - ) external onlyRole(MINTER_ROLE) { - uint256 startId = nextTokenIdToMint; - uint256 baseURIIndex = startId + _amount; - - nextTokenIdToMint = baseURIIndex; - baseURI[baseURIIndex] = _baseURIForTokens; - baseURIIndices.push(baseURIIndex); - - if (_data.length > 0) { - (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(_data, (bytes, bytes32)); - - if (encryptedURI.length != 0 && provenanceHash != "") { - encryptedData[baseURIIndex] = _data; - } - } - - emit TokensLazyMinted(startId, startId + _amount - 1, _baseURIForTokens, _data); - } - - /// @dev Lets an account with `MINTER_ROLE` reveal the URI for a batch of 'delayed-reveal' NFTs. - function reveal( - uint256 index, - bytes calldata _key - ) external onlyRole(MINTER_ROLE) returns (string memory revealedURI) { - require(index < baseURIIndices.length, "invalid index."); - - uint256 _index = baseURIIndices[index]; - bytes memory data = encryptedData[_index]; - (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(data, (bytes, bytes32)); - - require(encryptedURI.length != 0, "nothing to reveal."); - - revealedURI = string(encryptDecrypt(encryptedURI, _key)); - - require(keccak256(abi.encodePacked(revealedURI, _key, block.chainid)) == provenanceHash, "Incorrect key"); - - baseURI[_index] = revealedURI; - delete encryptedData[_index]; - - emit NFTRevealed(_index, revealedURI); - - return revealedURI; - } - - /// @dev See: https://ethereum.stackexchange.com/questions/69825/decrypt-message-on-chain - function encryptDecrypt(bytes memory data, bytes calldata key) public pure returns (bytes memory result) { - // Store data length on stack for later use - uint256 length = data.length; - - // solhint-disable-next-line no-inline-assembly - assembly { - // Set result to free memory pointer - result := mload(0x40) - // Increase free memory pointer by lenght + 32 - mstore(0x40, add(add(result, length), 32)) - // Set result length - mstore(result, length) - } - - // Iterate over the data stepping by 32 bytes - for (uint256 i = 0; i < length; i += 32) { - // Generate hash of the key and offset - bytes32 hash = keccak256(abi.encodePacked(key, i)); - - bytes32 chunk; - // solhint-disable-next-line no-inline-assembly - assembly { - // Read 32-bytes data chunk - chunk := mload(add(data, add(i, 32))) - } - // XOR the chunk with hash - chunk ^= hash; - // solhint-disable-next-line no-inline-assembly - assembly { - // Write 32-byte encrypted chunk - mstore(add(result, add(i, 32)), chunk) - } - } - } - - /*/////////////////////////////////////////////////////////////// - Claim logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets an account claim NFTs. - function claim( - address _receiver, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - bytes32[] calldata _proofs, - uint256 _proofMaxQuantityPerTransaction - ) external payable nonReentrant { - require(isTrustedForwarder(msg.sender) || _msgSender() == tx.origin, "BOT"); - - uint256 tokenIdToClaim = nextTokenIdToClaim; - - // Get the claim conditions. - uint256 activeConditionId = getActiveClaimConditionId(); - - /** - * We make allowlist checks (i.e. verifyClaimMerkleProof) before verifying the claim's general - * validity (i.e. verifyClaim) because we give precedence to the check of allow list quantity - * restriction over the check of the general claim condition's quantityLimitPerTransaction - * restriction. - */ - - // Verify inclusion in allowlist. - (bool validMerkleProof, ) = verifyClaimMerkleProof( - activeConditionId, - _msgSender(), - _quantity, - _proofs, - _proofMaxQuantityPerTransaction - ); - - // Verify claim validity. If not valid, revert. - // when there's allowlist present --> verifyClaimMerkleProof will verify the _proofMaxQuantityPerTransaction value with hashed leaf in the allowlist - // when there's no allowlist, this check is true --> verifyClaim will check for _quantity being less/equal than the limit - bool toVerifyMaxQuantityPerTransaction = _proofMaxQuantityPerTransaction == 0 || - claimCondition.phases[activeConditionId].merkleRoot == bytes32(0); - verifyClaim( - activeConditionId, - _msgSender(), - _quantity, - _currency, - _pricePerToken, - toVerifyMaxQuantityPerTransaction - ); - - if (validMerkleProof && _proofMaxQuantityPerTransaction > 0) { - /** - * Mark the claimer's use of their position in the allowlist. A spot in an allowlist - * can be used only once. - */ - claimCondition.limitMerkleProofClaim[activeConditionId].set(uint256(uint160(_msgSender()))); - } - - // If there's a price, collect price. - collectClaimPrice(_quantity, _currency, _pricePerToken); - - // Mint the relevant NFTs to claimer. - transferClaimedTokens(_receiver, activeConditionId, _quantity); - - emit TokensClaimed(activeConditionId, _msgSender(), _receiver, tokenIdToClaim, _quantity); - } - - /// @dev Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - function setClaimConditions( - ClaimCondition[] calldata _phases, - bool _resetClaimEligibility - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - uint256 existingStartIndex = claimCondition.currentStartId; - uint256 existingPhaseCount = claimCondition.count; - - /** - * `limitLastClaimTimestamp` and `limitMerkleProofClaim` are mappings that use a - * claim condition's UID as a key. - * - * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim - * conditions in `_phases`, effectively resetting the restrictions on claims expressed - * by `limitLastClaimTimestamp` and `limitMerkleProofClaim`. - */ - uint256 newStartIndex = existingStartIndex; - if (_resetClaimEligibility) { - newStartIndex = existingStartIndex + existingPhaseCount; - } - - claimCondition.count = _phases.length; - claimCondition.currentStartId = newStartIndex; - - uint256 lastConditionStartTimestamp; - for (uint256 i = 0; i < _phases.length; i++) { - require(i == 0 || lastConditionStartTimestamp < _phases[i].startTimestamp, "ST"); - - uint256 supplyClaimedAlready = claimCondition.phases[newStartIndex + i].supplyClaimed; - require(supplyClaimedAlready <= _phases[i].maxClaimableSupply, "max supply claimed already"); - - claimCondition.phases[newStartIndex + i] = _phases[i]; - claimCondition.phases[newStartIndex + i].supplyClaimed = supplyClaimedAlready; - - lastConditionStartTimestamp = _phases[i].startTimestamp; - } - - /** - * Gas refunds (as much as possible) - * - * If `_resetClaimEligibility == true`, we assign completely new UIDs to the claim - * conditions in `_phases`. So, we delete claim conditions with UID < `newStartIndex`. - * - * If `_resetClaimEligibility == false`, and there are more existing claim conditions - * than in `_phases`, we delete the existing claim conditions that don't get replaced - * by the conditions in `_phases`. - */ - if (_resetClaimEligibility) { - for (uint256 i = existingStartIndex; i < newStartIndex; i++) { - delete claimCondition.phases[i]; - delete claimCondition.limitMerkleProofClaim[i]; - } - } else { - if (existingPhaseCount > _phases.length) { - for (uint256 i = _phases.length; i < existingPhaseCount; i++) { - delete claimCondition.phases[newStartIndex + i]; - delete claimCondition.limitMerkleProofClaim[newStartIndex + i]; - } - } - } - - emit ClaimConditionsUpdated(_phases); - } - - /// @dev Collects and distributes the primary sale value of NFTs being claimed. - function collectClaimPrice(uint256 _quantityToClaim, address _currency, uint256 _pricePerToken) internal { - if (_pricePerToken == 0) { - return; - } - - uint256 totalPrice = _quantityToClaim * _pricePerToken; - uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; - - if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { - require(msg.value == totalPrice, "must send total price."); - } - - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), primarySaleRecipient, totalPrice - platformFees); - } - - /// @dev Transfers the NFTs being claimed. - function transferClaimedTokens(address _to, uint256 _conditionId, uint256 _quantityBeingClaimed) internal { - // Update the supply minted under mint condition. - claimCondition.phases[_conditionId].supplyClaimed += _quantityBeingClaimed; - - // if transfer claimed tokens is called when `to != msg.sender`, it'd use msg.sender's limits. - // behavior would be similar to `msg.sender` mint for itself, then transfer to `_to`. - claimCondition.limitLastClaimTimestamp[_conditionId][_msgSender()] = block.timestamp; - walletClaimCount[_msgSender()] += _quantityBeingClaimed; - - uint256 tokenIdToClaim = nextTokenIdToClaim; - - for (uint256 i = 0; i < _quantityBeingClaimed; i += 1) { - _mint(_to, tokenIdToClaim); - tokenIdToClaim += 1; - } - - nextTokenIdToClaim = tokenIdToClaim; - } - - /// @dev Checks a request to claim NFTs against the active claim condition's criteria. - function verifyClaim( - uint256 _conditionId, - address _claimer, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - bool verifyMaxQuantityPerTransaction - ) public view { - ClaimCondition memory currentClaimPhase = claimCondition.phases[_conditionId]; - - require( - _currency == currentClaimPhase.currency && _pricePerToken == currentClaimPhase.pricePerToken, - "invalid currency or price." - ); - - // If we're checking for an allowlist quantity restriction, ignore the general quantity restriction. - require( - _quantity > 0 && - (!verifyMaxQuantityPerTransaction || _quantity <= currentClaimPhase.quantityLimitPerTransaction), - "invalid quantity." - ); - require( - currentClaimPhase.supplyClaimed + _quantity <= currentClaimPhase.maxClaimableSupply, - "exceed max claimable supply." - ); - require(nextTokenIdToClaim + _quantity <= nextTokenIdToMint, "not enough minted tokens."); - require(maxTotalSupply == 0 || nextTokenIdToClaim + _quantity <= maxTotalSupply, "exceed max total supply."); - require( - maxWalletClaimCount == 0 || walletClaimCount[_claimer] + _quantity <= maxWalletClaimCount, - "exceed claim limit" - ); - - (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) = getClaimTimestamp(_conditionId, _claimer); - require(lastClaimTimestamp == 0 || block.timestamp >= nextValidClaimTimestamp, "cannot claim."); - } - - /// @dev Checks whether a claimer meets the claim condition's allowlist criteria. - function verifyClaimMerkleProof( - uint256 _conditionId, - address _claimer, - uint256 _quantity, - bytes32[] calldata _proofs, - uint256 _proofMaxQuantityPerTransaction - ) public view returns (bool validMerkleProof, uint256 merkleProofIndex) { - ClaimCondition memory currentClaimPhase = claimCondition.phases[_conditionId]; - - if (currentClaimPhase.merkleRoot != bytes32(0)) { - (validMerkleProof, merkleProofIndex) = MerkleProof.verify( - _proofs, - currentClaimPhase.merkleRoot, - keccak256(abi.encodePacked(_claimer, _proofMaxQuantityPerTransaction)) - ); - require(validMerkleProof, "not in whitelist."); - require( - !claimCondition.limitMerkleProofClaim[_conditionId].get(uint256(uint160(_claimer))), - "proof claimed." - ); - require( - _proofMaxQuantityPerTransaction == 0 || _quantity <= _proofMaxQuantityPerTransaction, - "invalid quantity proof." - ); - } - } - - /*/////////////////////////////////////////////////////////////// - Getter functions - //////////////////////////////////////////////////////////////*/ - - /// @dev At any given moment, returns the uid for the active claim condition. - function getActiveClaimConditionId() public view returns (uint256) { - for (uint256 i = claimCondition.currentStartId + claimCondition.count; i > claimCondition.currentStartId; i--) { - if (block.timestamp >= claimCondition.phases[i - 1].startTimestamp) { - return i - 1; - } - } - - revert("!CONDITION."); - } - - /// @dev Returns the royalty recipient and bps for a particular token Id. - function getRoyaltyInfoForToken(uint256 _tokenId) public view returns (address, uint16) { - RoyaltyInfo memory royaltyForToken = royaltyInfoForToken[_tokenId]; - - return - royaltyForToken.recipient == address(0) - ? (royaltyRecipient, uint16(royaltyBps)) - : (royaltyForToken.recipient, uint16(royaltyForToken.bps)); - } - - /// @dev Returns the platform fee recipient and bps. - function getPlatformFeeInfo() external view returns (address, uint16) { - return (platformFeeRecipient, uint16(platformFeeBps)); - } - - /// @dev Returns the default royalty recipient and bps. - function getDefaultRoyaltyInfo() external view returns (address, uint16) { - return (royaltyRecipient, uint16(royaltyBps)); - } - - /// @dev Returns the timestamp for when a claimer is eligible for claiming NFTs again. - function getClaimTimestamp( - uint256 _conditionId, - address _claimer - ) public view returns (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) { - lastClaimTimestamp = claimCondition.limitLastClaimTimestamp[_conditionId][_claimer]; - - unchecked { - nextValidClaimTimestamp = - lastClaimTimestamp + - claimCondition.phases[_conditionId].waitTimeInSecondsBetweenClaims; - - if (nextValidClaimTimestamp < lastClaimTimestamp) { - nextValidClaimTimestamp = type(uint256).max; - } - } - } - - /// @dev Returns the claim condition at the given uid. - function getClaimConditionById(uint256 _conditionId) external view returns (ClaimCondition memory condition) { - condition = claimCondition.phases[_conditionId]; - } - - /// @dev Returns the amount of stored baseURIs - function getBaseURICount() external view returns (uint256) { - return baseURIIndices.length; - } - - /*/////////////////////////////////////////////////////////////// - Setter functions - //////////////////////////////////////////////////////////////*/ - - /// @dev Lets a contract admin set a claim count for a wallet. - function setWalletClaimCount(address _claimer, uint256 _count) external onlyRole(DEFAULT_ADMIN_ROLE) { - walletClaimCount[_claimer] = _count; - emit WalletClaimCountUpdated(_claimer, _count); - } - - /// @dev Lets a contract admin set a maximum number of NFTs that can be claimed by any wallet. - function setMaxWalletClaimCount(uint256 _count) external onlyRole(DEFAULT_ADMIN_ROLE) { - maxWalletClaimCount = _count; - emit MaxWalletClaimCountUpdated(_count); - } - - /// @dev Lets a contract admin set the global maximum supply for collection's NFTs. - function setMaxTotalSupply(uint256 _maxTotalSupply) external onlyRole(DEFAULT_ADMIN_ROLE) { - maxTotalSupply = _maxTotalSupply; - emit MaxTotalSupplyUpdated(_maxTotalSupply); - } - - /// @dev Lets a contract admin set the recipient for all primary sales. - function setPrimarySaleRecipient(address _saleRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { - primarySaleRecipient = _saleRecipient; - emit PrimarySaleRecipientUpdated(_saleRecipient); - } - - /// @dev Lets a contract admin update the default royalty recipient and bps. - function setDefaultRoyaltyInfo( - address _royaltyRecipient, - uint256 _royaltyBps - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(_royaltyBps <= MAX_BPS, "> MAX_BPS"); - - royaltyRecipient = _royaltyRecipient; - royaltyBps = uint16(_royaltyBps); - - emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); - } - - /// @dev Lets a contract admin set the royalty recipient and bps for a particular token Id. - function setRoyaltyInfoForToken( - uint256 _tokenId, - address _recipient, - uint256 _bps - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(_bps <= MAX_BPS, "> MAX_BPS"); - - royaltyInfoForToken[_tokenId] = RoyaltyInfo({ recipient: _recipient, bps: _bps }); - - emit RoyaltyForToken(_tokenId, _recipient, _bps); - } - - /// @dev Lets a contract admin update the platform fee recipient and bps - function setPlatformFeeInfo( - address _platformFeeRecipient, - uint256 _platformFeeBps - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(_platformFeeBps <= MAX_BPS, "> MAX_BPS."); - - platformFeeBps = uint16(_platformFeeBps); - platformFeeRecipient = _platformFeeRecipient; - - emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); - } - - /// @dev Lets a contract admin set a new owner for the contract. The new owner must be a contract admin. - function setOwner(address _newOwner) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(hasRole(DEFAULT_ADMIN_ROLE, _newOwner), "!ADMIN"); - address _prevOwner = _owner; - _owner = _newOwner; - - emit OwnerUpdated(_prevOwner, _newOwner); - } - - /// @dev Lets a contract admin set the URI for contract-level metadata. - function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { - contractURI = _uri; - } - - /*/////////////////////////////////////////////////////////////// - Miscellaneous - //////////////////////////////////////////////////////////////*/ - - /// @dev Burns `tokenId`. See {ERC721-_burn}. - function burn(uint256 tokenId) public virtual { - //solhint-disable-next-line max-line-length - require(_isApprovedOrOwner(_msgSender(), tokenId), "caller not owner nor approved"); - _burn(tokenId); - } - - /// @dev See {ERC721-_beforeTokenTransfer}. - function _beforeTokenTransfer( - address from, - address to, - uint256 tokenId, - uint256 batchSize - ) internal virtual override(ERC721EnumerableUpgradeable) { - super._beforeTokenTransfer(from, to, tokenId, batchSize); - - // if transfer is restricted on the contract, we still want to allow burning and minting - if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { - require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "!TRANSFER_ROLE"); - } - } - - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } -} diff --git a/contracts/legacy-contracts/pre-builts/SignatureDrop_V4.sol b/contracts/legacy-contracts/pre-builts/SignatureDrop_V4.sol deleted file mode 100644 index 4b3e313bc..000000000 --- a/contracts/legacy-contracts/pre-builts/SignatureDrop_V4.sol +++ /dev/null @@ -1,360 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -/// @author thirdweb - -// ========== External imports ========== - -import "../../extension/Multicall.sol"; -import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; - -import "erc721a-upgradeable/contracts/ERC721AUpgradeable.sol"; - -// ========== Internal imports ========== - -import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; -import "../../lib/CurrencyTransferLib.sol"; - -// ========== Features ========== - -import "../../extension/ContractMetadata.sol"; -import "../../extension/PlatformFee.sol"; -import "../../extension/Royalty.sol"; -import "../../extension/PrimarySale.sol"; -import "../../extension/Ownable.sol"; -import "../../extension/DelayedReveal.sol"; -import "../extension/LazyMint_V1.sol"; -import "../../extension/PermissionsEnumerable.sol"; -import "../extension/DropSinglePhase_V1.sol"; -import "../../extension/SignatureMintERC721Upgradeable.sol"; - -contract SignatureDrop_V4 is - Initializable, - ContractMetadata, - PlatformFee, - Royalty, - PrimarySale, - Ownable, - DelayedReveal, - LazyMint_V1, - PermissionsEnumerable, - DropSinglePhase_V1, - SignatureMintERC721Upgradeable, - ERC2771ContextUpgradeable, - Multicall, - ERC721AUpgradeable -{ - using StringsUpgradeable for uint256; - - /*/////////////////////////////////////////////////////////////// - State variables - //////////////////////////////////////////////////////////////*/ - - /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. - bytes32 private transferRole; - /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s and lazy mint tokens. - bytes32 private minterRole; - - /// @dev Max bps in the thirdweb system. - uint256 private constant MAX_BPS = 10_000; - - /*/////////////////////////////////////////////////////////////// - Constructor + initializer logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Initializes the contract, like a constructor. - function initialize( - address _defaultAdmin, - string memory _name, - string memory _symbol, - string memory _contractURI, - address[] memory _trustedForwarders, - address _saleRecipient, - address _royaltyRecipient, - uint128 _royaltyBps, - uint128 _platformFeeBps, - address _platformFeeRecipient - ) external initializer { - transferRole = keccak256("TRANSFER_ROLE"); - minterRole = keccak256("MINTER_ROLE"); - - // Initialize inherited contracts, most base-like -> most derived. - __ERC2771Context_init(_trustedForwarders); - __ERC721A_init(_name, _symbol); - __SignatureMintERC721_init(); - - _setupContractURI(_contractURI); - _setupOwner(_defaultAdmin); - - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setupRole(minterRole, _defaultAdmin); - _setupRole(transferRole, _defaultAdmin); - _setupRole(transferRole, address(0)); - - _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); - _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); - _setupPrimarySaleRecipient(_saleRecipient); - } - - /*/////////////////////////////////////////////////////////////// - ERC 165 / 721 / 2981 logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the URI for a given tokenId. - function tokenURI(uint256 _tokenId) public view override returns (string memory) { - (uint256 batchId, ) = _getBatchId(_tokenId); - string memory batchUri = _getBaseURI(_tokenId); - - if (isEncryptedBatch(batchId)) { - return string(abi.encodePacked(batchUri, "0")); - } else { - return string(abi.encodePacked(batchUri, _tokenId.toString())); - } - } - - /// @dev See ERC 165 - function supportsInterface( - bytes4 interfaceId - ) public view virtual override(ERC721AUpgradeable, IERC165) returns (bool) { - return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; - } - - function contractType() external pure returns (bytes32) { - return bytes32("SignatureDrop"); - } - - function contractVersion() external pure returns (uint8) { - return uint8(4); - } - - /*/////////////////////////////////////////////////////////////// - Lazy minting + delayed-reveal logic - //////////////////////////////////////////////////////////////*/ - - /** - * @dev Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. - * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. - */ - function lazyMint( - uint256 _amount, - string calldata _baseURIForTokens, - bytes calldata _data - ) public override returns (uint256 batchId) { - if (_data.length > 0) { - (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(_data, (bytes, bytes32)); - if (encryptedURI.length != 0 && provenanceHash != "") { - _setEncryptedData(nextTokenIdToLazyMint + _amount, _data); - } - } - - return super.lazyMint(_amount, _baseURIForTokens, _data); - } - - /// @dev Lets an account with `MINTER_ROLE` reveal the URI for a batch of 'delayed-reveal' NFTs. - function reveal( - uint256 _index, - bytes calldata _key - ) external onlyRole(minterRole) returns (string memory revealedURI) { - uint256 batchId = getBatchIdAtIndex(_index); - revealedURI = getRevealURI(batchId, _key); - - _setEncryptedData(batchId, ""); - _setBaseURI(batchId, revealedURI); - - emit TokenURIRevealed(_index, revealedURI); - } - - /*/////////////////////////////////////////////////////////////// - Claiming lazy minted tokens logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Claim lazy minted tokens via signature. - function mintWithSignature( - MintRequest calldata _req, - bytes calldata _signature - ) external payable returns (address signer) { - uint256 tokenIdToMint = _currentIndex; - if (tokenIdToMint + _req.quantity > nextTokenIdToLazyMint) { - revert("Not enough tokens"); - } - - // Verify and process payload. - signer = _processRequest(_req, _signature); - - address receiver = _req.to; - - // Collect price - _collectPriceOnClaim(_req.primarySaleRecipient, _req.quantity, _req.currency, _req.pricePerToken); - - // Set royalties, if applicable. - if (_req.royaltyRecipient != address(0) && _req.royaltyBps != 0) { - _setupRoyaltyInfoForToken(tokenIdToMint, _req.royaltyRecipient, _req.royaltyBps); - } - - // Mint tokens. - _safeMint(receiver, _req.quantity); - - emit TokensMintedWithSignature(signer, receiver, tokenIdToMint, _req); - } - - /*/////////////////////////////////////////////////////////////// - Internal functions - //////////////////////////////////////////////////////////////*/ - - /// @dev Runs before every `claim` function call. - function _beforeClaim( - address, - uint256 _quantity, - address, - uint256, - AllowlistProof calldata, - bytes memory - ) internal view override { - bool bot = isTrustedForwarder(msg.sender) || _msgSender() == tx.origin; - require(bot, "BOT"); - require(_currentIndex + _quantity <= nextTokenIdToLazyMint, "Not enough tokens"); - } - - /// @dev Collects and distributes the primary sale value of NFTs being claimed. - function _collectPriceOnClaim( - address _primarySaleRecipient, - uint256 _quantityToClaim, - address _currency, - uint256 _pricePerToken - ) internal override { - if (_pricePerToken == 0) { - return; - } - - (address platformFeeRecipient, uint16 platformFeeBps) = getPlatformFeeInfo(); - - address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; - - uint256 totalPrice = _quantityToClaim * _pricePerToken; - uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; - - if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { - if (msg.value != totalPrice) { - revert("Must send total price"); - } - } - - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), saleRecipient, totalPrice - platformFees); - } - - /// @dev Transfers the NFTs being claimed. - function _transferTokensOnClaim( - address _to, - uint256 _quantityBeingClaimed - ) internal override returns (uint256 startTokenId) { - startTokenId = _currentIndex; - _safeMint(_to, _quantityBeingClaimed); - } - - /// @dev Returns whether a given address is authorized to sign mint requests. - function _isAuthorizedSigner(address _signer) internal view override returns (bool) { - return hasRole(minterRole, _signer); - } - - /// @dev Checks whether platform fee info can be set in the given execution context. - function _canSetPlatformFeeInfo() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether primary sale recipient can be set in the given execution context. - function _canSetPrimarySaleRecipient() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether owner can be set in the given execution context. - function _canSetOwner() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether royalty info can be set in the given execution context. - function _canSetRoyaltyInfo() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether contract metadata can be set in the given execution context. - function _canSetContractURI() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether platform fee info can be set in the given execution context. - function _canSetClaimConditions() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Returns whether lazy minting can be done in the given execution context. - function _canLazyMint() internal view virtual override returns (bool) { - return hasRole(minterRole, _msgSender()); - } - - /*/////////////////////////////////////////////////////////////// - Miscellaneous - //////////////////////////////////////////////////////////////*/ - - /** - * Returns the total amount of tokens minted in the contract. - */ - function totalMinted() external view returns (uint256) { - unchecked { - return _currentIndex - _startTokenId(); - } - } - - /// @dev The tokenId of the next NFT that will be minted / lazy minted. - function nextTokenIdToMint() external view returns (uint256) { - return nextTokenIdToLazyMint; - } - - /// @dev Burns `tokenId`. See {ERC721-_burn}. - function burn(uint256 tokenId) external virtual { - // note: ERC721AUpgradeable's `_burn(uint256,bool)` internally checks for token approvals. - _burn(tokenId, true); - } - - /// @dev See {ERC721-_beforeTokenTransfer}. - function _beforeTokenTransfers( - address from, - address to, - uint256 startTokenId, - uint256 quantity - ) internal virtual override { - super._beforeTokenTransfers(from, to, startTokenId, quantity); - - // if transfer is restricted on the contract, we still want to allow burning and minting - if (!hasRole(transferRole, address(0)) && from != address(0) && to != address(0)) { - if (!hasRole(transferRole, from) && !hasRole(transferRole, to)) { - revert("!Transfer-Role"); - } - } - } - - function _dropMsgSender() internal view virtual override returns (address) { - return _msgSender(); - } - - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } -} diff --git a/contracts/legacy-contracts/smart-wallet/interface/IAccountPermissions_V1.sol b/contracts/legacy-contracts/smart-wallet/interface/IAccountPermissions_V1.sol deleted file mode 100644 index 3a2124861..000000000 --- a/contracts/legacy-contracts/smart-wallet/interface/IAccountPermissions_V1.sol +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/// @author thirdweb - -interface IAccountPermissions_V1 { - /*/////////////////////////////////////////////////////////////// - Types - //////////////////////////////////////////////////////////////*/ - - /** - * @notice The payload that must be signed by an authorized wallet to set permissions for a signer to use the smart wallet. - * - * @param signer The addres of the signer to give permissions. - * @param approvedTargets The list of approved targets that a role holder can call using the smart wallet. - * @param nativeTokenLimitPerTransaction The maximum value that can be transferred by a role holder in a single transaction. - * @param permissionStartTimestamp The UNIX timestamp at and after which a signer has permission to use the smart wallet. - * @param permissionEndTimestamp The UNIX timestamp at and after which a signer no longer has permission to use the smart wallet. - * @param reqValidityStartTimestamp The UNIX timestamp at and after which a signature is valid. - * @param reqValidityEndTimestamp The UNIX timestamp at and after which a signature is invalid/expired. - * @param uid A unique non-repeatable ID for the payload. - */ - struct SignerPermissionRequest { - address signer; - address[] approvedTargets; - uint256 nativeTokenLimitPerTransaction; - uint128 permissionStartTimestamp; - uint128 permissionEndTimestamp; - uint128 reqValidityStartTimestamp; - uint128 reqValidityEndTimestamp; - bytes32 uid; - } - - /** - * @notice The permissions that a signer has to use the smart wallet. - * - * @param signer The address of the signer. - * @param approvedTargets The list of approved targets that a role holder can call using the smart wallet. - * @param nativeTokenLimitPerTransaction The maximum value that can be transferred by a role holder in a single transaction. - * @param startTimestamp The UNIX timestamp at and after which a signer has permission to use the smart wallet. - * @param endTimestamp The UNIX timestamp at and after which a signer no longer has permission to use the smart wallet. - */ - struct SignerPermissions { - address signer; - address[] approvedTargets; - uint256 nativeTokenLimitPerTransaction; - uint128 startTimestamp; - uint128 endTimestamp; - } - - /** - * @notice Internal struct for storing permissions for a signer (without approved targets). - * - * @param nativeTokenLimitPerTransaction The maximum value that can be transferred by a role holder in a single transaction. - * @param startTimestamp The UNIX timestamp at and after which a signer has permission to use the smart wallet. - * @param endTimestamp The UNIX timestamp at and after which a signer no longer has permission to use the smart wallet. - */ - struct SignerPermissionsStatic { - uint256 nativeTokenLimitPerTransaction; - uint128 startTimestamp; - uint128 endTimestamp; - } - - /*/////////////////////////////////////////////////////////////// - Events - //////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when permissions for a signer are updated. - event SignerPermissionsUpdated( - address indexed authorizingSigner, - address indexed targetSigner, - SignerPermissionRequest permissions - ); - - /// @notice Emitted when an admin is set or removed. - event AdminUpdated(address indexed signer, bool isAdmin); - - /*/////////////////////////////////////////////////////////////// - View functions - //////////////////////////////////////////////////////////////*/ - - /// @notice Returns whether the given account is an admin. - function isAdmin(address signer) external view returns (bool); - - /// @notice Returns whether the given account is an active signer on the account. - function isActiveSigner(address signer) external view returns (bool); - - /// @notice Returns the restrictions under which a signer can use the smart wallet. - function getPermissionsForSigner(address signer) external view returns (SignerPermissions memory permissions); - - /// @notice Returns all active and inactive signers of the account. - function getAllSigners() external view returns (SignerPermissions[] memory signers); - - /// @notice Returns all signers with active permissions to use the account. - function getAllActiveSigners() external view returns (SignerPermissions[] memory signers); - - /// @notice Returns all admins of the account. - function getAllAdmins() external view returns (address[] memory admins); - - /// @dev Verifies that a request is signed by an authorized account. - function verifySignerPermissionRequest( - SignerPermissionRequest calldata req, - bytes calldata signature - ) external view returns (bool success, address signer); - - /*/////////////////////////////////////////////////////////////// - External functions - //////////////////////////////////////////////////////////////*/ - - /// @notice Adds / removes an account as an admin. - function setAdmin(address account, bool isAdmin) external; - - /// @notice Sets the permissions for a given signer. - function setPermissionsForSigner(SignerPermissionRequest calldata req, bytes calldata signature) external; -} diff --git a/contracts/prebuilts/pack/Pack.sol b/contracts/prebuilts/pack/Pack.sol deleted file mode 100644 index 196448def..000000000 --- a/contracts/prebuilts/pack/Pack.sol +++ /dev/null @@ -1,463 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -/// @author thirdweb - -// $$\ $$\ $$\ $$\ $$\ -// $$ | $$ | \__| $$ | $$ | -// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ -// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ -// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | -// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | -// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | -// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ - -// ========== External imports ========== - -import "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155PausableUpgradeable.sol"; - -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; -import "@openzeppelin/contracts/interfaces/IERC721Receiver.sol"; -import { IERC1155Receiver } from "@openzeppelin/contracts/interfaces/IERC1155Receiver.sol"; - -// ========== Internal imports ========== - -import "../interface/IPack.sol"; -import "../../extension/Multicall.sol"; -import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; - -// ========== Features ========== - -import "../../extension/ContractMetadata.sol"; -import "../../extension/Royalty.sol"; -import "../../extension/Ownable.sol"; -import "../../extension/PermissionsEnumerable.sol"; -import { TokenStore, ERC1155Receiver } from "../../extension/TokenStore.sol"; - -contract Pack is - Initializable, - ContractMetadata, - Ownable, - Royalty, - PermissionsEnumerable, - TokenStore, - ReentrancyGuardUpgradeable, - ERC2771ContextUpgradeable, - Multicall, - ERC1155Upgradeable, - IPack -{ - /*/////////////////////////////////////////////////////////////// - State variables - //////////////////////////////////////////////////////////////*/ - - bytes32 private constant MODULE_TYPE = bytes32("Pack"); - uint256 private constant VERSION = 2; - - /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. - bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); - /// @dev Only MINTER_ROLE holders can create packs. - bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); - /// @dev Only assets with ASSET_ROLE can be packed, when packing is restricted to particular assets. - bytes32 private constant ASSET_ROLE = keccak256("ASSET_ROLE"); - - // Token name - string public name; - - // Token symbol - string public symbol; - - /// @dev The token Id of the next set of packs to be minted. - uint256 public nextTokenIdToMint; - - /*/////////////////////////////////////////////////////////////// - Mappings - //////////////////////////////////////////////////////////////*/ - - /// @dev Mapping from token ID => total circulating supply of token with that ID. - mapping(uint256 => uint256) public totalSupply; - - /// @dev Mapping from pack ID => The state of that set of packs. - mapping(uint256 => PackInfo) private packInfo; - - /// @dev Checks if pack-creator allowed to add more tokens to a packId; set to false after first transfer - mapping(uint256 => bool) public canUpdatePack; - - /*/////////////////////////////////////////////////////////////// - Constructor + initializer logic - //////////////////////////////////////////////////////////////*/ - - constructor(address _nativeTokenWrapper) TokenStore(_nativeTokenWrapper) initializer {} - - /// @dev Initializes the contract, like a constructor. - /* solhint-disable no-unused-vars */ - function initialize( - address _defaultAdmin, - string memory _name, - string memory _symbol, - string memory _contractURI, - address[] memory _trustedForwarders, - address _royaltyRecipient, - uint256 _royaltyBps - ) external initializer { - __ERC1155_init(_contractURI); - - name = _name; - symbol = _symbol; - - _setupContractURI(_contractURI); - _setupOwner(_defaultAdmin); - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setupRole(MINTER_ROLE, _defaultAdmin); - - // note: see `onlyRoleWithSwitch` for ASSET_ROLE behaviour. - _setupRole(ASSET_ROLE, address(0)); - _setupRole(TRANSFER_ROLE, address(0)); - - _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); - } - - /* solhint-enable no-unused-vars */ - - receive() external payable { - require(msg.sender == nativeTokenWrapper, "!nativeTokenWrapper."); - } - - /*/////////////////////////////////////////////////////////////// - Modifiers - //////////////////////////////////////////////////////////////*/ - - modifier onlyRoleWithSwitch(bytes32 role) { - _checkRoleWithSwitch(role, _msgSender()); - _; - } - - /*/////////////////////////////////////////////////////////////// - Generic contract logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the type of the contract. - function contractType() external pure returns (bytes32) { - return MODULE_TYPE; - } - - /// @dev Returns the version of the contract. - function contractVersion() external pure returns (uint8) { - return uint8(VERSION); - } - - /*/////////////////////////////////////////////////////////////// - ERC 165 / 1155 / 2981 logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the URI for a given tokenId. - function uri(uint256 _tokenId) public view override returns (string memory) { - return getUriOfBundle(_tokenId); - } - - /// @dev See ERC 165 - function supportsInterface( - bytes4 interfaceId - ) public view virtual override(ERC1155Receiver, ERC1155Upgradeable, IERC165) returns (bool) { - return - super.supportsInterface(interfaceId) || - type(IERC2981Upgradeable).interfaceId == interfaceId || - type(IERC721Receiver).interfaceId == interfaceId || - type(IERC1155Receiver).interfaceId == interfaceId; - } - - /*/////////////////////////////////////////////////////////////// - Pack logic: create | open packs. - //////////////////////////////////////////////////////////////*/ - - /// @dev Creates a pack with the stated contents. - function createPack( - Token[] calldata _contents, - uint256[] calldata _numOfRewardUnits, - string memory _packUri, - uint128 _openStartTimestamp, - uint128 _amountDistributedPerOpen, - address _recipient - ) external payable onlyRoleWithSwitch(MINTER_ROLE) nonReentrant returns (uint256 packId, uint256 packTotalSupply) { - require(_contents.length > 0 && _contents.length == _numOfRewardUnits.length, "!Len"); - - if (!hasRole(ASSET_ROLE, address(0))) { - for (uint256 i = 0; i < _contents.length; i += 1) { - _checkRole(ASSET_ROLE, _contents[i].assetContract); - } - } - - packId = nextTokenIdToMint; - nextTokenIdToMint += 1; - - packTotalSupply = escrowPackContents( - _contents, - _numOfRewardUnits, - _packUri, - packId, - _amountDistributedPerOpen, - false - ); - - packInfo[packId].openStartTimestamp = _openStartTimestamp; - packInfo[packId].amountDistributedPerOpen = _amountDistributedPerOpen; - - canUpdatePack[packId] = true; - - _mint(_recipient, packId, packTotalSupply, ""); - - emit PackCreated(packId, _recipient, packTotalSupply); - } - - /// @dev Add contents to an existing packId. - function addPackContents( - uint256 _packId, - Token[] calldata _contents, - uint256[] calldata _numOfRewardUnits, - address _recipient - ) - external - payable - onlyRoleWithSwitch(MINTER_ROLE) - nonReentrant - returns (uint256 packTotalSupply, uint256 newSupplyAdded) - { - require(canUpdatePack[_packId], "!Allowed"); - require(_contents.length > 0 && _contents.length == _numOfRewardUnits.length, "!Len"); - require(balanceOf(_recipient, _packId) != 0, "!Bal"); - - if (!hasRole(ASSET_ROLE, address(0))) { - for (uint256 i = 0; i < _contents.length; i += 1) { - _checkRole(ASSET_ROLE, _contents[i].assetContract); - } - } - - uint256 amountPerOpen = packInfo[_packId].amountDistributedPerOpen; - - newSupplyAdded = escrowPackContents(_contents, _numOfRewardUnits, "", _packId, amountPerOpen, true); - packTotalSupply = totalSupply[_packId] + newSupplyAdded; - - _mint(_recipient, _packId, newSupplyAdded, ""); - - emit PackUpdated(_packId, _recipient, newSupplyAdded); - } - - /// @notice Lets a pack owner open packs and receive the packs' reward units. - function openPack(uint256 _packId, uint256 _amountToOpen) external nonReentrant returns (Token[] memory) { - address opener = _msgSender(); - - require(opener == tx.origin, "!EOA"); - require(balanceOf(opener, _packId) >= _amountToOpen, "!Bal"); - - PackInfo memory pack = packInfo[_packId]; - require(pack.openStartTimestamp <= block.timestamp, "cant open"); - - Token[] memory rewardUnits = getRewardUnits(_packId, _amountToOpen, pack.amountDistributedPerOpen, pack); - - _burn(opener, _packId, _amountToOpen); - - _transferTokenBatch(address(this), opener, rewardUnits); - - emit PackOpened(_packId, opener, _amountToOpen, rewardUnits); - - return rewardUnits; - } - - /// @dev Stores assets within the contract. - function escrowPackContents( - Token[] calldata _contents, - uint256[] calldata _numOfRewardUnits, - string memory _packUri, - uint256 packId, - uint256 amountPerOpen, - bool isUpdate - ) internal returns (uint256 supplyToMint) { - uint256 sumOfRewardUnits; - - for (uint256 i = 0; i < _contents.length; i += 1) { - require(_contents[i].totalAmount != 0, "0 amt"); - require(_contents[i].totalAmount % _numOfRewardUnits[i] == 0, "!R"); - require(_contents[i].tokenType != TokenType.ERC721 || _contents[i].totalAmount == 1, "!R"); - - sumOfRewardUnits += _numOfRewardUnits[i]; - - packInfo[packId].perUnitAmounts.push(_contents[i].totalAmount / _numOfRewardUnits[i]); - } - - require(sumOfRewardUnits % amountPerOpen == 0, "!Amt"); - supplyToMint = sumOfRewardUnits / amountPerOpen; - - if (isUpdate) { - for (uint256 i = 0; i < _contents.length; i += 1) { - _addTokenInBundle(_contents[i], packId); - } - _transferTokenBatch(_msgSender(), address(this), _contents); - } else { - _storeTokens(_msgSender(), _contents, _packUri, packId); - } - } - - /// @dev Returns the reward units to distribute. - function getRewardUnits( - uint256 _packId, - uint256 _numOfPacksToOpen, - uint256 _rewardUnitsPerOpen, - PackInfo memory pack - ) internal returns (Token[] memory rewardUnits) { - uint256 numOfRewardUnitsToDistribute = _numOfPacksToOpen * _rewardUnitsPerOpen; - rewardUnits = new Token[](numOfRewardUnitsToDistribute); - uint256 totalRewardUnits = totalSupply[_packId] * _rewardUnitsPerOpen; - uint256 totalRewardKinds = getTokenCountOfBundle(_packId); - - uint256 random = generateRandomValue(); - - (Token[] memory _token, ) = getPackContents(_packId); - bool[] memory _isUpdated = new bool[](totalRewardKinds); - for (uint256 i; i < numOfRewardUnitsToDistribute; ) { - uint256 randomVal = uint256(keccak256(abi.encode(random, i))); - uint256 target = randomVal % totalRewardUnits; - uint256 step; - for (uint256 j; j < totalRewardKinds; ) { - uint256 perUnitAmount = pack.perUnitAmounts[j]; - uint256 totalRewardUnitsOfKind = _token[j].totalAmount / perUnitAmount; - if (target < step + totalRewardUnitsOfKind) { - _token[j].totalAmount -= perUnitAmount; - _isUpdated[j] = true; - rewardUnits[i].assetContract = _token[j].assetContract; - rewardUnits[i].tokenType = _token[j].tokenType; - rewardUnits[i].tokenId = _token[j].tokenId; - rewardUnits[i].totalAmount = perUnitAmount; - totalRewardUnits -= 1; - break; - } else { - step += totalRewardUnitsOfKind; - } - unchecked { - ++j; - } - } - unchecked { - ++i; - } - } - for (uint256 i; i < totalRewardKinds; ) { - if (_isUpdated[i]) { - _updateTokenInBundle(_token[i], _packId, i); - } - unchecked { - ++i; - } - } - } - - /*/////////////////////////////////////////////////////////////// - Getter functions - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the underlying contents of a pack. - function getPackContents( - uint256 _packId - ) public view returns (Token[] memory contents, uint256[] memory perUnitAmounts) { - PackInfo memory pack = packInfo[_packId]; - uint256 total = getTokenCountOfBundle(_packId); - contents = new Token[](total); - perUnitAmounts = new uint256[](total); - - for (uint256 i; i < total; ) { - contents[i] = getTokenOfBundle(_packId, i); - unchecked { - ++i; - } - } - perUnitAmounts = pack.perUnitAmounts; - } - - /*/////////////////////////////////////////////////////////////// - Internal functions - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns whether owner can be set in the given execution context. - function _canSetOwner() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Returns whether royalty info can be set in the given execution context. - function _canSetRoyaltyInfo() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Returns whether contract metadata can be set in the given execution context. - function _canSetContractURI() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /*/////////////////////////////////////////////////////////////// - Miscellaneous - //////////////////////////////////////////////////////////////*/ - - function generateRandomValue() internal view returns (uint256 random) { - random = uint256(keccak256(abi.encodePacked(_msgSender(), blockhash(block.number - 1), block.difficulty))); - } - - /** - * @dev See {ERC1155-_beforeTokenTransfer}. - */ - function _beforeTokenTransfer( - address operator, - address from, - address to, - uint256[] memory ids, - uint256[] memory amounts, - bytes memory data - ) internal virtual override { - super._beforeTokenTransfer(operator, from, to, ids, amounts, data); - - // if transfer is restricted on the contract, we still want to allow burning and minting - if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { - require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "!TRANSFER_ROLE"); - } - - if (from == address(0)) { - for (uint256 i = 0; i < ids.length; ++i) { - totalSupply[ids[i]] += amounts[i]; - } - } else { - for (uint256 i = 0; i < ids.length; ++i) { - // pack can no longer be updated after first transfer to non-zero address - if (canUpdatePack[ids[i]] && amounts[i] != 0) { - canUpdatePack[ids[i]] = false; - } - } - } - - if (to == address(0)) { - for (uint256 i = 0; i < ids.length; ++i) { - totalSupply[ids[i]] -= amounts[i]; - } - } - } - - /// @dev See EIP-2771 - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - /// @dev See EIP-2771 - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } -} diff --git a/contracts/prebuilts/pack/PackVRFDirect.sol b/contracts/prebuilts/pack/PackVRFDirect.sol deleted file mode 100644 index 541ceb904..000000000 --- a/contracts/prebuilts/pack/PackVRFDirect.sol +++ /dev/null @@ -1,516 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -/// @author thirdweb - -// $$\ $$\ $$\ $$\ $$\ -// $$ | $$ | \__| $$ | $$ | -// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ -// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ -// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | -// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | -// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | -// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ - -// ========== External imports ========== - -import "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155PausableUpgradeable.sol"; - -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; -import "@openzeppelin/contracts/interfaces/IERC721Receiver.sol"; -import { IERC1155Receiver } from "@openzeppelin/contracts/interfaces/IERC1155Receiver.sol"; - -import "../../external-deps/chainlink/VRFV2WrapperConsumerBase.sol"; - -// ========== Internal imports ========== - -import "../interface/IPackVRFDirect.sol"; -import "../../extension/Multicall.sol"; -import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; - -// ========== Features ========== - -import "../../extension/ContractMetadata.sol"; -import "../../extension/Royalty.sol"; -import "../../extension/Ownable.sol"; -import "../../extension/PermissionsEnumerable.sol"; -import { TokenStore, ERC1155Receiver } from "../../extension/TokenStore.sol"; - -/** - NOTE: This contract is a work in progress. - */ - -contract PackVRFDirect is - Initializable, - VRFV2WrapperConsumerBase, - ContractMetadata, - Ownable, - Royalty, - Permissions, - TokenStore, - ReentrancyGuardUpgradeable, - ERC2771ContextUpgradeable, - Multicall, - ERC1155Upgradeable, - IPackVRFDirect -{ - /*/////////////////////////////////////////////////////////////// - State variables - //////////////////////////////////////////////////////////////*/ - - bytes32 private constant MODULE_TYPE = bytes32("PackVRFDirect"); - uint256 private constant VERSION = 2; - - address private immutable forwarder; - - // Token name - string public name; - - // Token symbol - string public symbol; - - /// @dev Only MINTER_ROLE holders can create packs. - bytes32 private minterRole; - - /// @dev The token Id of the next set of packs to be minted. - uint256 public nextTokenIdToMint; - - /*/////////////////////////////////////////////////////////////// - Mappings - //////////////////////////////////////////////////////////////*/ - - /// @dev Mapping from token ID => total circulating supply of token with that ID. - mapping(uint256 => uint256) public totalSupply; - - /// @dev Mapping from pack ID => The state of that set of packs. - mapping(uint256 => PackInfo) private packInfo; - - /*/////////////////////////////////////////////////////////////// - VRF state - //////////////////////////////////////////////////////////////*/ - - uint32 private constant CALLBACKGASLIMIT = 100_000; - uint16 private constant REQUEST_CONFIRMATIONS = 3; - uint32 private constant NUMWORDS = 1; - - struct RequestInfo { - uint256 packId; - address opener; - uint256 amountToOpen; - uint256[] randomWords; - bool openOnFulfillRandomness; - } - - mapping(uint256 => RequestInfo) private requestInfo; - mapping(address => uint256) private openerToReqId; - - /*/////////////////////////////////////////////////////////////// - Constructor + initializer logic - //////////////////////////////////////////////////////////////*/ - - constructor( - address _nativeTokenWrapper, - address _trustedForwarder, - address _linkTokenAddress, - address _vrfV2Wrapper - ) VRFV2WrapperConsumerBase(_linkTokenAddress, _vrfV2Wrapper) TokenStore(_nativeTokenWrapper) initializer { - forwarder = _trustedForwarder; - } - - /// @dev Initializes the contract, like a constructor. - function initialize( - address _defaultAdmin, - string memory _name, - string memory _symbol, - string memory _contractURI, - address[] memory _trustedForwarders, - address _royaltyRecipient, - uint256 _royaltyBps - ) external initializer { - bytes32 _minterRole = keccak256("MINTER_ROLE"); - - /** note: The immutable state-variable `forwarder` is an EOA-only forwarder, - * which guards against automated attacks. - * - * Use other forwarders only if there's a strong reason to bypass this check. - */ - address[] memory forwarders = new address[](_trustedForwarders.length + 1); - uint256 i; - for (; i < _trustedForwarders.length; i++) { - forwarders[i] = _trustedForwarders[i]; - } - forwarders[i] = forwarder; - __ERC2771Context_init(forwarders); - __ERC1155_init(_contractURI); - - name = _name; - symbol = _symbol; - - _setupContractURI(_contractURI); - _setupOwner(_defaultAdmin); - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setupRole(_minterRole, _defaultAdmin); - - _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); - - minterRole = _minterRole; - } - - receive() external payable { - require(msg.sender == nativeTokenWrapper, "!nativeTokenWrapper."); - } - - /*/////////////////////////////////////////////////////////////// - Generic contract logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the type of the contract. - function contractType() external pure returns (bytes32) { - return MODULE_TYPE; - } - - /// @dev Returns the version of the contract. - function contractVersion() external pure returns (uint8) { - return uint8(VERSION); - } - - /*/////////////////////////////////////////////////////////////// - ERC 165 / 1155 / 2981 logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the URI for a given tokenId. - function uri(uint256 _tokenId) public view override returns (string memory) { - return getUriOfBundle(_tokenId); - } - - /// @dev See ERC 165 - function supportsInterface( - bytes4 interfaceId - ) public view virtual override(ERC1155Receiver, ERC1155Upgradeable, IERC165) returns (bool) { - return - super.supportsInterface(interfaceId) || - type(IERC2981Upgradeable).interfaceId == interfaceId || - type(IERC721Receiver).interfaceId == interfaceId || - type(IERC1155Receiver).interfaceId == interfaceId; - } - - /*/////////////////////////////////////////////////////////////// - Pack logic: create | open packs. - //////////////////////////////////////////////////////////////*/ - - /// @dev Creates a pack with the stated contents. - function createPack( - Token[] calldata _contents, - uint256[] calldata _numOfRewardUnits, - string memory _packUri, - uint128 _openStartTimestamp, - uint128 _amountDistributedPerOpen, - address _recipient - ) external payable onlyRole(minterRole) nonReentrant returns (uint256 packId, uint256 packTotalSupply) { - require(_contents.length > 0 && _contents.length == _numOfRewardUnits.length, "!Len"); - - packId = nextTokenIdToMint; - nextTokenIdToMint += 1; - - packTotalSupply = escrowPackContents( - _contents, - _numOfRewardUnits, - _packUri, - packId, - _amountDistributedPerOpen, - false - ); - - packInfo[packId].openStartTimestamp = _openStartTimestamp; - packInfo[packId].amountDistributedPerOpen = _amountDistributedPerOpen; - - // canUpdatePack[packId] = true; - - _mint(_recipient, packId, packTotalSupply, ""); - - emit PackCreated(packId, _recipient, packTotalSupply); - } - - /*/////////////////////////////////////////////////////////////// - VRF logic - //////////////////////////////////////////////////////////////*/ - - function openPackAndClaimRewards( - uint256 _packId, - uint256 _amountToOpen, - uint32 _callBackGasLimit - ) external returns (uint256) { - return _requestOpenPack(_packId, _amountToOpen, _callBackGasLimit, true); - } - - /// @notice Lets a pack owner open packs and receive the packs' reward units. - function openPack(uint256 _packId, uint256 _amountToOpen) external returns (uint256) { - return _requestOpenPack(_packId, _amountToOpen, CALLBACKGASLIMIT, false); - } - - function _requestOpenPack( - uint256 _packId, - uint256 _amountToOpen, - uint32 _callBackGasLimit, - bool _openOnFulfill - ) internal returns (uint256 requestId) { - address opener = _msgSender(); - - require(isTrustedForwarder(msg.sender) || opener == tx.origin, "!EOA"); - - require(openerToReqId[opener] == 0, "ReqInFlight"); - - require(_amountToOpen > 0 && balanceOf(opener, _packId) >= _amountToOpen, "!Bal"); - require(packInfo[_packId].openStartTimestamp <= block.timestamp, "!Open"); - - // Transfer packs into the contract. - _safeTransferFrom(opener, address(this), _packId, _amountToOpen, ""); - - // Request VRF for randomness. - requestId = requestRandomness(_callBackGasLimit, REQUEST_CONFIRMATIONS, NUMWORDS); - require(requestId > 0, "!VRF"); - - // Mark request as active; store request parameters. - requestInfo[requestId].packId = _packId; - requestInfo[requestId].opener = opener; - requestInfo[requestId].amountToOpen = _amountToOpen; - requestInfo[requestId].openOnFulfillRandomness = _openOnFulfill; - openerToReqId[opener] = requestId; - - emit PackOpenRequested(opener, _packId, _amountToOpen, requestId); - } - - /// @notice Called by Chainlink VRF to fulfill a random number request. - function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override { - RequestInfo memory info = requestInfo[_requestId]; - - require(info.randomWords.length == 0, "!Req"); - requestInfo[_requestId].randomWords = _randomWords; - - emit PackRandomnessFulfilled(info.packId, _requestId); - - if (info.openOnFulfillRandomness) { - try PackVRFDirect(payable(address(this))).sendRewardsIndirect(info.opener) {} catch {} - } - } - - /// @notice Returns whether a pack opener is ready to call `claimRewards`. - function canClaimRewards(address _opener) public view returns (bool) { - uint256 requestId = openerToReqId[_opener]; - return requestId > 0 && requestInfo[requestId].randomWords.length > 0; - } - - /// @notice Lets a pack owner open packs and receive the packs' reward units. - function claimRewards() external returns (Token[] memory) { - return _claimRewards(_msgSender()); - } - - /// @notice Lets a pack owner open packs and receive the packs' reward units. - function sendRewardsIndirect(address _opener) external { - require(msg.sender == address(this)); - _claimRewards(_opener); - } - - function _claimRewards(address opener) internal returns (Token[] memory) { - require(isTrustedForwarder(msg.sender) || msg.sender == address(this) || opener == tx.origin, "!EOA"); - require(canClaimRewards(opener), "!ActiveReq"); - uint256 reqId = openerToReqId[opener]; - RequestInfo memory info = requestInfo[reqId]; - - delete openerToReqId[opener]; - delete requestInfo[reqId]; - - PackInfo memory pack = packInfo[info.packId]; - - Token[] memory rewardUnits = getRewardUnits( - info.randomWords[0], - info.packId, - info.amountToOpen, - pack.amountDistributedPerOpen, - pack - ); - - // Burn packs. - _burn(address(this), info.packId, info.amountToOpen); - - _transferTokenBatch(address(this), opener, rewardUnits); - - emit PackOpened(info.packId, opener, info.amountToOpen, rewardUnits); - - return rewardUnits; - } - - /// @dev Stores assets within the contract. - function escrowPackContents( - Token[] calldata _contents, - uint256[] calldata _numOfRewardUnits, - string memory _packUri, - uint256 packId, - uint256 amountPerOpen, - bool isUpdate - ) internal returns (uint256 supplyToMint) { - uint256 sumOfRewardUnits; - - for (uint256 i = 0; i < _contents.length; i += 1) { - require(_contents[i].totalAmount != 0, "0 amt"); - require(_contents[i].totalAmount % _numOfRewardUnits[i] == 0, "!R"); - require(_contents[i].tokenType != TokenType.ERC721 || _contents[i].totalAmount == 1, "!R"); - - sumOfRewardUnits += _numOfRewardUnits[i]; - - packInfo[packId].perUnitAmounts.push(_contents[i].totalAmount / _numOfRewardUnits[i]); - } - - require(sumOfRewardUnits % amountPerOpen == 0, "!Amt"); - supplyToMint = sumOfRewardUnits / amountPerOpen; - - if (isUpdate) { - for (uint256 i = 0; i < _contents.length; i += 1) { - _addTokenInBundle(_contents[i], packId); - } - _transferTokenBatch(_msgSender(), address(this), _contents); - } else { - _storeTokens(_msgSender(), _contents, _packUri, packId); - } - } - - /// @dev Returns the reward units to distribute. - function getRewardUnits( - uint256 _random, - uint256 _packId, - uint256 _numOfPacksToOpen, - uint256 _rewardUnitsPerOpen, - PackInfo memory pack - ) internal returns (Token[] memory rewardUnits) { - uint256 numOfRewardUnitsToDistribute = _numOfPacksToOpen * _rewardUnitsPerOpen; - rewardUnits = new Token[](numOfRewardUnitsToDistribute); - uint256 totalRewardUnits = totalSupply[_packId] * _rewardUnitsPerOpen; - uint256 totalRewardKinds = getTokenCountOfBundle(_packId); - - (Token[] memory _token, ) = getPackContents(_packId); - bool[] memory _isUpdated = new bool[](totalRewardKinds); - for (uint256 i = 0; i < numOfRewardUnitsToDistribute; i += 1) { - uint256 randomVal = uint256(keccak256(abi.encode(_random, i))); - uint256 target = randomVal % totalRewardUnits; - uint256 step; - - for (uint256 j = 0; j < totalRewardKinds; j += 1) { - uint256 totalRewardUnitsOfKind = _token[j].totalAmount / pack.perUnitAmounts[j]; - - if (target < step + totalRewardUnitsOfKind) { - _token[j].totalAmount -= pack.perUnitAmounts[j]; - _isUpdated[j] = true; - - rewardUnits[i].assetContract = _token[j].assetContract; - rewardUnits[i].tokenType = _token[j].tokenType; - rewardUnits[i].tokenId = _token[j].tokenId; - rewardUnits[i].totalAmount = pack.perUnitAmounts[j]; - - totalRewardUnits -= 1; - - break; - } else { - step += totalRewardUnitsOfKind; - } - } - } - - for (uint256 i = 0; i < totalRewardKinds; i += 1) { - if (_isUpdated[i]) { - _updateTokenInBundle(_token[i], _packId, i); - } - } - } - - /*/////////////////////////////////////////////////////////////// - Getter functions - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the underlying contents of a pack. - function getPackContents( - uint256 _packId - ) public view returns (Token[] memory contents, uint256[] memory perUnitAmounts) { - PackInfo memory pack = packInfo[_packId]; - uint256 total = getTokenCountOfBundle(_packId); - contents = new Token[](total); - perUnitAmounts = new uint256[](total); - - for (uint256 i = 0; i < total; i += 1) { - contents[i] = getTokenOfBundle(_packId, i); - } - perUnitAmounts = pack.perUnitAmounts; - } - - /*/////////////////////////////////////////////////////////////// - Internal functions - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns whether owner can be set in the given execution context. - function _canSetOwner() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Returns whether royalty info can be set in the given execution context. - function _canSetRoyaltyInfo() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Returns whether contract metadata can be set in the given execution context. - function _canSetContractURI() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /*/////////////////////////////////////////////////////////////// - Miscellaneous - //////////////////////////////////////////////////////////////*/ - - /** - * @dev See {ERC1155-_beforeTokenTransfer}. - */ - function _beforeTokenTransfer( - address operator, - address from, - address to, - uint256[] memory ids, - uint256[] memory amounts, - bytes memory data - ) internal virtual override { - super._beforeTokenTransfer(operator, from, to, ids, amounts, data); - - if (from == address(0)) { - for (uint256 i = 0; i < ids.length; ++i) { - totalSupply[ids[i]] += amounts[i]; - } - } - - if (to == address(0)) { - for (uint256 i = 0; i < ids.length; ++i) { - totalSupply[ids[i]] -= amounts[i]; - } - } - } - - /// @dev See EIP-2771 - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - /// @dev See EIP-2771 - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } -} diff --git a/contracts/prebuilts/pack/pack.md b/contracts/prebuilts/pack/pack.md deleted file mode 100644 index 986805bf2..000000000 --- a/contracts/prebuilts/pack/pack.md +++ /dev/null @@ -1,261 +0,0 @@ -# Pack design document. - -This is a live document that explains what the [thirdweb](https://thirdweb.com/) `Pack` smart contract is, how it works and can be used, and why it is designed the way it is. - -The document is written for technical and non-technical readers. To ask further questions about thirdweb’s `Pack` contract, please join the [thirdweb discord](https://discord.gg/thirdweb) or create a github issue. - -# Background - -The thirdweb `Pack` contract is a lootbox mechanism. An account can bundle up arbitrary ERC20, ERC721 and ERC1155 tokens into a set of packs. A pack can then be opened in return for a selection of the tokens in the pack. The selection of tokens distributed on opening a pack depends on the relative supply of all tokens in the packs. - -> **IMPORTANT**: _Pack functions, such as opening of packs, can be costly in terms of gas usage due to random selection of rewards. Please check your gas estimates/usage, and do a trial on testnets before any mainnet deployment._ - -## Product: How packs _should_ work (without web3 terminology) - -Let's say we want to create a set of packs with three kinds of rewards - 80 **circles**, 15 **squares**, and 5 **stars** — and we want exactly 1 reward to be distributed when a pack is opened. - -In this case, with thirdweb’s `Pack` contract, each pack is guaranteed to yield exactly 1 reward. To deliver this guarantee, the number of packs created is equal to the sum of the supplies of each reward. So, we now have `80 + 15 + 5` i.e. `100` packs at hand. - -![pack-diag-1.png](/assets/pack-diag-1.png) - -On opening one of these 100 packs, the opener will receive one of the pack's rewards - either a **circle**, a **square**, or a **star**. The chances of receiving a particular reward is determined by how many of that reward exists across our set of packs. - -The percentage chance of receiving a particular kind of reward (e.g. a **star**) on opening a pack is calculated as:`(number_of_stars_packed) / (total number of packs)` - -In the beginning, 80 **circles**, 15 **squares**, and 5 **stars** exist across our set of 100 packs. That means the chances of receiving a **circle** upon opening a pack is `80/100` i.e. 80%. Similarly, a pack opener stands a 15% chance of receiving a **square**, and a 5% chance of receiving a **star** upon opening a pack. - -![pack-diag-2.png](/assets/pack-diag-2.png) - -The chances of receiving each kind of reward change as packs are opened. Let's say one of our 100 packs is opened, yielding a **circle**. We then have 99 packs remaining, with _79_ **circles**, 15 **squares**, and 5 **stars** packed. - -For the next pack that is opened, the opener will have a `79/99` i.e. around 79.8% chance of receiving a **circle**, around 15.2% chance of receiving a **square**, and around 5.1% chance of receiving a **star**. - -### Core parts of `Pack` as a product - -Given the above illustration of ‘how packs _should_ work’, we can now note down certain core parts of the `Pack` product, that any implementation of `Pack` should maintain: - -- A creator can pack arbitrary ERC20, ERC721 and ERC1155 tokens into a set of packs. -- The % chance of receiving a particular reward on opening a pack should be a function of the relative supplies of the rewards within a pack. That is, opening a pack _should not_ be like a lottery, where there’s an unchanging % chance of being distributed, assigned to rewards in a set of packs. -- A pack opener _should not_ be able to tell beforehand what reward they’ll receive on opening a pack. -- Each pack in a set of packs can be opened whenever the respective pack owner chooses to open the pack. -- Packs must be capable of being transferred and sold on a marketplace. - -## Why we’re building `Pack` - -Packs are designed to work as generic packs that contain rewards in them, where a pack can be opened to retrieve the rewards in that pack. - -Packs like these already exist as e.g. regular [Pokemon card packs](https://www.pokemoncenter.com/category/booster-packs), or in other forms that use blockchain technology, like [NBA Topshot](https://nbatopshot.com/) packs. This concept is ubiquitous across various cultures, sectors and products. - -As tokens continue to get legitimized as assets / items, we’re bringing ‘packs’ — a long-standing way of gamifying distribution of items — on-chain, as a primitive with a robust implementation that can be used across all chains, and for all kinds of use cases. - -# Technical details - -We’ll now go over the technical details of the `Pack` contract, with references to the example given in the previous section — ‘How packs work (without web3 terminology)’. - -## What can be packed in packs? - -You can create a set of packs with any combination of any number of ERC20, ERC721 and ERC1155 tokens. For example, you can create a set of packs with 10,000 [USDC](https://www.circle.com/en/usdc) (ERC20), 1 [Bored Ape Yacht Club](https://opensea.io/collection/boredapeyachtclub) NFT (ERC721), and 50 of [adidas originals’ first NFT](https://opensea.io/assets/0x28472a58a490c5e09a238847f66a68a47cc76f0f/0) (ERC1155). - -With strictly non-fungible tokens i.e. ERC721 NFTs, each NFT has a supply of 1. This means if a pack is opened and an ERC721 NFT is selected by the `Pack` contract to be distributed to the opener, that 1 NFT will be distributed to the opener. - -With fungible (ERC20) and semi-fungible (ERC1155) tokens, you must specify how many of those tokens must be distributed on opening a pack, as a unit. For example, if adding 10,000 USDC to a pack, you may specify that 20 USDC, as a unit, are meant to be distributed on opening a pack. This means you’re adding 500 units of 20 USDC to the set of packs you’re creating. - -And so, what can be packed in packs are _n_ number of configurations like ‘500 units of 20 USDC’. These configurations are interpreted by the `Pack` contract as `PackContent`: - -```solidity -enum TokenType { ERC20, ERC721, ERC1155 } - -struct Token { - address assetContract; - TokenType tokenType; - uint256 tokenId; - uint256 totalAmount; -} - -uint256 perUnitAmount; -``` - -| Value | Description | -| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| assetContract | The contract address of the token. | -| tokenType | The type of the token -- ERC20 / ERC721 / ERC1155 | -| tokenId | The tokenId of the token. (Not applicable for ERC20 tokens. The contract will ignore this value for ERC20 tokens.) | -| totalAmount | The total amount of this token packed in the pack. (Not applicable for ERC721 tokens. The contract will always consider this as 1 for ERC721 tokens.) | -| perUnitAmount | The amount of this token to distribute as a unit, on opening a pack. (Not applicable for ERC721 tokens. The contract will always consider this as 1 for ERC721 tokens.) | - -**Note:** A pack can contain different configurations for the same token. For example, the same set of packs can contain ‘500 units of 20 USDC’ and ‘10 units of 1000 USDC’ as two independent types of underlying rewards. - -## Creating packs - -You can create packs with any ERC20, ERC721 or ERC1155 tokens that you own. To create packs, you must specify the following: - -```solidity -/// @dev Creates a pack with the stated contents. -function createPack( - Token[] calldata contents, - uint256[] calldata numOfRewardUnits, - string calldata packUri, - uint128 openStartTimestamp, - uint128 amountDistributedPerOpen, - address recipient -) external -``` - -| Parameter | Description | -| ------------------------ | -------------------------------------------------------------------------------------------------------------- | -| contents | Tokens/assets packed in the set of pack. | -| numOfRewardUnits | Number of reward units for each asset, where each reward unit contains per unit amount of corresponding asset. | -| packUri | The (metadata) URI assigned to the packs created. | -| openStartTimestamp | The timestamp after which packs can be opened. | -| amountDistributedPerOpen | The number of reward units distributed per open. | -| recipient | The recipient of the packs created. | - -### Packs are ERC1155 tokens i.e. NFTs - -Packs themselves are ERC1155 tokens. And so, a set of packs created with your tokens is itself identified by a unique tokenId, has an associated metadata URI and a variable supply. - -In the example given in the previous section — ‘Non technical overview’, there is a set of 100 packs created, where that entire set of packs is identified by a unique tokenId. - -Since packs are ERC1155 tokens, you can publish multiple sets of packs using the same `Pack` contract. - -### Supply of packs - -When creating packs, you can specify the number of reward units to distribute to the opener on opening a pack. And so, when creating a set of packs, the total number of packs in that set is calculated as: - -`total_supply_of_packs = (total_reward_units) / (reward_units_to_distribute_per_open)` - -This guarantees that each pack can be opened to retrieve the intended _n_ reward units from inside the set of packs. - -## Updating packs - -You can add more contents to a created pack, up till the first transfer of packs. No addition can be made post that. - -```solidity -/// @dev Add contents to an existing packId. -function addPackContents( - uint256 packId, - Token[] calldata contents, - uint256[] calldata numOfRewardUnits, - address recipient -) external -``` - -| Parameter | Description | -| ---------------- | -------------------------------------------------------------------------------------------------------------- | -| PackId | The identifier of the pack to add contents to. | -| contents | Tokens/assets packed in the set of pack. | -| numOfRewardUnits | Number of reward units for each asset, where each reward unit contains per unit amount of corresponding asset. | -| recipient | The recipient of the new supply added. Should be the same address used during creation of packs. | - -## Opening packs - -Packs can be opened by owners of packs. A pack owner can open multiple packs at once. ‘Opening a pack’ essentially means burning the pack and receiving the intended _n_ number of reward units from inside the set of packs, in exchange. - -```solidity -function openPack(uint256 packId, uint256 amountToOpen) external; - -``` - -| Parameter | Description | -| ------------ | ------------------------------------ | -| packId | The identifier of the pack to open. | -| amountToOpen | The number of packs to open at once. | - -### How reward units are selected to distribute on opening packs - -We build on the example in the previous section — ‘Non-technical overview’. - -Each single **square**, **circle** or **star** is considered as a ‘reward unit’. For example, the 5 **stars** in the packs may be “5 units of 1000 USDC”, which is represented in the `Pack` contract by the following information - -```solidity -struct Token { - address assetContract; // USDC address - TokenType tokenType; // TokenType.ERC20 - uint256 tokenId; // Not applicable - uint256 totalAmount; // 5000 -} - -uint256 perUnitAmount; // 1000 -``` - -The percentage chance of receiving a particular kind of reward (e.g. a **star**) on opening a pack is calculated as:`(number_of_stars_packed) / (total number of packs)`. Here, `number_of_stars_packed` refers to the total number of reward units of the **star** kind inside the set of packs e.g. a total of 5 units of 1000 USDC. - -Going back to the example in the previous section — ‘Non-technical overview’. — the supply of the reward units in the relevant set of packs - 80 **circles**, 15 **squares**, and 5 **stars -** can be represented on a number line, from zero to the total supply of packs - in this case, 100. - -![pack-diag-2.png](/assets/pack-diag-2.png) - -Whenever a pack is opened, the `Pack` contract uses a new _random_ number in the range of the total supply of packs to determine what reward unit will be distributed to the pack opener. - -In our example case, the `Pack` contract uses a random number less than 100 to determine whether the pack opener will receive a **circle**, **square** or a **star**. - -So e.g. if the random number `num` is such that `0 <= num < 5`, the pack opener will receive a **star**. Similarly, if `5 <= num < 20`, the opener will receive a **square**, and if `20 <= num < 100`, the opener will receive a **circle**. - -Note that given this design, the opener truly has a 5% chance of receiving a **star**, a 15% chance of receiving a **square**, and an 80% chance of receiving a **circle**, as long as the random number used in the selection of the reward unit(s) to distribute is truly random. - -## The problem with random numbers - -From the previous section — ‘How reward units are selected to distribute on opening packs’: - -> Note that given this design, the opener truly has a 5% chance of receiving a **star**, a 15% chance of receiving a **square**, and an 80% chance of receiving a **circle**, as long as the random number used in the selection of the reward unit(s) to distribute is truly random. - -In the event of a pack opening, the random number used in the process affects what unit of reward is selected by the `Pack` contract to be distributed to the pack owner. - -If a pack owner can predict, at any moment, what random number will be used in this process of the contract selecting what unit of reward to distribute on opening a pack at that moment, the pack owner can selectively open their pack at a moment where they’ll receive the reward they want from the pack. - -This is a **possible** **critical vulnerability** since a core feature of the `Pack` product offering is the guarantee that each reward unit in a pack has a % probability of being distributed on opening a pack, and that this probability has some integrity (in the common sense way). Being able to predict the random numbers, as described above, overturns this guarantee. - -### Sourcing random numbers — solution - -The `Pack` contract requires a design where a pack owner _cannot possibly_ predict the random number that will be used in the process of their pack opening. - -To ensure the above, we make a simple check in the `openPack` function: - -```solidity -require(isTrustedForwarder(msg.sender) || _msgSender() == tx.origin, "opener cannot be smart contract"); -``` - -`tx.origin` returns the address of the external account that initiated the transaction, of which the `openPack` function call is a part of. - -The above check essentially means that only an external account i.e. an end user wallet, and no smart contract, can open packs. This lets us generate a pseudo random number using block variables, for the purpose of `openPack`: - -```solidity -uint256 random = uint256(keccak256(abi.encodePacked(_msgSender(), blockhash(block.number - 1), block.difficulty))); -``` - -Since only end user wallets can open packs, a pack owner _cannot possibly_ predict the random number that will be used in the process of their pack opening. That is because a pack opener cannot query the result of the random number calculation during a given block, and call `openPack` within that same block. - -We now list the single most important advantage, and consequent trade-off of using this solution: - -| Advantage | Trade-off | -| -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -| A pack owner cannot possibly predict the random number that will be used in the process of their pack opening. | Only external accounts / EOAs can open packs. Smart contracts cannot open packs. | - -### Sourcing random numbers — discarded solutions - -We’ll now discuss some possible solutions for this design problem along with their trade-offs / why we do not use these solutions: - -- **Using an oracle (e.g. Chainlink VRF)** - - Using an oracle like Chainlink VRF enables the original design for the `Pack` contract: a pack owner can open _n_ number of packs, whenever they want, independent of when the other pack owners choose to open their own packs. All in all — opening _n_ packs becomes a closed isolated event performed by a single pack owner. - - ![pack-diag-3.png](/assets/pack-diag-3.png) - - **Why we’re not using this solution:** - - - Chainlink VRF v1 is only on Ethereum and Polygon, and Chainlink VRF v2 (current version) is only on Ethereum and Binance. As a result, this solution cannot be used by itself across all the chains thirdweb supports (and wants to support). - - Each random number request costs an end user Chainlink’s LINK token — it is costly, and seems like a random requirement for using a thirdweb offering. - -- **Delayed-reveal randomness: rewards for all packs in a set of packs visible all at once** - By ‘delayed-reveal’ randomness, we mean the following — - - When creating a set of packs, the creator provides (1) an encrypted seed i.e. integer (see the [encryption pattern used in thirdweb’s delayed-reveal NFTs](https://blog.thirdweb.com/delayed-reveal-nfts#step-1-encryption)), and (2) a future block number. - - The created packs are _non-transferrable_ by any address except the (1) pack creator, or (2) addresses manually approved by the pack creator. This is to let the creator distribute packs as they desire, _and_ is essential for the next step. - - After the specified future block number passes, the creator submits the unencrypted seed to the `Pack` contract. Whenever a pack owner now opens a pack, we calculate the random number to be used in the opening process as follows: - ```solidity - uint256 random = uint(keccak256(seed, msg.sender, blockhash(storedBlockNumber))); - ``` - - No one can predict the block hash of the stored future block unless the pack creator is the miner of the block with that block number (highly unlikely). - - The seed is controlled by the creator, submitted at the time of pack creation, and cannot be changed after submission. - - Since packs are non-transferrable in the way described above, as long as the pack opener is not approved to transfer packs, the opener cannot manipulate the value of `random` by transferring packs to a desirable address and then opening the pack from that address. - **Why we’re not using this solution:** - - Active involvement from the pack creator. They’re trusted to reveal the unencrypted seed once packs are eligible to be opened. - - Packs _must_ be non-transferrable in the way described above, which means they can’t be purchased on a marketplace, etc. Lack of a built-in distribution mechanism for the packs. diff --git a/contracts/prebuilts/signature-drop/SignatureDrop.sol b/contracts/prebuilts/signature-drop/SignatureDrop.sol deleted file mode 100644 index b505940bb..000000000 --- a/contracts/prebuilts/signature-drop/SignatureDrop.sol +++ /dev/null @@ -1,371 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -/// @author thirdweb - -// $$\ $$\ $$\ $$\ $$\ -// $$ | $$ | \__| $$ | $$ | -// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ -// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ -// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | -// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | -// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | -// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ - -// ========== External imports ========== - -import "../../extension/Multicall.sol"; -import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; - -import "erc721a-upgradeable/contracts/ERC721AUpgradeable.sol"; - -// ========== Internal imports ========== - -import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; -import "../../lib/CurrencyTransferLib.sol"; - -// ========== Features ========== - -import "../../extension/ContractMetadata.sol"; -import "../../legacy-contracts/extension/PlatformFee_V1.sol"; -import "../../extension/Royalty.sol"; -import "../../legacy-contracts/extension/PrimarySale_V1.sol"; -import "../../extension/Ownable.sol"; -import "../../extension/DelayedReveal.sol"; -import "../../extension/LazyMint.sol"; -import "../../extension/PermissionsEnumerable.sol"; -import "../../extension/DropSinglePhase.sol"; -import "../../extension/SignatureMintERC721Upgradeable.sol"; - -contract SignatureDrop is - Initializable, - ContractMetadata, - PlatformFee, - Royalty, - PrimarySale, - Ownable, - DelayedReveal, - LazyMint, - PermissionsEnumerable, - DropSinglePhase, - SignatureMintERC721Upgradeable, - ERC2771ContextUpgradeable, - Multicall, - ERC721AUpgradeable -{ - using StringsUpgradeable for uint256; - - /*/////////////////////////////////////////////////////////////// - State variables - //////////////////////////////////////////////////////////////*/ - - /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. - bytes32 private transferRole; - /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s and lazy mint tokens. - bytes32 private minterRole; - - /// @dev Max bps in the thirdweb system. - uint256 private constant MAX_BPS = 10_000; - - /*/////////////////////////////////////////////////////////////// - Constructor + initializer logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Initializes the contract, like a constructor. - function initialize( - address _defaultAdmin, - string memory _name, - string memory _symbol, - string memory _contractURI, - address[] memory _trustedForwarders, - address _saleRecipient, - address _royaltyRecipient, - uint128 _royaltyBps, - uint128 _platformFeeBps, - address _platformFeeRecipient - ) external initializer { - bytes32 _transferRole = keccak256("TRANSFER_ROLE"); - bytes32 _minterRole = keccak256("MINTER_ROLE"); - - // Initialize inherited contracts, most base-like -> most derived. - __ERC2771Context_init(_trustedForwarders); - __ERC721A_init(_name, _symbol); - __SignatureMintERC721_init(); - - _setupContractURI(_contractURI); - _setupOwner(_defaultAdmin); - - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setupRole(_minterRole, _defaultAdmin); - _setupRole(_transferRole, _defaultAdmin); - _setupRole(_transferRole, address(0)); - - _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); - _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); - _setupPrimarySaleRecipient(_saleRecipient); - - transferRole = _transferRole; - minterRole = _minterRole; - } - - /*/////////////////////////////////////////////////////////////// - ERC 165 / 721 / 2981 logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the URI for a given tokenId. - function tokenURI(uint256 _tokenId) public view override returns (string memory) { - (uint256 batchId, ) = _getBatchId(_tokenId); - string memory batchUri = _getBaseURI(_tokenId); - - if (isEncryptedBatch(batchId)) { - return string(abi.encodePacked(batchUri, "0")); - } else { - return string(abi.encodePacked(batchUri, _tokenId.toString())); - } - } - - /// @dev See ERC 165 - function supportsInterface( - bytes4 interfaceId - ) public view virtual override(ERC721AUpgradeable, IERC165) returns (bool) { - return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; - } - - function contractType() external pure returns (bytes32) { - return bytes32("SignatureDrop"); - } - - function contractVersion() external pure returns (uint8) { - return uint8(5); - } - - /*/////////////////////////////////////////////////////////////// - Lazy minting + delayed-reveal logic - //////////////////////////////////////////////////////////////*/ - - /** - * @dev Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. - * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. - */ - function lazyMint( - uint256 _amount, - string calldata _baseURIForTokens, - bytes calldata _data - ) public override returns (uint256 batchId) { - if (_data.length > 0) { - (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(_data, (bytes, bytes32)); - if (encryptedURI.length != 0 && provenanceHash != "") { - _setEncryptedData(nextTokenIdToLazyMint + _amount, _data); - } - } - - return super.lazyMint(_amount, _baseURIForTokens, _data); - } - - /// @dev Lets an account with `MINTER_ROLE` reveal the URI for a batch of 'delayed-reveal' NFTs. - function reveal( - uint256 _index, - bytes calldata _key - ) external onlyRole(minterRole) returns (string memory revealedURI) { - uint256 batchId = getBatchIdAtIndex(_index); - revealedURI = getRevealURI(batchId, _key); - - _setEncryptedData(batchId, ""); - _setBaseURI(batchId, revealedURI); - - emit TokenURIRevealed(_index, revealedURI); - } - - /*/////////////////////////////////////////////////////////////// - Claiming lazy minted tokens logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Claim lazy minted tokens via signature. - function mintWithSignature( - MintRequest calldata _req, - bytes calldata _signature - ) external payable returns (address signer) { - uint256 tokenIdToMint = _currentIndex; - if (tokenIdToMint + _req.quantity > nextTokenIdToLazyMint) { - revert("!Tokens"); - } - - // Verify and process payload. - signer = _processRequest(_req, _signature); - - address receiver = _req.to; - - // Collect price - _collectPriceOnClaim(_req.primarySaleRecipient, _req.quantity, _req.currency, _req.pricePerToken); - - // Set royalties, if applicable. - if (_req.royaltyRecipient != address(0) && _req.royaltyBps != 0) { - _setupRoyaltyInfoForToken(tokenIdToMint, _req.royaltyRecipient, _req.royaltyBps); - } - - // Mint tokens. - _safeMint(receiver, _req.quantity); - - emit TokensMintedWithSignature(signer, receiver, tokenIdToMint, _req); - } - - /*/////////////////////////////////////////////////////////////// - Internal functions - //////////////////////////////////////////////////////////////*/ - - /// @dev Runs before every `claim` function call. - function _beforeClaim( - address, - uint256 _quantity, - address, - uint256, - AllowlistProof calldata, - bytes memory - ) internal view override { - require(_currentIndex + _quantity <= nextTokenIdToLazyMint, "!Tokens"); - } - - /// @dev Collects and distributes the primary sale value of NFTs being claimed. - function _collectPriceOnClaim( - address _primarySaleRecipient, - uint256 _quantityToClaim, - address _currency, - uint256 _pricePerToken - ) internal override { - if (_pricePerToken == 0) { - require(msg.value == 0, "!Value"); - return; - } - - (address platformFeeRecipient, uint16 platformFeeBps) = getPlatformFeeInfo(); - - address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; - - uint256 totalPrice = _quantityToClaim * _pricePerToken; - uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; - - if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { - require(msg.value == totalPrice, "!Price"); - } else { - require(msg.value == 0, "!Value"); - } - - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), saleRecipient, totalPrice - platformFees); - } - - /// @dev Transfers the NFTs being claimed. - function _transferTokensOnClaim( - address _to, - uint256 _quantityBeingClaimed - ) internal override returns (uint256 startTokenId) { - startTokenId = _currentIndex; - _safeMint(_to, _quantityBeingClaimed); - } - - /// @dev Returns whether a given address is authorized to sign mint requests. - function _isAuthorizedSigner(address _signer) internal view override returns (bool) { - return hasRole(minterRole, _signer); - } - - /// @dev Checks whether platform fee info can be set in the given execution context. - function _canSetPlatformFeeInfo() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether primary sale recipient can be set in the given execution context. - function _canSetPrimarySaleRecipient() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether owner can be set in the given execution context. - function _canSetOwner() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether royalty info can be set in the given execution context. - function _canSetRoyaltyInfo() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether contract metadata can be set in the given execution context. - function _canSetContractURI() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether platform fee info can be set in the given execution context. - function _canSetClaimConditions() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Returns whether lazy minting can be done in the given execution context. - function _canLazyMint() internal view virtual override returns (bool) { - return hasRole(minterRole, _msgSender()); - } - - /*/////////////////////////////////////////////////////////////// - Miscellaneous - //////////////////////////////////////////////////////////////*/ - - /** - * Returns the total amount of tokens minted in the contract. - */ - function totalMinted() external view returns (uint256) { - unchecked { - return _currentIndex - _startTokenId(); - } - } - - /// @dev The tokenId of the next NFT that will be minted / lazy minted. - function nextTokenIdToMint() external view returns (uint256) { - return nextTokenIdToLazyMint; - } - - /// @dev Burns `tokenId`. See {ERC721-_burn}. - function burn(uint256 tokenId) external virtual { - // note: ERC721AUpgradeable's `_burn(uint256,bool)` internally checks for token approvals. - _burn(tokenId, true); - } - - /// @dev See {ERC721-_beforeTokenTransfer}. - function _beforeTokenTransfers( - address from, - address to, - uint256 startTokenId, - uint256 quantity - ) internal virtual override { - super._beforeTokenTransfers(from, to, startTokenId, quantity); - - // if transfer is restricted on the contract, we still want to allow burning and minting - if (!hasRole(transferRole, address(0)) && from != address(0) && to != address(0)) { - if (!hasRole(transferRole, from) && !hasRole(transferRole, to)) { - revert("!Transfer-Role"); - } - } - } - - function _dropMsgSender() internal view virtual override returns (address) { - return _msgSender(); - } - - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } -} diff --git a/contracts/prebuilts/signature-drop/signatureDrop.md b/contracts/prebuilts/signature-drop/signatureDrop.md deleted file mode 100644 index 826b4c556..000000000 --- a/contracts/prebuilts/signature-drop/signatureDrop.md +++ /dev/null @@ -1,232 +0,0 @@ -# SignatureDrop design document. - -This is a live document that explains what the [thirdweb](https://thirdweb.com/) `SignatureDrop` smart contract is, how it works and can be used, and why it is designed the way it is. - -The document is written for technical and non-technical readers. To ask further questions about thirdweb’s `SignatureDrop` contract, please join the [thirdweb discord](https://discord.gg/thirdweb) or create a [github issue](https://github.com/thirdweb-dev/contracts/issues). - ---- - -## Background - -The thirdweb [`Drop`](https://portal.thirdweb.com/contracts/design/Drop) and [signature minting](https://portal.thirdweb.com/contracts/design/SignatureMint) are distribution mechanisms for tokens. - -The `Drop` contracts are meant to be used when the goal of the contract creator is for an audience to come in and claim tokens within certain restrictions e.g. — ‘only addresses in an allowlist can mint tokens’, or ‘minters must pay **x** amount of price in **y** currency to mint’, etc. - -Built-in contracts that implement [signature minting](https://portal.thirdweb.com/contracts/design/SignatureMint) are meant to be used when the restrictions around a wallet's minting tokens are not pre-defined, like in `Drop`. With signature minting, a contract creator can set custom restrictions around a token's minting, such as a price, at the very time that a wallet wants to mint tokens. - -The `SignatureDrop` contract supports both distribution mechanisms - of drop and signature minting - in the same contract. - -The contract creator 'lazy mints' i.e. defines the content for a batch of NFTs (yet un-minted). An NFT from this batch is distributed to a wallet in one of two ways: -1. claiming tokens under the restrictions defined in the time's active claim phase, as in `Drop`. -2. claiming tokens via a signed payload from a contract admin, as in 'signature minting'. - -### How `SignatureDrop` works - -![signature-drop-diag.png](/assets/signature-drop-diag.png) - -The `SignatureDrop` contract supports both distribution mechanisms - of drop and signature minting - in the same contract. The following is an end-to-end flow, from the contract admin actions, to an end user wallet's actions when minting tokens: - -- A contract admin (particularly, a wallet with `MINTER_ROLE`) 'lazy mints' i.e. defines the content for a batch of NFTs. This batch of NFTs can optionally be a batch of [delayed-reveal](https://blog.thirdweb.com/delayed-reveal-nfts) NFTs. -- A contract admin (particularly, a wallet with `DEFAULT_ADMIN_ROLE`) sets a claim phase, which defines restrictions around minting NFTs from the lazy minted batch of NFTs. - - **Note:** unlike the `NFT Drop` or `Edition Drop` contracts, where the contract admin can set a series of claim phases at once, the `SignatureDrop` contract lets the contract admin set only *one* claim phase at a time. -- A wallet claims tokens from the batch of lazy minted tokens in one of two ways: - - claiming tokens under the restrictions defined in the claim phase, as in `Drop`. - - claiming tokens via a signed payload from a contract admin, as in 'signature minting'. - -### Use cases for `SignatureDrop` - -We built our `Drop` contracts for the following [reason](https://portal.thirdweb.com/contracts/design/Drop#why-were-building-drop). The limitation of our `Drop` contracts is that all wallets in an audience attempting to claim tokens are subject to the same restrictions in the single, active claim phase at any moment. - -In the `SignatureDrop` contract, a wallet can now claim tokens [via a signature](https://portal.thirdweb.com/contracts/design/SignatureMint#background) from an authorized wallet, from the same pool of lazy minted tokens which can be claimed via the `Drop` mechanism. This means a contract owner can now set custom restrictions for a wallet to claim tokens, that may be different from the active claim phase at the time. - -An example of using this added feature of the `SignatureDrop` contract is when you want to maintain an allowlist off-chain i.e. not in the claim phase details, which are stored on-chain, and difficult to update once set. The contract's claim phase can define a common set of restrictions that any wallet not in your allowlist will mint tokens under. And using [signature minting](https://portal.thirdweb.com/contracts/design/SignatureMint), you can apply custom restrictions around minting, such as a price, currency and so on, on a per wallet basis, for wallets in your off-chain allowlist. - -## Technical Details - -`SignatureDrop` is an ERC721 contract. - -A contract admin can lazy mint tokens, and establish phases for an audience to come claim those tokens under the restrictions of the active phase at the time. On a per wallet basis, a contract admin can let a wallet claim those tokens under restrictions different than the active claim phase, via signature minting. - -### Batch upload of NFTs metadata: LazyMint - -The contract creator or an address with `MINTER_ROLE` mints *n* NFTs, by providing base URI for the tokens or an encrypted URI. -```solidity -function lazyMint( - uint256 _amount, - string calldata _baseURIForTokens, - bytes calldata _encryptedBaseURI -) external onlyRole(MINTER_ROLE) returns (uint256 batchId) -``` -| Parameters | Type | Description | -| --- | --- | --- | -| _amount | uint256 | Amount of tokens to lazy-mint. | -| _baseURIForTokens | string | The metadata URI for the batch of tokens. | -| _encryptedBaseURI | bytes | Encrypted URI for the batch of tokens. | - -### Delayed reveal - -An account with `MINTER_ROLE` can reveal the URI for a batch of ‘delayed-reveal’ NFTs. The URI can be revealed by calling the following function: -```solidity -function reveal(uint256 _index, bytes calldata _key) - external - onlyRole(MINTER_ROLE) - returns (string memory revealedURI) -``` -| Parameters | Type | Description | -| --- | --- | --- | -| _index | uint256 | Index of the batch for which URI is to be revealed. | -| _key | bytes | Key for decrypting the URI. | - -### Claiming tokens via signature - -An account with `MINTER_ROLE` signs the mint request for a user. The mint request is then submitted for claiming the tokens. The mint request is specified in the following format: -```solidity -struct MintRequest { - address to; - address royaltyRecipient; - uint256 royaltyBps; - address primarySaleRecipient; - string uri; - uint256 quantity; - uint256 pricePerToken; - address currency; - uint128 validityStartTimestamp; - uint128 validityEndTimestamp; - bytes32 uid; -} -``` -| Parameters | Type | Description | -| --- | --- | --- | -| to | address | The recipient of the tokens to mint. | -| royaltyRecipient | address | The recipient of the minted token's secondary sales royalties. | -| royaltyBps | uint256 | The percentage of the minted token's secondary sales to take as royalties. | -| primarySaleRecipient | address | The recipient of the minted token's primary sales proceeds. | -| uri | string | The metadata URI of the token to mint. | -| quantity | uint256 | The quantity of tokens to mint. | -| pricePerToken | uint256 | The price to pay per quantity of tokens minted. | -| currency | address | The currency in which to pay the price per token minted. | -| validityStartTimestamp | uint128 | The unix timestamp after which the payload is valid. | -| validityEndTimestamp | uint128 | The unix timestamp at which the payload expires. | -| uid | bytes32 | A unique identifier for the payload. | - -The authorized external party can mint the tokens by submitting mint-request and contract owner’s signature to the following function: -```solidity -function mintWithSignature( - ISignatureMintERC721.MintRequest calldata _req, - bytes calldata _signature -) external payable -``` -| Parameters | Type | Description | -| --- | --- | --- | -| _req | ISignatureMintERC721.MintRequest | Mint request in the format specified above. | -| _signature | bytes | Contact owner’s signature for the mint request. | - -### Setting claim conditions - -A contract admin (i.e. a holder of `DEFAULT_ADMIN_ROLE`) can set a *single* claim condition; this defines restrictions around claiming from the batch of lazy minted tokens. An active claim condition can be completely overwritten, or updated, by the contract admin. At any moment, there is only one active claim condition. - -A claim condition is specified in the following format: -```solidity -struct ClaimCondition { - uint256 startTimestamp; - uint256 maxClaimableSupply; - uint256 supplyClaimed; - uint256 quantityLimitPerTransaction; - uint256 waitTimeInSecondsBetweenClaims; - bytes32 merkleRoot; - uint256 pricePerToken; - address currency; -} -``` -| Parameters | Type | Description | -| --- | --- | --- | -| startTimestamp | uint256 | The unix timestamp after which the claim condition applies. The same claim condition applies until the startTimestamp of the next claim condition. | -| maxClaimableSupply | uint256 | The maximum total number of tokens that can be claimed under the claim condition. | -| supplyClaimed | uint256 | At any given point, the number of tokens that have been claimed under the claim condition. | -| quantityLimitPerTransaction | uint256 | The maximum number of tokens that can be claimed in a single transaction. | -| waitTimeInSecondsBetweenClaims | uint256 | The least number of seconds an account must wait after claiming tokens, to be able to claim tokens again.. | -| merkleRoot | bytes32 | The allowlist of addresses that can claim tokens under the claim condition. | -| pricePerToken | uint256 | The price required to pay per token claimed. | -| currency | address | The currency in which the pricePerToken must be paid. | - -Per wallet restrictions related to the claim condition are stored as follows: -```solidity -/** - * @dev Map from an account and uid for a claim condition, to the last timestamp - * at which the account claimed tokens under that claim condition. - */ - mapping(bytes32 => mapping(address => uint256)) private lastClaimTimestamp; - -/** - * @dev Map from a claim condition uid to whether an address in an allowlist - * has already claimed tokens i.e. used their place in the allowlist. - */ - mapping(bytes32 => BitMapsUpgradeable.BitMap) private usedAllowlistSpot; -``` -| Parameters | Type | Description | -| --- | --- | --- | -| lastClaimTimestamp | mapping(bytes32 => mapping(address => uint256)) | Map from an account and uid for a claim condition, to the last timestamp at which the account claimed tokens under that claim condition. | -| usedAllowlistSpot | mapping(bytes32 => BitMapsUpgradeable.BitMap) | Map from a uid for a claim condition to whether an address in an allowlist has already claimed tokens i.e. used their place in the allowlist. | - -**Note:** if a claim condition has an allowlist, a wallet can only use their spot in the condition's allowlist *once*. Allowlists can optionally specify the max amount of tokens each wallet in the allowlist can claim. A wallet in such an allowlist, too, can use their allowlist spot only *once*, regardless of the number of tokens they end up claiming. - -A contract admin sets claim conditions by calling the following function: -```solidity -/// @dev Lets a contract admin set claim conditions. -function setClaimConditions( - ClaimCondition calldata _condition, - bool _resetClaimEligibility, - bytes memory -) external override; -``` -| Parameter | Type | Description | -| --- | --- | --- | -| _condition | ClaimCondition | Defines restrictions around claiming lazy minted tokens. | -| resetClaimEligibility | bool | Whether to reset lastClaimTimestamp and usedAllowlistSpot values when setting a claim conditions. | - -You can read into the technical details of setting claim conditions in the [`Drop` design document](https://portal.thirdweb.com/contracts/design/Drop#setting-claim-conditions). - - -### Claiming tokens via `Drop` -An account can claim the tokens by calling the following function: -```solidity -function claim( - address _receiver, - uint256 _quantity, - address _currency, - uint256 _pricePerToken, - AllowlistProof calldata _allowlistProof, - bytes memory _data -) public payable; -``` -| Parameters | Type | Description | -| --- | --- | --- | -| _receiver | address | Mint request in the format specified above. | -| _quantity | uint256 | Contact owner’s signature for the mint request. | -| _currency | address | The currency in which the price must be paid. | -| _pricePerToken | uint256 | The price required to pay per token claimed. | -| _allowlistProof | AllowlistProof | The proof of the claimer's inclusion in the merkle root allowlist of the claim conditions that apply. | -| _data | bytes | Arbitrary bytes data that can be leveraged in the implementation of this interface. | - -## Permissions - -| Role name | Type (Switch / !Switch) | Purpose | -| -- | -- | -- | -| TRANSFER_ROLE | Switch | Only token transfers to or from role holders are allowed. Minting and burning are not affected. | -| MINTER_ROLE | !Switch | Only MINTER_ROLE holders can sign off on MintRequests and lazy mint tokens. | - -What does **Type (Switch / !Switch)** mean? -- **Switch:** If `address(0)` has `ROLE`, then the `ROLE` restrictions don't apply. -- **!Switch:** `ROLE` restrictions always apply. - -## Relevant EIPs - -| EIP | Link | Relation to SignatureDrop | -| --- | --- | --- | -| 721 | https://eips.ethereum.org/EIPS/eip-721 | `SignatureDrop` is an ERC721 contract. | -| 2981 | https://eips.ethereum.org/EIPS/eip-2981 | `SignatureDrop` implements ERC 2981 for distributing royalties for sales of the wrapped NFTs. | -| 2771 | https://eips.ethereum.org/EIPS/eip-2771 | `SignatureDrop` implements ERC 2771 to support meta-transactions (aka “gasless” transactions). | - -## Authors -- [kumaryash90](https://github.com/kumaryash90) -- [thirdweb team](https://github.com/thirdweb-dev) \ No newline at end of file diff --git a/contracts/prebuilts/tiered-drop/TieredDrop.sol b/contracts/prebuilts/tiered-drop/TieredDrop.sol deleted file mode 100644 index f52c82b17..000000000 --- a/contracts/prebuilts/tiered-drop/TieredDrop.sol +++ /dev/null @@ -1,607 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.11; - -/// @author thirdweb - -// $$\ $$\ $$\ $$\ $$\ -// $$ | $$ | \__| $$ | $$ | -// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ -// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ -// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | -// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | -// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | -// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ - -// ========== External imports ========== - -import "../../extension/Multicall.sol"; -import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; - -import "erc721a-upgradeable/contracts/ERC721AUpgradeable.sol"; - -// ========== Internal imports ========== - -import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; -import "../../lib/CurrencyTransferLib.sol"; - -// ========== Features ========== - -import "../../extension/ContractMetadata.sol"; -import "../../extension/PlatformFee.sol"; -import "../../extension/Royalty.sol"; -import "../../extension/PrimarySale.sol"; -import "../../extension/Ownable.sol"; -import "../../extension/DelayedReveal.sol"; -import "../../extension/PermissionsEnumerable.sol"; - -// ========== New Features ========== - -import "../../extension/LazyMintWithTier.sol"; -import "../../extension/SignatureActionUpgradeable.sol"; - -contract TieredDrop is - Initializable, - ContractMetadata, - Royalty, - PrimarySale, - Ownable, - DelayedReveal, - LazyMintWithTier, - PermissionsEnumerable, - SignatureActionUpgradeable, - ERC2771ContextUpgradeable, - Multicall, - ERC721AUpgradeable -{ - using StringsUpgradeable for uint256; - - /*/////////////////////////////////////////////////////////////// - State variables - //////////////////////////////////////////////////////////////*/ - - /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. - bytes32 private transferRole; - /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s and lazy mint tokens. - bytes32 private minterRole; - - /** - * @dev Conceptually, tokens are minted on this contract one-batch-of-a-tier at a time. Each batch is comprised of - * a given range of tokenIds [startId, endId). - * - * This array stores each such endId, in chronological order of minting. - */ - uint256 private lengthEndIdsAtMint; - mapping(uint256 => uint256) private endIdsAtMint; - - /** - * @dev Conceptually, tokens are minted on this contract one-batch-of-a-tier at a time. Each batch is comprised of - * a given range of tokenIds [startId, endId). - * - * This is a mapping from such an `endId` -> the tier that tokenIds [startId, endId) belong to. - * Together with `endIdsAtMint`, this mapping is used to return the tokenIds that belong to a given tier. - */ - mapping(uint256 => string) private tierAtEndId; - - /** - * @dev This contract lets an admin lazy mint batches of metadata at once, for a given tier. E.g. an admin may lazy mint - * the metadata of 5000 tokens that will actually be minted in the future. - * - * Lazy minting of NFT metafata happens from a start metadata ID (inclusive) to an end metadata ID (non-inclusive), - * where the lazy minted metadata lives at `providedBaseURI/${metadataId}` for each unit metadata. - * - * At the time of actual minting, the minter specifies the tier of NFTs they're minting. So, the order in which lazy minted - * metadata for a tier is assigned integer IDs may differ from the actual tokenIds minted for a tier. - * - * This is a mapping from an actually minted end tokenId -> the range of lazy minted metadata that now belongs - * to NFTs of [start tokenId, end tokenid). - */ - mapping(uint256 => TokenRange) private proxyTokenRange; - - /// @dev Mapping from tier -> the metadata ID up till which metadata IDs have been mapped to minted NFTs' tokenIds. - mapping(string => uint256) private nextMetadataIdToMapFromTier; - - /// @dev Mapping from tier -> how many units of lazy minted metadata have not yet been mapped to minted NFTs' tokenIds. - mapping(string => uint256) private totalRemainingInTier; - - /// @dev Mapping from batchId => tokenId offset for that batchId. - mapping(uint256 => bytes32) private tokenIdOffset; - - /// @dev Mapping from hash(tier, "minted") -> total minted in tier. - mapping(bytes32 => uint256) private totalsForTier; - - /*/////////////////////////////////////////////////////////////// - Events - //////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when tokens are claimed via `claimWithSignature`. - event TokensClaimed( - address indexed claimer, - address indexed receiver, - uint256 startTokenId, - uint256 quantityClaimed, - string[] tiersInPriority - ); - - /*/////////////////////////////////////////////////////////////// - Constructor + initializer logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Initializes the contract, like a constructor. - function initialize( - address _defaultAdmin, - string memory _name, - string memory _symbol, - string memory _contractURI, - address[] memory _trustedForwarders, - address _saleRecipient, - address _royaltyRecipient, - uint16 _royaltyBps - ) external initializer { - bytes32 _transferRole = keccak256("TRANSFER_ROLE"); - bytes32 _minterRole = keccak256("MINTER_ROLE"); - - // Initialize inherited contracts, most base-like -> most derived. - __ERC2771Context_init(_trustedForwarders); - __ERC721A_init(_name, _symbol); - __SignatureAction_init(); - - _setupContractURI(_contractURI); - _setupOwner(_defaultAdmin); - - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setupRole(_minterRole, _defaultAdmin); - _setupRole(_transferRole, _defaultAdmin); - _setupRole(_transferRole, address(0)); - - _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); - _setupPrimarySaleRecipient(_saleRecipient); - - transferRole = _transferRole; - minterRole = _minterRole; - } - - /*/////////////////////////////////////////////////////////////// - ERC 165 / 721 / 2981 logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Returns the URI for a given tokenId. - function tokenURI(uint256 _tokenId) public view override returns (string memory) { - // Retrieve metadata ID for token. - uint256 metadataId = _getMetadataId(_tokenId); - - // Use metadata ID to return token metadata. - (uint256 batchId, uint256 index) = _getBatchId(metadataId); - string memory batchUri = _getBaseURI(metadataId); - - if (isEncryptedBatch(batchId)) { - return string(abi.encodePacked(batchUri, "0")); - } else { - uint256 fairMetadataId = _getFairMetadataId(metadataId, batchId, index); - return string(abi.encodePacked(batchUri, fairMetadataId.toString())); - } - } - - /// @dev See ERC 165 - function supportsInterface( - bytes4 interfaceId - ) public view virtual override(ERC721AUpgradeable, IERC165) returns (bool) { - return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; - } - - /*/////////////////////////////////////////////////////////////// - Lazy minting + delayed-reveal logic - //////////////////////////////////////////////////////////////* - - /** - * @dev Lets an account with `MINTER_ROLE` lazy mint 'n' NFTs. - * The URIs for each token is the provided `_baseURIForTokens` + `{tokenId}`. - */ - function lazyMint( - uint256 _amount, - string calldata _baseURIForTokens, - string calldata _tier, - bytes calldata _data - ) public override returns (uint256 batchId) { - if (_data.length > 0) { - (bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(_data, (bytes, bytes32)); - if (encryptedURI.length != 0 && provenanceHash != "") { - _setEncryptedData(nextTokenIdToLazyMint + _amount, _data); - } - } - - totalRemainingInTier[_tier] += _amount; - - uint256 startId = nextTokenIdToLazyMint; - if (isTierEmpty(_tier) || nextMetadataIdToMapFromTier[_tier] == type(uint256).max) { - nextMetadataIdToMapFromTier[_tier] = startId; - } - - return super.lazyMint(_amount, _baseURIForTokens, _tier, _data); - } - - /// @dev Lets an account with `MINTER_ROLE` reveal the URI for a batch of 'delayed-reveal' NFTs. - function reveal( - uint256 _index, - bytes calldata _key - ) external onlyRole(minterRole) returns (string memory revealedURI) { - uint256 batchId = getBatchIdAtIndex(_index); - revealedURI = getRevealURI(batchId, _key); - - _setEncryptedData(batchId, ""); - _setBaseURI(batchId, revealedURI); - - _scrambleOffset(batchId, _key); - - emit TokenURIRevealed(_index, revealedURI); - } - - /*/////////////////////////////////////////////////////////////// - Claiming lazy minted tokens logic - //////////////////////////////////////////////////////////////*/ - - /// @dev Claim lazy minted tokens via signature. - function claimWithSignature( - GenericRequest calldata _req, - bytes calldata _signature - ) external payable returns (address signer) { - ( - string[] memory tiersInPriority, - address to, - address royaltyRecipient, - uint256 royaltyBps, - address primarySaleRecipient, - uint256 quantity, - uint256 totalPrice, - address currency - ) = abi.decode(_req.data, (string[], address, address, uint256, address, uint256, uint256, address)); - - if (quantity == 0) { - revert("0 qty"); - } - - uint256 tokenIdToMint = _currentIndex; - if (tokenIdToMint + quantity > nextTokenIdToLazyMint) { - revert("!Tokens"); - } - - // Verify and process payload. - signer = _processRequest(_req, _signature); - - // Collect price - collectPriceOnClaim(primarySaleRecipient, currency, totalPrice); - - // Set royalties, if applicable. - if (royaltyRecipient != address(0) && royaltyBps != 0) { - _setupRoyaltyInfoForToken(tokenIdToMint, royaltyRecipient, royaltyBps); - } - - // Mint tokens. - transferTokensOnClaim(to, quantity, tiersInPriority); - - emit TokensClaimed(_msgSender(), to, tokenIdToMint, quantity, tiersInPriority); - } - - /*/////////////////////////////////////////////////////////////// - Internal functions - //////////////////////////////////////////////////////////////*/ - /// @dev Collects and distributes the primary sale value of NFTs being claimed. - function collectPriceOnClaim(address _primarySaleRecipient, address _currency, uint256 _totalPrice) internal { - if (_totalPrice == 0) { - require(msg.value == 0, "!Value"); - return; - } - - address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; - - bool validMsgValue; - if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { - validMsgValue = msg.value == _totalPrice; - } else { - validMsgValue = msg.value == 0; - } - require(validMsgValue, "Invalid msg value"); - - CurrencyTransferLib.transferCurrency(_currency, _msgSender(), saleRecipient, _totalPrice); - } - - /// @dev Transfers the NFTs being claimed. - function transferTokensOnClaim(address _to, uint256 _totalQuantityBeingClaimed, string[] memory _tiers) internal { - uint256 startTokenIdToMint = _currentIndex; - - uint256 startIdToMap = startTokenIdToMint; - uint256 remaningToDistribute = _totalQuantityBeingClaimed; - - for (uint256 i = 0; i < _tiers.length; i += 1) { - string memory tier = _tiers[i]; - - uint256 qtyFulfilled = _getQuantityFulfilledByTier(tier, remaningToDistribute); - - if (qtyFulfilled == 0) { - continue; - } - - remaningToDistribute -= qtyFulfilled; - - _mapTokensToTier(tier, startIdToMap, qtyFulfilled); - - totalRemainingInTier[tier] -= qtyFulfilled; - totalsForTier[keccak256(abi.encodePacked(tier, "minted"))] += qtyFulfilled; - - if (remaningToDistribute > 0) { - startIdToMap += qtyFulfilled; - } else { - break; - } - } - - require(remaningToDistribute == 0, "Insufficient tokens in tiers."); - - _safeMint(_to, _totalQuantityBeingClaimed); - } - - /// @dev Maps lazy minted metadata to NFT tokenIds. - function _mapTokensToTier(string memory _tier, uint256 _startIdToMap, uint256 _quantity) private { - uint256 nextIdFromTier = nextMetadataIdToMapFromTier[_tier]; - uint256 startTokenId = _startIdToMap; - - TokenRange[] memory tokensInTier = tokensInTier[_tier]; - uint256 len = tokensInTier.length; - - uint256 qtyRemaining = _quantity; - - for (uint256 i = 0; i < len; i += 1) { - TokenRange memory range = tokensInTier[i]; - uint256 gap = 0; - - if (range.startIdInclusive <= nextIdFromTier && nextIdFromTier < range.endIdNonInclusive) { - uint256 proxyStartId = nextIdFromTier; - uint256 proxyEndId = proxyStartId + qtyRemaining <= range.endIdNonInclusive - ? proxyStartId + qtyRemaining - : range.endIdNonInclusive; - - gap = proxyEndId - proxyStartId; - - uint256 endTokenId = startTokenId + gap; - - endIdsAtMint[lengthEndIdsAtMint] = endTokenId; - lengthEndIdsAtMint += 1; - - tierAtEndId[endTokenId] = _tier; - proxyTokenRange[endTokenId] = TokenRange(proxyStartId, proxyEndId); - - startTokenId += gap; - qtyRemaining -= gap; - - if (nextIdFromTier + gap < range.endIdNonInclusive) { - nextIdFromTier += gap; - } else if (i < (len - 1)) { - nextIdFromTier = tokensInTier[i + 1].startIdInclusive; - } else { - nextIdFromTier = type(uint256).max; - } - } - - if (qtyRemaining == 0) { - nextMetadataIdToMapFromTier[_tier] = nextIdFromTier; - break; - } - } - } - - /// @dev Returns how much of the total-quantity-to-distribute can come from the given tier. - function _getQuantityFulfilledByTier( - string memory _tier, - uint256 _quantity - ) private view returns (uint256 fulfilled) { - uint256 total = totalRemainingInTier[_tier]; - - if (total >= _quantity) { - fulfilled = _quantity; - } else { - fulfilled = total; - } - } - - /// @dev Returns the tier that the given token is associated with. - function getTierForToken(uint256 _tokenId) external view returns (string memory) { - uint256 len = lengthEndIdsAtMint; - - for (uint256 i = 0; i < len; i += 1) { - uint256 endId = endIdsAtMint[i]; - - if (_tokenId < endId) { - return tierAtEndId[endId]; - } - } - - revert("!Tier"); - } - - /// @dev Returns the max `endIndex` that can be used with getTokensInTier. - function getTokensInTierLen() external view returns (uint256) { - return lengthEndIdsAtMint; - } - - /// @dev Returns all tokenIds that belong to the given tier. - function getTokensInTier( - string memory _tier, - uint256 _startIdx, - uint256 _endIdx - ) external view returns (TokenRange[] memory ranges) { - uint256 len = lengthEndIdsAtMint; - - require(_startIdx < _endIdx && _endIdx <= len, "TieredDrop: invalid indices."); - - uint256 numOfRangesForTier = 0; - bytes32 hashOfTier = keccak256(abi.encodePacked(_tier)); - - for (uint256 i = _startIdx; i < _endIdx; i += 1) { - bytes32 hashOfStoredTier = keccak256(abi.encodePacked(tierAtEndId[endIdsAtMint[i]])); - - if (hashOfStoredTier == hashOfTier) { - numOfRangesForTier += 1; - } - } - - ranges = new TokenRange[](numOfRangesForTier); - uint256 idx = 0; - - for (uint256 i = _startIdx; i < _endIdx; i += 1) { - bytes32 hashOfStoredTier = keccak256(abi.encodePacked(tierAtEndId[endIdsAtMint[i]])); - - if (hashOfStoredTier == hashOfTier) { - uint256 end = endIdsAtMint[i]; - - uint256 start = 0; - if (i > 0) { - start = endIdsAtMint[i - 1]; - } - - ranges[idx] = TokenRange(start, end); - idx += 1; - } - } - } - - /// @dev Returns the metadata ID for the given tokenID. - function _getMetadataId(uint256 _tokenId) private view returns (uint256) { - uint256 len = lengthEndIdsAtMint; - - for (uint256 i = 0; i < len; i += 1) { - if (_tokenId < endIdsAtMint[i]) { - uint256 targetEndId = endIdsAtMint[i]; - uint256 diff = targetEndId - _tokenId; - - TokenRange memory range = proxyTokenRange[targetEndId]; - - return range.endIdNonInclusive - diff; - } - } - - revert("!Metadata-ID"); - } - - /// @dev Returns the fair metadata ID for a given tokenId. - function _getFairMetadataId( - uint256 _metadataId, - uint256 _batchId, - uint256 _indexOfBatchId - ) private view returns (uint256 fairMetadataId) { - bytes32 bytesRandom = tokenIdOffset[_batchId]; - if (bytesRandom == bytes32(0)) { - return _metadataId; - } - - uint256 randomness = uint256(bytesRandom); - uint256 prevBatchId; - if (_indexOfBatchId > 0) { - prevBatchId = getBatchIdAtIndex(_indexOfBatchId - 1); - } - - uint256 batchSize = _batchId - prevBatchId; - uint256 offset = randomness % batchSize; - fairMetadataId = prevBatchId + ((_metadataId + offset) % batchSize); - } - - /// @dev Scrambles tokenId offset for a given batchId. - function _scrambleOffset(uint256 _batchId, bytes calldata _seed) private { - tokenIdOffset[_batchId] = keccak256(abi.encodePacked(_seed, block.timestamp, blockhash(block.number - 1))); - } - - /// @dev Returns whether a given address is authorized to sign mint requests. - function _isAuthorizedSigner(address _signer) internal view override returns (bool) { - return hasRole(minterRole, _signer); - } - - /// @dev Checks whether primary sale recipient can be set in the given execution context. - function _canSetPrimarySaleRecipient() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether owner can be set in the given execution context. - function _canSetOwner() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether royalty info can be set in the given execution context. - function _canSetRoyaltyInfo() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Checks whether contract metadata can be set in the given execution context. - function _canSetContractURI() internal view override returns (bool) { - return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); - } - - /// @dev Returns whether lazy minting can be done in the given execution context. - function _canLazyMint() internal view virtual override returns (bool) { - return hasRole(minterRole, _msgSender()); - } - - /*/////////////////////////////////////////////////////////////// - Miscellaneous - //////////////////////////////////////////////////////////////*/ - - /** - * @dev Returns the total amount of tokens minted in the contract. - */ - function totalMinted() external view returns (uint256) { - unchecked { - return _currentIndex - _startTokenId(); - } - } - - /// @dev Returns the total number of tokens minted from the given tier. - function totalMintedInTier(string memory _tier) external view returns (uint256) { - return totalsForTier[keccak256(abi.encodePacked(_tier, "minted"))]; - } - - /// @dev The tokenId of the next NFT that will be minted / lazy minted. - function nextTokenIdToMint() external view returns (uint256) { - return nextTokenIdToLazyMint; - } - - /// @dev Burns `tokenId`. See {ERC721-_burn}. - function burn(uint256 tokenId) external virtual { - // note: ERC721AUpgradeable's `_burn(uint256,bool)` internally checks for token approvals. - _burn(tokenId, true); - } - - /// @dev See {ERC721-_beforeTokenTransfer}. - function _beforeTokenTransfers( - address from, - address to, - uint256 startTokenId, - uint256 quantity - ) internal virtual override { - super._beforeTokenTransfers(from, to, startTokenId, quantity); - - // if transfer is restricted on the contract, we still want to allow burning and minting - if (!hasRole(transferRole, address(0)) && from != address(0) && to != address(0)) { - if (!hasRole(transferRole, from) && !hasRole(transferRole, to)) { - revert("!TRANSFER"); - } - } - } - - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable, Multicall) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } -} diff --git a/lib/chainlink b/lib/chainlink deleted file mode 160000 index 5d44bd4e8..000000000 --- a/lib/chainlink +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5d44bd4e8fa2bdc80228a0df891960d72246b645 diff --git a/src/test/SignatureDrop.t.sol b/src/test/SignatureDrop.t.sol deleted file mode 100644 index f26f82022..000000000 --- a/src/test/SignatureDrop.t.sol +++ /dev/null @@ -1,1370 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -import { SignatureDrop, DropSinglePhase, Permissions, LazyMint, BatchMintMetadata, DelayedReveal, IDropSinglePhase, IDelayedReveal, ISignatureMintERC721, ERC721AUpgradeable, IPermissions, ILazyMint } from "contracts/prebuilts/signature-drop/SignatureDrop.sol"; -import { SignatureMintERC721 } from "contracts/extension/SignatureMintERC721.sol"; - -// Test imports -import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; -import "./utils/BaseTest.sol"; -import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; - -contract SignatureDropTest is BaseTest { - using Strings for uint256; - using Strings for address; - - event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); - event TokenURIRevealed(uint256 indexed index, string revealedURI); - event TokensMintedWithSignature( - address indexed signer, - address indexed mintedTo, - uint256 indexed tokenIdMinted, - SignatureDrop.MintRequest mintRequest - ); - - SignatureDrop public sigdrop; - address internal deployerSigner; - bytes32 internal typehashMintRequest; - bytes32 internal nameHash; - bytes32 internal versionHash; - bytes32 internal typehashEip712; - bytes32 internal domainSeparator; - - bytes private emptyEncodedBytes = abi.encode("", ""); - - using stdStorage for StdStorage; - - function setUp() public override { - super.setUp(); - deployerSigner = signer; - sigdrop = SignatureDrop(getContract("SignatureDrop")); - - erc20.mint(deployerSigner, 1_000 ether); - vm.deal(deployerSigner, 1_000 ether); - - typehashMintRequest = keccak256( - "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" - ); - nameHash = keccak256(bytes("SignatureMintERC721")); - versionHash = keccak256(bytes("1")); - typehashEip712 = keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ); - domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(sigdrop))); - } - - /*/////////////////////////////////////////////////////////////// - Unit tests: misc. - //////////////////////////////////////////////////////////////*/ - - /** - * note: Tests whether contract reverts when a non-holder renounces a role. - */ - function test_revert_nonHolder_renounceRole() public { - address caller = address(0x123); - bytes32 role = keccak256("MINTER_ROLE"); - - vm.prank(caller); - vm.expectRevert(); - - sigdrop.renounceRole(role, caller); - } - - /** - * note: Tests whether contract reverts when a role admin revokes a role for a non-holder. - */ - function test_revert_revokeRoleForNonHolder() public { - address target = address(0x123); - bytes32 role = keccak256("MINTER_ROLE"); - - vm.prank(deployerSigner); - vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsUnauthorizedAccount.selector, target, role)); - - sigdrop.revokeRole(role, target); - } - - /** - * @dev Tests whether contract reverts when a role is granted to an existent role holder. - */ - function test_revert_grant_role_to_account_with_role() public { - bytes32 role = keccak256("ABC_ROLE"); - address receiver = getActor(0); - - vm.startPrank(deployerSigner); - - sigdrop.grantRole(role, receiver); - - vm.expectRevert(); - sigdrop.grantRole(role, receiver); - - vm.stopPrank(); - } - - /** - * @dev Tests contract state for Transfer role. - */ - function test_state_grant_transferRole() public { - bytes32 role = keccak256("TRANSFER_ROLE"); - - // check if admin and address(0) have transfer role in the beginning - bool checkAddressZero = sigdrop.hasRole(role, address(0)); - bool checkAdmin = sigdrop.hasRole(role, deployerSigner); - assertTrue(checkAddressZero); - assertTrue(checkAdmin); - - // check if transfer role can be granted to a non-holder - address receiver = getActor(0); - vm.startPrank(deployerSigner); - sigdrop.grantRole(role, receiver); - - // expect revert when granting to a holder - vm.expectRevert(abi.encodeWithSelector(Permissions.PermissionsAlreadyGranted.selector, receiver, role)); - sigdrop.grantRole(role, receiver); - - // check if receiver has transfer role - bool checkReceiver = sigdrop.hasRole(role, receiver); - assertTrue(checkReceiver); - - // check if role is correctly revoked - sigdrop.revokeRole(role, receiver); - checkReceiver = sigdrop.hasRole(role, receiver); - assertFalse(checkReceiver); - sigdrop.revokeRole(role, address(0)); - checkAddressZero = sigdrop.hasRole(role, address(0)); - assertFalse(checkAddressZero); - - vm.stopPrank(); - } - - /** - * @dev Tests contract state for Transfer role. - */ - function test_state_getRoleMember_transferRole() public { - bytes32 role = keccak256("TRANSFER_ROLE"); - - uint256 roleMemberCount = sigdrop.getRoleMemberCount(role); - assertEq(roleMemberCount, 2); - - address roleMember = sigdrop.getRoleMember(role, 1); - assertEq(roleMember, address(0)); - - vm.startPrank(deployerSigner); - sigdrop.grantRole(role, address(2)); - sigdrop.grantRole(role, address(3)); - sigdrop.grantRole(role, address(4)); - - roleMemberCount = sigdrop.getRoleMemberCount(role); - console.log(roleMemberCount); - for (uint256 i = 0; i < roleMemberCount; i++) { - console.log(sigdrop.getRoleMember(role, i)); - } - console.log(""); - - sigdrop.revokeRole(role, address(2)); - roleMemberCount = sigdrop.getRoleMemberCount(role); - console.log(roleMemberCount); - for (uint256 i = 0; i < roleMemberCount; i++) { - console.log(sigdrop.getRoleMember(role, i)); - } - console.log(""); - - sigdrop.revokeRole(role, address(0)); - roleMemberCount = sigdrop.getRoleMemberCount(role); - console.log(roleMemberCount); - for (uint256 i = 0; i < roleMemberCount; i++) { - console.log(sigdrop.getRoleMember(role, i)); - } - console.log(""); - - sigdrop.grantRole(role, address(5)); - roleMemberCount = sigdrop.getRoleMemberCount(role); - console.log(roleMemberCount); - for (uint256 i = 0; i < roleMemberCount; i++) { - console.log(sigdrop.getRoleMember(role, i)); - } - console.log(""); - - sigdrop.grantRole(role, address(0)); - roleMemberCount = sigdrop.getRoleMemberCount(role); - console.log(roleMemberCount); - for (uint256 i = 0; i < roleMemberCount; i++) { - console.log(sigdrop.getRoleMember(role, i)); - } - console.log(""); - - sigdrop.grantRole(role, address(6)); - roleMemberCount = sigdrop.getRoleMemberCount(role); - console.log(roleMemberCount); - for (uint256 i = 0; i < roleMemberCount; i++) { - console.log(sigdrop.getRoleMember(role, i)); - } - console.log(""); - } - - /** - * note: Testing transfer of tokens when transfer-role is restricted - */ - function test_claim_transferRole() public { - vm.warp(1); - - address receiver = getActor(0); - bytes32[] memory proofs = new bytes32[](0); - - SignatureDrop.AllowlistProof memory alp; - alp.proof = proofs; - - SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerWallet = 100; - - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions[0], false); - - vm.prank(getActor(5), getActor(5)); - sigdrop.claim(receiver, 1, address(0), 0, alp, ""); - - // revoke transfer role from address(0) - vm.prank(deployerSigner); - sigdrop.revokeRole(keccak256("TRANSFER_ROLE"), address(0)); - vm.startPrank(receiver); - vm.expectRevert("!Transfer-Role"); - sigdrop.transferFrom(receiver, address(123), 0); - } - - /** - * @dev Tests whether role member count is incremented correctly. - */ - function test_member_count_incremented_properly_when_role_granted() public { - bytes32 role = keccak256("ABC_ROLE"); - address receiver = getActor(0); - - vm.startPrank(deployerSigner); - uint256 roleMemberCount = sigdrop.getRoleMemberCount(role); - - assertEq(roleMemberCount, 0); - - sigdrop.grantRole(role, receiver); - - assertEq(sigdrop.getRoleMemberCount(role), 1); - - vm.stopPrank(); - } - - function test_claimCondition_with_startTimestamp() public { - vm.warp(1); - - address receiver = getActor(0); - bytes32[] memory proofs = new bytes32[](0); - - SignatureDrop.AllowlistProof memory alp; - alp.proof = proofs; - - SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - conditions[0].startTimestamp = 100; - conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerWallet = 100; - - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - - vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions[0], false); - - vm.warp(100); - vm.prank(getActor(4), getActor(4)); - sigdrop.claim(receiver, 1, address(0), 0, alp, ""); - - vm.warp(99); - vm.prank(getActor(5), getActor(5)); - vm.expectRevert( - abi.encodeWithSelector( - DropSinglePhase.DropClaimNotStarted.selector, - conditions[0].startTimestamp, - block.timestamp - ) - ); - sigdrop.claim(receiver, 1, address(0), 0, alp, ""); - } - - /*/////////////////////////////////////////////////////////////// - Lazy Mint Tests - //////////////////////////////////////////////////////////////*/ - - /* - * note: Testing state changes; lazy mint a batch of tokens with no encrypted base URI. - */ - function test_state_lazyMint_noEncryptedURI() public { - uint256 amountToLazyMint = 100; - string memory baseURI = "ipfs://"; - - uint256 nextTokenIdToMintBefore = sigdrop.nextTokenIdToMint(); - - vm.startPrank(deployerSigner); - uint256 batchId = sigdrop.lazyMint(amountToLazyMint, baseURI, emptyEncodedBytes); - - assertEq(nextTokenIdToMintBefore + amountToLazyMint, sigdrop.nextTokenIdToMint()); - assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); - - for (uint256 i = 0; i < amountToLazyMint; i += 1) { - string memory uri = sigdrop.tokenURI(i); - console.log(uri); - assertEq(uri, string(abi.encodePacked(baseURI, i.toString()))); - } - - vm.stopPrank(); - } - - /* - * note: Testing state changes; lazy mint a batch of tokens with encrypted base URI. - */ - function test_state_lazyMint_withEncryptedURI() public { - uint256 amountToLazyMint = 100; - string memory baseURI = "ipfs://"; - bytes memory encryptedBaseURI = "encryptedBaseURI://"; - bytes32 provenanceHash = bytes32("whatever"); - - uint256 nextTokenIdToMintBefore = sigdrop.nextTokenIdToMint(); - - vm.startPrank(deployerSigner); - uint256 batchId = sigdrop.lazyMint(amountToLazyMint, baseURI, abi.encode(encryptedBaseURI, provenanceHash)); - - assertEq(nextTokenIdToMintBefore + amountToLazyMint, sigdrop.nextTokenIdToMint()); - assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); - - for (uint256 i = 0; i < amountToLazyMint; i += 1) { - string memory uri = sigdrop.tokenURI(1); - assertEq(uri, string(abi.encodePacked(baseURI, "0"))); - } - - vm.stopPrank(); - } - - /** - * note: Testing revert condition; an address without MINTER_ROLE calls lazyMint function. - */ - function test_revert_lazyMint_MINTER_ROLE() public { - bytes32 _minterRole = keccak256("MINTER_ROLE"); - - vm.prank(deployerSigner); - sigdrop.grantRole(_minterRole, address(0x345)); - - vm.prank(address(0x345)); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - - vm.prank(address(0x567)); - vm.expectRevert(abi.encodeWithSelector(LazyMint.LazyMintUnauthorized.selector)); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - } - - /* - * note: Testing revert condition; calling tokenURI for invalid batch id. - */ - function test_revert_lazyMint_URIForNonLazyMintedToken() public { - vm.startPrank(deployerSigner); - - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - - vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidTokenId.selector, 100)); - sigdrop.tokenURI(100); - - vm.stopPrank(); - } - - /** - * note: Testing event emission; tokens lazy minted. - */ - function test_event_lazyMint_TokensLazyMinted() public { - vm.startPrank(deployerSigner); - - vm.expectEmit(true, false, false, true); - emit TokensLazyMinted(0, 99, "ipfs://", emptyEncodedBytes); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - - vm.stopPrank(); - } - - /* - * note: Fuzz testing state changes; lazy mint a batch of tokens with no encrypted base URI. - */ - function test_fuzz_lazyMint_noEncryptedURI(uint256 x) public { - vm.assume(x > 0); - - uint256 amountToLazyMint = x; - string memory baseURI = "ipfs://"; - - uint256 nextTokenIdToMintBefore = sigdrop.nextTokenIdToMint(); - - vm.startPrank(deployerSigner); - uint256 batchId = sigdrop.lazyMint(amountToLazyMint, baseURI, emptyEncodedBytes); - - assertEq(nextTokenIdToMintBefore + amountToLazyMint, sigdrop.nextTokenIdToMint()); - assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); - - string memory uri = sigdrop.tokenURI(0); - assertEq(uri, string(abi.encodePacked(baseURI, uint256(0).toString()))); - - uri = sigdrop.tokenURI(x - 1); - assertEq(uri, string(abi.encodePacked(baseURI, uint256(x - 1).toString()))); - - /** - * note: this loop takes too long to run with fuzz tests. - */ - // for(uint256 i = 0; i < amountToLazyMint; i += 1) { - // string memory uri = sigdrop.tokenURI(i); - // console.log(uri); - // assertEq(uri, string(abi.encodePacked(baseURI, i.toString()))); - // } - - vm.stopPrank(); - } - - /* - * note: Fuzz testing state changes; lazy mint a batch of tokens with encrypted base URI. - */ - function test_fuzz_lazyMint_withEncryptedURI(uint256 x) public { - vm.assume(x > 0); - - uint256 amountToLazyMint = x; - string memory baseURI = "ipfs://"; - bytes memory encryptedBaseURI = "encryptedBaseURI://"; - bytes32 provenanceHash = bytes32("whatever"); - - uint256 nextTokenIdToMintBefore = sigdrop.nextTokenIdToMint(); - - vm.startPrank(deployerSigner); - uint256 batchId = sigdrop.lazyMint(amountToLazyMint, baseURI, abi.encode(encryptedBaseURI, provenanceHash)); - - assertEq(nextTokenIdToMintBefore + amountToLazyMint, sigdrop.nextTokenIdToMint()); - assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); - - string memory uri = sigdrop.tokenURI(0); - assertEq(uri, string(abi.encodePacked(baseURI, "0"))); - - uri = sigdrop.tokenURI(x - 1); - assertEq(uri, string(abi.encodePacked(baseURI, "0"))); - - /** - * note: this loop takes too long to run with fuzz tests. - */ - // for(uint256 i = 0; i < amountToLazyMint; i += 1) { - // string memory uri = sigdrop.tokenURI(1); - // assertEq(uri, string(abi.encodePacked(baseURI, "0"))); - // } - - vm.stopPrank(); - } - - /* - * note: Fuzz testing; a batch of tokens, and nextTokenIdToMint - */ - function test_fuzz_lazyMint_batchMintAndNextTokenIdToMint(uint256 x) public { - vm.assume(x > 0); - vm.startPrank(deployerSigner); - - if (x == 0) { - vm.expectRevert("Zero amount"); - } - sigdrop.lazyMint(x, "ipfs://", emptyEncodedBytes); - - uint256 slot = stdstore.target(address(sigdrop)).sig("nextTokenIdToMint()").find(); - bytes32 loc = bytes32(slot); - uint256 nextTokenIdToMint = uint256(vm.load(address(sigdrop), loc)); - - assertEq(nextTokenIdToMint, x); - vm.stopPrank(); - } - - /*/////////////////////////////////////////////////////////////// - Delayed Reveal Tests - //////////////////////////////////////////////////////////////*/ - - /* - * note: Testing state changes; URI revealed for a batch of tokens. - */ - function test_state_reveal() public { - vm.startPrank(deployerSigner); - - bytes memory key = "key"; - uint256 amountToLazyMint = 100; - bytes memory secretURI = "ipfs://"; - string memory placeholderURI = "ipfs://"; - bytes memory encryptedURI = sigdrop.encryptDecrypt(secretURI, key); - bytes32 provenanceHash = keccak256(abi.encodePacked(secretURI, key, block.chainid)); - - sigdrop.lazyMint(amountToLazyMint, placeholderURI, abi.encode(encryptedURI, provenanceHash)); - - for (uint256 i = 0; i < amountToLazyMint; i += 1) { - string memory uri = sigdrop.tokenURI(i); - assertEq(uri, string(abi.encodePacked(placeholderURI, "0"))); - } - - string memory revealedURI = sigdrop.reveal(0, key); - assertEq(revealedURI, string(secretURI)); - - for (uint256 i = 0; i < amountToLazyMint; i += 1) { - string memory uri = sigdrop.tokenURI(i); - assertEq(uri, string(abi.encodePacked(secretURI, i.toString()))); - } - - vm.stopPrank(); - } - - /** - * note: Testing revert condition; an address without MINTER_ROLE calls reveal function. - */ - function test_revert_reveal_MINTER_ROLE() public { - bytes memory key = "key"; - bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", key); - bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); - - vm.prank(deployerSigner); - sigdrop.reveal(0, "key"); - - vm.expectRevert( - abi.encodeWithSelector( - Permissions.PermissionsUnauthorizedAccount.selector, - address(this), - keccak256("MINTER_ROLE") - ) - ); - sigdrop.reveal(0, "key"); - } - - /* - * note: Testing revert condition; trying to reveal URI for non-existent batch. - */ - function test_revert_reveal_revealingNonExistentBatch() public { - vm.startPrank(deployerSigner); - - bytes memory key = "key"; - bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", key); - bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); - sigdrop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); - sigdrop.reveal(0, "key"); - - console.log(sigdrop.getBaseURICount()); - - sigdrop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); - vm.expectRevert(abi.encodeWithSelector(BatchMintMetadata.BatchMintInvalidBatchId.selector, 2)); - sigdrop.reveal(2, "key"); - - vm.stopPrank(); - } - - /* - * note: Testing revert condition; already revealed URI. - */ - function test_revert_delayedReveal_alreadyRevealed() public { - vm.startPrank(deployerSigner); - - bytes memory key = "key"; - bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", key); - bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); - sigdrop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); - sigdrop.reveal(0, "key"); - - vm.expectRevert(abi.encodeWithSelector(DelayedReveal.DelayedRevealNothingToReveal.selector)); - sigdrop.reveal(0, "key"); - - vm.stopPrank(); - } - - /* - * note: Testing state changes; revealing URI with an incorrect key. - */ - function test_revert_reveal_incorrectKey() public { - vm.startPrank(deployerSigner); - - bytes memory key = "key"; - bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", key); - bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); - sigdrop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); - - vm.expectRevert(); - string memory revealedURI = sigdrop.reveal(0, "keyy"); - - vm.stopPrank(); - } - - /** - * note: Testing event emission; TokenURIRevealed. - */ - function test_event_reveal_TokenURIRevealed() public { - vm.startPrank(deployerSigner); - - bytes memory key = "key"; - bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", key); - bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); - sigdrop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); - - vm.expectEmit(true, false, false, true); - emit TokenURIRevealed(0, "ipfs://"); - sigdrop.reveal(0, "key"); - - vm.stopPrank(); - } - - /*/////////////////////////////////////////////////////////////// - Signature Mint Tests - //////////////////////////////////////////////////////////////*/ - - function signMintRequest( - SignatureDrop.MintRequest memory mintrequest, - uint256 privateKey - ) internal view returns (bytes memory) { - bytes memory encodedRequest = abi.encode( - typehashMintRequest, - mintrequest.to, - mintrequest.royaltyRecipient, - mintrequest.royaltyBps, - mintrequest.primarySaleRecipient, - keccak256(bytes(mintrequest.uri)), - mintrequest.quantity, - mintrequest.pricePerToken, - mintrequest.currency, - mintrequest.validityStartTimestamp, - mintrequest.validityEndTimestamp, - mintrequest.uid - ); - bytes32 structHash = keccak256(encodedRequest); - bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); - bytes memory signature = abi.encodePacked(r, s, v); - - return signature; - } - - /* - * note: Testing state changes; minting with signature, for a given price and currency. - */ - function test_state_mintWithSignature() public { - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - uint256 id = 0; - SignatureDrop.MintRequest memory mintrequest; - - mintrequest.to = address(0x567); - mintrequest.royaltyRecipient = address(2); - mintrequest.royaltyBps = 0; - mintrequest.primarySaleRecipient = address(deployer); - mintrequest.uri = "ipfs://"; - mintrequest.quantity = 1; - mintrequest.pricePerToken = 1; - mintrequest.currency = address(erc20); - mintrequest.validityStartTimestamp = 1000; - mintrequest.validityEndTimestamp = 2000; - mintrequest.uid = bytes32(id); - - // Test with ERC20 currency - { - uint256 totalSupplyBefore = sigdrop.totalSupply(); - - bytes memory signature = signMintRequest(mintrequest, privateKey); - vm.startPrank(deployerSigner); - vm.warp(1000); - erc20.approve(address(sigdrop), 1); - vm.expectEmit(true, true, true, false); - emit TokensMintedWithSignature(deployerSigner, address(0x567), 0, mintrequest); - sigdrop.mintWithSignature(mintrequest, signature); - vm.stopPrank(); - - assertEq(totalSupplyBefore + mintrequest.quantity, sigdrop.totalSupply()); - } - - // Test with native token currency - { - uint256 totalSupplyBefore = sigdrop.totalSupply(); - - mintrequest.currency = address(NATIVE_TOKEN); - id = 1; - mintrequest.uid = bytes32(id); - - bytes memory signature = signMintRequest(mintrequest, privateKey); - vm.startPrank(address(deployerSigner)); - vm.warp(1000); - sigdrop.mintWithSignature{ value: mintrequest.pricePerToken }(mintrequest, signature); - vm.stopPrank(); - - assertEq(totalSupplyBefore + mintrequest.quantity, sigdrop.totalSupply()); - } - } - - /* - * note: Testing state changes; minting with signature, for a given price and currency. - */ - function test_state_mintWithSignature_UpdateRoyaltyAndSaleInfo() public { - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - uint256 id = 0; - SignatureDrop.MintRequest memory mintrequest; - - mintrequest.to = address(0x567); - mintrequest.royaltyRecipient = address(0x567); - mintrequest.royaltyBps = 100; - mintrequest.primarySaleRecipient = address(0x567); - mintrequest.uri = "ipfs://"; - mintrequest.quantity = 1; - mintrequest.pricePerToken = 1 ether; - mintrequest.currency = address(erc20); - mintrequest.validityStartTimestamp = 1000; - mintrequest.validityEndTimestamp = 2000; - mintrequest.uid = bytes32(id); - - // Test with ERC20 currency - { - erc20.mint(address(0x345), 1 ether); - uint256 totalSupplyBefore = sigdrop.totalSupply(); - - bytes memory signature = signMintRequest(mintrequest, privateKey); - vm.startPrank(address(0x345)); - vm.warp(1000); - erc20.approve(address(sigdrop), 1 ether); - vm.expectEmit(true, true, true, true); - emit TokensMintedWithSignature(deployerSigner, address(0x567), 0, mintrequest); - sigdrop.mintWithSignature(mintrequest, signature); - vm.stopPrank(); - - assertEq(totalSupplyBefore + mintrequest.quantity, sigdrop.totalSupply()); - - (address _royaltyRecipient, uint16 _royaltyBps) = sigdrop.getRoyaltyInfoForToken(0); - assertEq(_royaltyRecipient, address(0x567)); - assertEq(_royaltyBps, 100); - - uint256 totalPrice = 1 * 1 ether; - uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; - assertEq(erc20.balanceOf(address(0x567)), totalPrice - platformFees); - } - - // Test with native token currency - { - vm.deal(address(0x345), 1 ether); - uint256 totalSupplyBefore = sigdrop.totalSupply(); - - mintrequest.currency = address(NATIVE_TOKEN); - id = 1; - mintrequest.uid = bytes32(id); - - bytes memory signature = signMintRequest(mintrequest, privateKey); - vm.startPrank(address(0x345)); - vm.warp(1000); - sigdrop.mintWithSignature{ value: mintrequest.pricePerToken }(mintrequest, signature); - vm.stopPrank(); - - assertEq(totalSupplyBefore + mintrequest.quantity, sigdrop.totalSupply()); - - (address _royaltyRecipient, uint16 _royaltyBps) = sigdrop.getRoyaltyInfoForToken(0); - assertEq(_royaltyRecipient, address(0x567)); - assertEq(_royaltyBps, 100); - - uint256 totalPrice = 1 * 1 ether; - uint256 platformFees = (totalPrice * platformFeeBps) / MAX_BPS; - assertEq(address(0x567).balance, totalPrice - platformFees); - } - } - - /** - * note: Testing revert condition; invalid signature. - */ - function test_revert_mintWithSignature_unapprovedSigner() public { - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - uint256 id = 0; - - SignatureDrop.MintRequest memory mintrequest; - mintrequest.to = address(0x567); - mintrequest.royaltyRecipient = address(2); - mintrequest.royaltyBps = 0; - mintrequest.primarySaleRecipient = address(deployer); - mintrequest.uri = "ipfs://"; - mintrequest.quantity = 1; - mintrequest.pricePerToken = 0; - mintrequest.currency = address(3); - mintrequest.validityStartTimestamp = 1000; - mintrequest.validityEndTimestamp = 2000; - mintrequest.uid = bytes32(id); - - bytes memory signature = signMintRequest(mintrequest, privateKey); - vm.warp(1000); - vm.prank(deployerSigner); - sigdrop.mintWithSignature(mintrequest, signature); - - signature = signMintRequest(mintrequest, 4321); - vm.expectRevert(abi.encodeWithSelector(SignatureMintERC721.SignatureMintInvalidSigner.selector)); - sigdrop.mintWithSignature(mintrequest, signature); - } - - /** - * note: Testing revert condition; minting zero tokens. - */ - function test_revert_mintWithSignature_zeroQuantity() public { - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - uint256 id = 0; - - SignatureDrop.MintRequest memory mintrequest; - mintrequest.to = address(0x567); - mintrequest.royaltyRecipient = address(2); - mintrequest.royaltyBps = 0; - mintrequest.primarySaleRecipient = address(deployer); - mintrequest.uri = "ipfs://"; - mintrequest.quantity = 0; - mintrequest.pricePerToken = 0; - mintrequest.currency = address(3); - mintrequest.validityStartTimestamp = 1000; - mintrequest.validityEndTimestamp = 2000; - mintrequest.uid = bytes32(id); - - bytes memory signature = signMintRequest(mintrequest, privateKey); - vm.warp(1000); - - vm.prank(deployerSigner); - vm.expectRevert(abi.encodeWithSelector(SignatureMintERC721.SignatureMintInvalidQuantity.selector)); - sigdrop.mintWithSignature(mintrequest, signature); - } - - /** - * note: Testing revert condition; not enough minted tokens. - */ - function test_revert_mintWithSignature_notEnoughMintedTokens() public { - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - uint256 id = 0; - - SignatureDrop.MintRequest memory mintrequest; - mintrequest.to = address(0); - mintrequest.royaltyRecipient = address(2); - mintrequest.royaltyBps = 0; - mintrequest.primarySaleRecipient = address(deployer); - mintrequest.uri = "ipfs://"; - mintrequest.quantity = 101; - mintrequest.pricePerToken = 0; - mintrequest.currency = address(3); - mintrequest.validityStartTimestamp = 1000; - mintrequest.validityEndTimestamp = 2000; - mintrequest.uid = bytes32(id); - - bytes memory signature = signMintRequest(mintrequest, privateKey); - vm.warp(1000); - vm.expectRevert("!Tokens"); - sigdrop.mintWithSignature(mintrequest, signature); - } - - /** - * note: Testing revert condition; sent value is not equal to price. - */ - function test_revert_mintWithSignature_notSentAmountRequired() public { - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - uint256 id = 0; - SignatureDrop.MintRequest memory mintrequest; - - mintrequest.to = address(0x567); - mintrequest.royaltyRecipient = address(2); - mintrequest.royaltyBps = 0; - mintrequest.primarySaleRecipient = address(deployer); - mintrequest.uri = "ipfs://"; - mintrequest.quantity = 1; - mintrequest.pricePerToken = 1; - mintrequest.currency = address(3); - mintrequest.validityStartTimestamp = 1000; - mintrequest.validityEndTimestamp = 2000; - mintrequest.uid = bytes32(id); - { - mintrequest.currency = address(NATIVE_TOKEN); - bytes memory signature = signMintRequest(mintrequest, privateKey); - vm.startPrank(address(deployerSigner)); - vm.warp(mintrequest.validityStartTimestamp); - vm.expectRevert("!Price"); - sigdrop.mintWithSignature{ value: 2 }(mintrequest, signature); - vm.stopPrank(); - } - } - - /** - * note: Testing token balances; checking balance and owner of tokens after minting with signature. - */ - function test_balances_mintWithSignature() public { - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - uint256 id = 0; - SignatureDrop.MintRequest memory mintrequest; - - mintrequest.to = address(0x567); - mintrequest.royaltyRecipient = address(2); - mintrequest.royaltyBps = 0; - mintrequest.primarySaleRecipient = address(deployer); - mintrequest.uri = "ipfs://"; - mintrequest.quantity = 1; - mintrequest.pricePerToken = 1; - mintrequest.currency = address(erc20); - mintrequest.validityStartTimestamp = 1000; - mintrequest.validityEndTimestamp = 2000; - mintrequest.uid = bytes32(id); - - { - uint256 currencyBalBefore = erc20.balanceOf(deployerSigner); - - bytes memory signature = signMintRequest(mintrequest, privateKey); - vm.startPrank(deployerSigner); - vm.warp(1000); - erc20.approve(address(sigdrop), 1); - sigdrop.mintWithSignature(mintrequest, signature); - vm.stopPrank(); - - uint256 balance = sigdrop.balanceOf(address(0x567)); - assertEq(balance, 1); - - address owner = sigdrop.ownerOf(0); - assertEq(address(0x567), owner); - - assertEq( - currencyBalBefore - mintrequest.pricePerToken * mintrequest.quantity, - erc20.balanceOf(deployerSigner) - ); - - vm.expectRevert(abi.encodeWithSelector(IERC721AUpgradeable.OwnerQueryForNonexistentToken.selector)); - owner = sigdrop.ownerOf(1); - } - } - - /* - * note: Testing state changes; minting with signature, for a given price and currency. - */ - function mintWithSignature(SignatureDrop.MintRequest memory mintrequest) internal { - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - uint256 id = 0; - - { - bytes memory signature = signMintRequest(mintrequest, privateKey); - vm.startPrank(deployerSigner); - vm.warp(mintrequest.validityStartTimestamp); - erc20.approve(address(sigdrop), 1); - sigdrop.mintWithSignature(mintrequest, signature); - vm.stopPrank(); - } - - { - mintrequest.currency = address(NATIVE_TOKEN); - id = 1; - mintrequest.uid = bytes32(id); - bytes memory signature = signMintRequest(mintrequest, privateKey); - vm.startPrank(address(deployerSigner)); - vm.warp(mintrequest.validityStartTimestamp); - sigdrop.mintWithSignature{ value: mintrequest.pricePerToken }(mintrequest, signature); - vm.stopPrank(); - } - } - - function test_fuzz_mintWithSignature(uint128 x, uint128 y) public { - if (x < y) { - uint256 id = 0; - SignatureDrop.MintRequest memory mintrequest; - - mintrequest.to = address(0x567); - mintrequest.royaltyRecipient = address(2); - mintrequest.royaltyBps = 0; - mintrequest.primarySaleRecipient = address(deployer); - mintrequest.uri = "ipfs://"; - mintrequest.quantity = 1; - mintrequest.pricePerToken = 1; - mintrequest.currency = address(erc20); - mintrequest.validityStartTimestamp = x; - mintrequest.validityEndTimestamp = y; - mintrequest.uid = bytes32(id); - - mintWithSignature(mintrequest); - } - } - - /*/////////////////////////////////////////////////////////////// - Claim Tests - //////////////////////////////////////////////////////////////*/ - - /** - * note: Testing revert condition; not enough minted tokens. - */ - function test_revert_claimCondition_notEnoughMintedTokens() public { - vm.warp(1); - - address receiver = getActor(0); - bytes32[] memory proofs = new bytes32[](0); - - SignatureDrop.AllowlistProof memory alp; - alp.proof = proofs; - - SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerWallet = 100; - - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions[0], false); - - vm.expectRevert("!Tokens"); - vm.prank(getActor(6), getActor(6)); - sigdrop.claim(receiver, 101, address(0), 0, alp, ""); - } - - /** - * note: Testing revert condition; exceed max claimable supply. - */ - function test_revert_claimCondition_exceedMaxClaimableSupply() public { - vm.warp(1); - - address receiver = getActor(0); - bytes32[] memory proofs = new bytes32[](0); - - SignatureDrop.AllowlistProof memory alp; - alp.proof = proofs; - - SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerWallet = 100; - - vm.prank(deployerSigner); - sigdrop.lazyMint(200, "ipfs://", emptyEncodedBytes); - vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions[0], false); - - vm.prank(getActor(5), getActor(5)); - sigdrop.claim(receiver, 100, address(0), 0, alp, ""); - - vm.expectRevert( - abi.encodeWithSelector( - DropSinglePhase.DropClaimExceedMaxSupply.selector, - conditions[0].maxClaimableSupply, - 101 - ) - ); - vm.prank(getActor(6), getActor(6)); - sigdrop.claim(receiver, 1, address(0), 0, alp, ""); - } - - /** - * note: Testing quantity limit restriction when no allowlist present. - */ - function test_fuzz_claim_noAllowlist(uint256 x) public { - vm.assume(x != 0); - vm.warp(1); - - address receiver = getActor(0); - bytes32[] memory proofs = new bytes32[](0); - - SignatureDrop.AllowlistProof memory alp; - alp.proof = proofs; - alp.quantityLimitPerWallet = x; - - SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - conditions[0].maxClaimableSupply = 500; - conditions[0].quantityLimitPerWallet = 100; - - vm.prank(deployerSigner); - sigdrop.lazyMint(500, "ipfs://", emptyEncodedBytes); - - vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions[0], false); - - vm.prank(getActor(5), getActor(5)); - vm.expectRevert( - abi.encodeWithSelector( - DropSinglePhase.DropClaimExceedLimit.selector, - conditions[0].quantityLimitPerWallet, - 101 - ) - ); - sigdrop.claim(receiver, 101, address(0), 0, alp, ""); - - vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions[0], true); - - vm.prank(getActor(5), getActor(5)); - vm.expectRevert( - abi.encodeWithSelector( - DropSinglePhase.DropClaimExceedLimit.selector, - conditions[0].quantityLimitPerWallet, - 101 - ) - ); - sigdrop.claim(receiver, 101, address(0), 0, alp, ""); - } - - function test_fuzz_claim_merkleProof(uint256 x) public { - vm.assume(x > 10 && x < 500); - string[] memory inputs = new string[](5); - - inputs[0] = "node"; - inputs[1] = "src/test/scripts/generateRoot.ts"; - inputs[2] = Strings.toString(x); - inputs[3] = "0"; - inputs[4] = "0x0000000000000000000000000000000000000000"; - - bytes memory result = vm.ffi(inputs); - // revert(); - bytes32 root = abi.decode(result, (bytes32)); - - inputs[1] = "src/test/scripts/getProof.ts"; - result = vm.ffi(inputs); - bytes32[] memory proofs = abi.decode(result, (bytes32[])); - - SignatureDrop.AllowlistProof memory alp; - alp.proof = proofs; - alp.quantityLimitPerWallet = x; - alp.pricePerToken = 0; - alp.currency = address(0); - - vm.warp(1); - - address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); - - // bytes32[] memory proofs = new bytes32[](0); - - SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - conditions[0].maxClaimableSupply = x; - conditions[0].quantityLimitPerWallet = 1; - conditions[0].merkleRoot = root; - - vm.prank(deployerSigner); - sigdrop.lazyMint(2 * x, "ipfs://", emptyEncodedBytes); - vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions[0], false); - - // vm.prank(getActor(5), getActor(5)); - vm.prank(receiver, receiver); - sigdrop.claim(receiver, x - 5, address(0), 0, alp, ""); - assertEq(sigdrop.getSupplyClaimedByWallet(receiver), x - 5); - - vm.prank(receiver, receiver); - vm.expectRevert(abi.encodeWithSelector(DropSinglePhase.DropClaimExceedLimit.selector, x, x + 1)); - sigdrop.claim(receiver, 6, address(0), 0, alp, ""); - - vm.prank(receiver, receiver); - sigdrop.claim(receiver, 5, address(0), 0, alp, ""); - assertEq(sigdrop.getSupplyClaimedByWallet(receiver), x); - - vm.prank(receiver, receiver); - vm.expectRevert(abi.encodeWithSelector(DropSinglePhase.DropClaimExceedLimit.selector, x, x + 5)); - sigdrop.claim(receiver, 5, address(0), 0, alp, ""); - } - - /** - * note: Testing state changes; reset eligibility of claim conditions and claiming again for same condition id. - */ - function test_state_claimCondition_resetEligibility() public { - vm.warp(1); - - address receiver = getActor(0); - bytes32[] memory proofs = new bytes32[](0); - - SignatureDrop.AllowlistProof memory alp; - alp.proof = proofs; - - SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerWallet = 100; - - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - - vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions[0], false); - - vm.prank(getActor(5), getActor(5)); - sigdrop.claim(receiver, 1, address(0), 0, alp, ""); - - vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions[0], true); - - vm.prank(getActor(5), getActor(5)); - sigdrop.claim(receiver, 1, address(0), 0, alp, ""); - } - - /*/////////////////////////////////////////////////////////////// - Miscellaneous - //////////////////////////////////////////////////////////////*/ - - function test_delayedReveal_withNewLazyMintedEmptyBatch() public { - vm.startPrank(deployerSigner); - - bytes memory encryptedURI = sigdrop.encryptDecrypt("ipfs://", "key"); - bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", "key", block.chainid)); - sigdrop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); - sigdrop.reveal(0, "key"); - - string memory uri = sigdrop.tokenURI(1); - assertEq(uri, string(abi.encodePacked("ipfs://", "1"))); - - bytes memory newEncryptedURI = sigdrop.encryptDecrypt("ipfs://secret", "key"); - vm.expectRevert(abi.encodeWithSelector(LazyMint.LazyMintInvalidAmount.selector)); - sigdrop.lazyMint(0, "", abi.encode(newEncryptedURI, provenanceHash)); - - vm.stopPrank(); - } - - /*/////////////////////////////////////////////////////////////// - Reentrancy related Tests - //////////////////////////////////////////////////////////////*/ - - function test_revert_reentrancy_mintWithSignature() public { - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - uint256 id = 0; - SignatureDrop.MintRequest memory mintrequest; - - mintrequest.to = address(0); - mintrequest.royaltyRecipient = address(2); - mintrequest.royaltyBps = 0; - mintrequest.primarySaleRecipient = address(deployer); - mintrequest.uri = "ipfs://"; - mintrequest.quantity = 1; - mintrequest.pricePerToken = 1; - mintrequest.currency = address(NATIVE_TOKEN); - mintrequest.validityStartTimestamp = 1000; - mintrequest.validityEndTimestamp = 2000; - mintrequest.uid = bytes32(id); - - // Test with native token currency - { - uint256 totalSupplyBefore = sigdrop.totalSupply(); - - mintrequest.uid = bytes32(id); - bytes memory signature = signMintRequest(mintrequest, privateKey); - - MaliciousReceiver mal = new MaliciousReceiver(address(sigdrop)); - vm.deal(address(mal), 100 ether); - vm.warp(1000); - vm.expectRevert(); - mal.attackMintWithSignature(mintrequest, signature, false); - } - } - - function test_revert_reentrancy_claim() public { - vm.warp(1); - bytes32[] memory proofs = new bytes32[](0); - - SignatureDrop.AllowlistProof memory alp; - alp.proof = proofs; - - SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerWallet = 100; - - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - - vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions[0], false); - - MaliciousReceiver mal = new MaliciousReceiver(address(sigdrop)); - vm.deal(address(mal), 100 ether); - vm.expectRevert(); - mal.attackClaim(alp, false); - } - - function test_revert_combination_signatureAndClaim() public { - vm.warp(1); - bytes32[] memory proofs = new bytes32[](0); - - SignatureDrop.AllowlistProof memory alp; - alp.proof = proofs; - - SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerWallet = 100; - - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions[0], false); - - uint256 id = 0; - SignatureDrop.MintRequest memory mintrequest; - - mintrequest.to = address(0); - mintrequest.royaltyRecipient = address(2); - mintrequest.royaltyBps = 0; - mintrequest.primarySaleRecipient = address(deployer); - mintrequest.uri = "ipfs://"; - mintrequest.quantity = 1; - mintrequest.pricePerToken = 1; - mintrequest.currency = address(NATIVE_TOKEN); - mintrequest.validityStartTimestamp = 1000; - mintrequest.validityEndTimestamp = 2000; - mintrequest.uid = bytes32(id); - - // Test with native token currency - { - uint256 totalSupplyBefore = sigdrop.totalSupply(); - - mintrequest.uid = bytes32(id); - bytes memory signature = signMintRequest(mintrequest, privateKey); - - MaliciousReceiver mal = new MaliciousReceiver(address(sigdrop)); - vm.deal(address(mal), 100 ether); - vm.warp(1000); - mal.saveCombination(mintrequest, signature, alp); - vm.expectRevert(); - mal.attackMintWithSignature(mintrequest, signature, true); - // mal.attackClaim(alp, true); - } - } -} - -contract MaliciousReceiver { - SignatureDrop public sigdrop; - - SignatureDrop.MintRequest public mintrequest; - SignatureDrop.AllowlistProof public alp; - bytes public signature; - bool public claim; - bool public loop = true; - - constructor(address _sigdrop) { - sigdrop = SignatureDrop(_sigdrop); - } - - function attackMintWithSignature( - SignatureDrop.MintRequest calldata _mintrequest, - bytes calldata _signature, - bool swap - ) external { - claim = swap; - mintrequest = _mintrequest; - signature = _signature; - sigdrop.mintWithSignature{ value: _mintrequest.pricePerToken }(_mintrequest, _signature); - } - - function attackClaim(SignatureDrop.AllowlistProof calldata _alp, bool swap) external { - claim = !swap; - alp = _alp; - sigdrop.claim(address(this), 1, address(0), 0, _alp, ""); - } - - function saveCombination( - SignatureDrop.MintRequest calldata _mintrequest, - bytes calldata _signature, - SignatureDrop.AllowlistProof calldata _alp - ) external { - mintrequest = _mintrequest; - signature = _signature; - alp = _alp; - } - - function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4) { - if (claim && loop) { - loop = false; - claim = false; - sigdrop.claim(address(this), 1, address(0), 0, alp, ""); - } else if (!claim && loop) { - loop = false; - sigdrop.mintWithSignature{ value: mintrequest.pricePerToken }(mintrequest, signature); - } - return this.onERC721Received.selector; - } -} diff --git a/src/test/TieredDrop.t.sol b/src/test/TieredDrop.t.sol deleted file mode 100644 index 151d12ecb..000000000 --- a/src/test/TieredDrop.t.sol +++ /dev/null @@ -1,1103 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -import "./utils/BaseTest.sol"; - -import { TieredDrop } from "contracts/prebuilts/tiered-drop/TieredDrop.sol"; -import { TWProxy } from "contracts/infra/TWProxy.sol"; - -contract TieredDropTest is BaseTest { - using Strings for uint256; - - TieredDrop public tieredDrop; - - address internal dropAdmin; - address internal claimer; - - // Signature params - address internal deployerSigner; - bytes32 internal typehashGenericRequest; - bytes32 internal nameHash; - bytes32 internal versionHash; - bytes32 internal typehashEip712; - bytes32 internal domainSeparator; - - // Lazy mint variables - uint256 internal quantityTier1 = 10; - string internal tier1 = "tier1"; - string internal baseURITier1 = "baseURI1/"; - string internal placeholderURITier1 = "placeholderURI1/"; - bytes internal keyTier1 = "tier1_key"; - - uint256 internal quantityTier2 = 20; - string internal tier2 = "tier2"; - string internal baseURITier2 = "baseURI2/"; - string internal placeholderURITier2 = "placeholderURI2/"; - bytes internal keyTier2 = "tier2_key"; - - uint256 internal quantityTier3 = 30; - string internal tier3 = "tier3"; - string internal baseURITier3 = "baseURI3/"; - string internal placeholderURITier3 = "placeholderURI3/"; - bytes internal keyTier3 = "tier3_key"; - - function setUp() public virtual override { - super.setUp(); - - dropAdmin = getActor(1); - claimer = getActor(2); - - // Deploy implementation. - address tieredDropImpl = address(new TieredDrop()); - - // Deploy proxy pointing to implementaion. - vm.prank(dropAdmin); - tieredDrop = TieredDrop( - address( - new TWProxy( - tieredDropImpl, - abi.encodeCall( - TieredDrop.initialize, - (dropAdmin, "Tiered Drop", "TD", "ipfs://", new address[](0), dropAdmin, dropAdmin, 0) - ) - ) - ) - ); - - // ====== signature params - - deployerSigner = signer; - vm.prank(dropAdmin); - tieredDrop.grantRole(keccak256("MINTER_ROLE"), deployerSigner); - - typehashGenericRequest = keccak256( - "GenericRequest(uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid,bytes data)" - ); - nameHash = keccak256(bytes("SignatureAction")); - versionHash = keccak256(bytes("1")); - typehashEip712 = keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ); - domainSeparator = keccak256( - abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tieredDrop)) - ); - - // ====== - } - - TieredDrop.GenericRequest internal claimRequest; - bytes internal claimSignature; - - uint256 internal nonce; - - function _setupClaimSignature(string[] memory _orderedTiers, uint256 _totalQuantity) internal { - claimRequest.validityStartTimestamp = 1000; - claimRequest.validityEndTimestamp = 2000; - claimRequest.uid = keccak256(abi.encodePacked(nonce)); - nonce += 1; - claimRequest.data = abi.encode( - _orderedTiers, - claimer, - address(0), - 0, - dropAdmin, - _totalQuantity, - 0, - NATIVE_TOKEN - ); - - bytes memory encodedRequest = abi.encode( - typehashGenericRequest, - claimRequest.validityStartTimestamp, - claimRequest.validityEndTimestamp, - claimRequest.uid, - keccak256(bytes(claimRequest.data)) - ); - - bytes32 structHash = keccak256(encodedRequest); - bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); - claimSignature = abi.encodePacked(r, s, v); - } - - //////////////////////////////////////////////// - // // - // lazyMintWithTier tests // - // // - //////////////////////////////////////////////// - - // function test_state_lazyMintWithTier() public { - // // Lazy mint tokens: 3 different tiers - // vm.startPrank(dropAdmin); - - // // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. - // tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); - // // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. - // tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); - // // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. - // tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); - - // vm.stopPrank(); - - // TieredDrop.TierMetadata[] memory metadataForAllTiers = tieredDrop.getMetadataForAllTiers(); - // (TieredDrop.TokenRange[] memory tokens_1, string[] memory baseURIs_1) = ( - // metadataForAllTiers[0].ranges, - // metadataForAllTiers[0].baseURIs - // ); - // (TieredDrop.TokenRange[] memory tokens_2, string[] memory baseURIs_2) = ( - // metadataForAllTiers[1].ranges, - // metadataForAllTiers[1].baseURIs - // ); - // (TieredDrop.TokenRange[] memory tokens_3, string[] memory baseURIs_3) = ( - // metadataForAllTiers[2].ranges, - // metadataForAllTiers[2].baseURIs - // ); - - // uint256 cumulativeStart = 0; - - // TieredDrop.TokenRange memory range = tokens_1[0]; - // string memory baseURI = baseURIs_1[0]; - - // assertEq(range.startIdInclusive, cumulativeStart); - // assertEq(range.endIdNonInclusive, cumulativeStart + quantityTier1); - // assertEq(baseURI, baseURITier1); - - // cumulativeStart += quantityTier1; - - // range = tokens_2[0]; - // baseURI = baseURIs_2[0]; - - // assertEq(range.startIdInclusive, cumulativeStart); - // assertEq(range.endIdNonInclusive, cumulativeStart + quantityTier2); - // assertEq(baseURI, baseURITier2); - - // cumulativeStart += quantityTier2; - - // range = tokens_3[0]; - // baseURI = baseURIs_3[0]; - - // assertEq(range.startIdInclusive, cumulativeStart); - // assertEq(range.endIdNonInclusive, cumulativeStart + quantityTier3); - // assertEq(baseURI, baseURITier3); - // } - - // function test_state_lazyMintWithTier_sameTier() public { - // // Lazy mint tokens: 3 different tiers - // vm.startPrank(dropAdmin); - - // // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. - // tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); - // // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. - // tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); - // // Tier 1 Again: tokenIds assigned 30 -> 60 non-inclusive. - // tieredDrop.lazyMint(quantityTier3, baseURITier3, tier1, ""); - - // TieredDrop.TierMetadata[] memory metadataForAllTiers = tieredDrop.getMetadataForAllTiers(); - // (TieredDrop.TokenRange[] memory tokens_1, string[] memory baseURIs_1) = ( - // metadataForAllTiers[0].ranges, - // metadataForAllTiers[0].baseURIs - // ); - // (TieredDrop.TokenRange[] memory tokens_2, string[] memory baseURIs_2) = ( - // metadataForAllTiers[1].ranges, - // metadataForAllTiers[1].baseURIs - // ); - - // vm.stopPrank(); - - // uint256 cumulativeStart = 0; - - // TieredDrop.TokenRange memory range = tokens_1[0]; - // string memory baseURI = baseURIs_1[0]; - - // assertEq(range.startIdInclusive, cumulativeStart); - // assertEq(range.endIdNonInclusive, cumulativeStart + quantityTier1); - // assertEq(baseURI, baseURITier1); - - // cumulativeStart += quantityTier1; - - // range = tokens_2[0]; - // baseURI = baseURIs_2[0]; - - // assertEq(range.startIdInclusive, cumulativeStart); - // assertEq(range.endIdNonInclusive, cumulativeStart + quantityTier2); - // assertEq(baseURI, baseURITier2); - - // cumulativeStart += quantityTier2; - - // range = tokens_1[1]; - // baseURI = baseURIs_1[1]; - - // assertEq(range.startIdInclusive, cumulativeStart); - // assertEq(range.endIdNonInclusive, cumulativeStart + quantityTier3); - // assertEq(baseURI, baseURITier3); - // } - - function test_revert_lazyMintWithTier_notMinterRole() public { - vm.expectRevert("Not authorized"); - tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); - } - - function test_revert_lazyMintWithTier_mintingZeroAmount() public { - vm.prank(dropAdmin); - vm.expectRevert("0 amt"); - tieredDrop.lazyMint(0, baseURITier1, tier1, ""); - } - - //////////////////////////////////////////////// - // // - // claimWithSignature tests // - // // - //////////////////////////////////////////////// - - function test_state_claimWithSignature() public { - // Lazy mint tokens: 3 different tiers - vm.startPrank(dropAdmin); - - // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. - tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); - // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. - tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); - // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. - tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); - - vm.stopPrank(); - - /** - * Claim tokens. - * - Order of priority: [tier2, tier1] - * - Total quantity: 25. [20 from tier2, 5 from tier1] - */ - - string[] memory tiers = new string[](2); - tiers[0] = tier2; - tiers[1] = tier1; - - uint256 claimQuantity = 25; - - _setupClaimSignature(tiers, claimQuantity); - - assertEq(tieredDrop.hasRole(keccak256("MINTER_ROLE"), deployerSigner), true); - - vm.warp(claimRequest.validityStartTimestamp); - vm.prank(claimer); - tieredDrop.claimWithSignature(claimRequest, claimSignature); - - /** - * Check token URIs for tokens of tiers: - * - Tier 2: token IDs 0 -> 19 mapped one-to-one to metadata IDs 10 -> 29 - * - Tier 1: token IDs 20 -> 24 mapped one-to-one to metadata IDs 0 -> 4 - */ - - uint256 tier2Id = 10; - uint256 tier1Id = 0; - - for (uint256 i = 0; i < claimQuantity; i += 1) { - if (i < 20) { - assertEq(tieredDrop.tokenURI(i), string(abi.encodePacked(baseURITier2, tier2Id.toString()))); - tier2Id += 1; - } else { - assertEq(tieredDrop.tokenURI(i), string(abi.encodePacked(baseURITier1, tier1Id.toString()))); - tier1Id += 1; - } - } - } - - function test_revert_claimWithSignature_invalidEncoding() public { - // Lazy mint tokens: 3 different tiers - vm.startPrank(dropAdmin); - - // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. - tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); - // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. - tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); - // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. - tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); - - vm.stopPrank(); - - /** - * Claim tokens. - * - Order of priority: [tier2, tier1] - * - Total quantity: 25. [20 from tier2, 5 from tier1] - */ - - string[] memory tiers = new string[](2); - tiers[0] = tier2; - tiers[1] = tier1; - - uint256 claimQuantity = 25; - - // Create data with invalid encoding. - claimRequest.data = abi.encode(1, ""); - _setupClaimSignature(tiers, claimQuantity); - - claimRequest.data = abi.encode(1, ""); - - assertEq(tieredDrop.hasRole(keccak256("MINTER_ROLE"), deployerSigner), true); - - vm.warp(claimRequest.validityStartTimestamp); - vm.prank(claimer); - vm.expectRevert(); - tieredDrop.claimWithSignature(claimRequest, claimSignature); - } - - function test_revert_claimWithSignature_mintingZeroQuantity() public { - // Lazy mint tokens: 3 different tiers - vm.startPrank(dropAdmin); - - // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. - tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); - // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. - tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); - // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. - tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); - - vm.stopPrank(); - - /** - * Claim tokens. - * - Order of priority: [tier2, tier1] - * - Total quantity: 25. [20 from tier2, 5 from tier1] - */ - - string[] memory tiers = new string[](2); - tiers[0] = tier2; - tiers[1] = tier1; - - uint256 claimQuantity = 0; - - _setupClaimSignature(tiers, claimQuantity); - - assertEq(tieredDrop.hasRole(keccak256("MINTER_ROLE"), deployerSigner), true); - - vm.warp(claimRequest.validityStartTimestamp); - vm.prank(claimer); - vm.expectRevert("0 qty"); - tieredDrop.claimWithSignature(claimRequest, claimSignature); - } - - function test_revert_claimWithSignature_notEnoughLazyMintedTokens() public { - // Lazy mint tokens: 3 different tiers - vm.startPrank(dropAdmin); - - // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. - tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); - // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. - tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); - // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. - tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); - - vm.stopPrank(); - - /** - * Claim tokens. - * - Order of priority: [tier2, tier1] - * - Total quantity: 25. [20 from tier2, 5 from tier1] - */ - - string[] memory tiers = new string[](2); - tiers[0] = tier2; - tiers[1] = tier1; - - uint256 claimQuantity = quantityTier1 + quantityTier2 + quantityTier3 + 1; - - _setupClaimSignature(tiers, claimQuantity); - - assertEq(tieredDrop.hasRole(keccak256("MINTER_ROLE"), deployerSigner), true); - - vm.warp(claimRequest.validityStartTimestamp); - vm.prank(claimer); - vm.expectRevert("!Tokens"); - tieredDrop.claimWithSignature(claimRequest, claimSignature); - } - - function test_revert_claimWithSignature_insufficientTokensInTiers() public { - // Lazy mint tokens: 3 different tiers - vm.startPrank(dropAdmin); - - // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. - tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); - // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. - tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); - // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. - tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); - - vm.stopPrank(); - - /** - * Claim tokens. - * - Order of priority: [tier2, tier1] - * - Total quantity: 25. [20 from tier2, 5 from tier1] - */ - - string[] memory tiers = new string[](2); - tiers[0] = "non-exsitent tier 1"; - tiers[1] = "non-exsitent tier 2"; - - uint256 claimQuantity = 25; - - _setupClaimSignature(tiers, claimQuantity); - - assertEq(tieredDrop.hasRole(keccak256("MINTER_ROLE"), deployerSigner), true); - - vm.warp(claimRequest.validityStartTimestamp); - vm.prank(claimer); - vm.expectRevert("Insufficient tokens in tiers."); - tieredDrop.claimWithSignature(claimRequest, claimSignature); - } - - //////////////////////////////////////////////// - // // - // reveal tests // - // // - //////////////////////////////////////////////// - - function _getProvenanceHash(string memory _revealURI, bytes memory _key) private view returns (bytes32) { - return keccak256(abi.encodePacked(_revealURI, _key, block.chainid)); - } - - function test_state_revealWithScrambleOffset() public { - // Lazy mint tokens: 3 different tiers: with delayed reveal - bytes memory encryptedURITier1 = tieredDrop.encryptDecrypt(bytes(baseURITier1), keyTier1); - bytes memory encryptedURITier2 = tieredDrop.encryptDecrypt(bytes(baseURITier2), keyTier2); - bytes memory encryptedURITier3 = tieredDrop.encryptDecrypt(bytes(baseURITier3), keyTier3); - - vm.startPrank(dropAdmin); - - // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. - tieredDrop.lazyMint( - quantityTier1, - placeholderURITier1, - tier1, - abi.encode(encryptedURITier1, _getProvenanceHash(baseURITier1, keyTier1)) - ); - // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. - tieredDrop.lazyMint( - quantityTier2, - placeholderURITier2, - tier2, - abi.encode(encryptedURITier2, _getProvenanceHash(baseURITier2, keyTier2)) - ); - // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. - tieredDrop.lazyMint( - quantityTier3, - placeholderURITier3, - tier3, - abi.encode(encryptedURITier3, _getProvenanceHash(baseURITier3, keyTier3)) - ); - - vm.stopPrank(); - - /** - * Claim tokens. - * - Order of priority: [tier2, tier1] - * - Total quantity: 25. [20 from tier2, 5 from tier1] - */ - - string[] memory tiers = new string[](2); - tiers[0] = tier2; - tiers[1] = tier1; - - uint256 claimQuantity = 25; - - _setupClaimSignature(tiers, claimQuantity); - - assertEq(tieredDrop.hasRole(keccak256("MINTER_ROLE"), deployerSigner), true); - - vm.warp(claimRequest.validityStartTimestamp); - vm.prank(claimer); - tieredDrop.claimWithSignature(claimRequest, claimSignature); - - /** - * Check token URIs for tokens of tiers: - * - Tier 2: token IDs 0 -> 19 mapped one-to-one to metadata IDs 10 -> 29 - * - Tier 1: token IDs 20 -> 24 mapped one-to-one to metadata IDs 0 -> 4 - */ - - uint256 tier2Id = 10; - uint256 tier1Id = 0; - - for (uint256 i = 0; i < claimQuantity; i += 1) { - // console.log(i); - if (i < 20) { - assertEq(tieredDrop.tokenURI(i), string(abi.encodePacked(placeholderURITier2, uint256(0).toString()))); - tier2Id += 1; - } else { - assertEq(tieredDrop.tokenURI(i), string(abi.encodePacked(placeholderURITier1, uint256(0).toString()))); - tier1Id += 1; - } - } - - // Reveal tokens. - vm.startPrank(dropAdmin); - tieredDrop.reveal(0, keyTier1); - tieredDrop.reveal(1, keyTier2); - tieredDrop.reveal(2, keyTier3); - - uint256 tier2IdStart = 10; - uint256 tier2IdEnd = 30; - - uint256 tier1IdStart = 0; - uint256 tier1IdEnd = 10; - - for (uint256 i = 0; i < claimQuantity; i += 1) { - bytes32 tokenURIHash = keccak256(abi.encodePacked(tieredDrop.tokenURI(i))); - bool detected = false; - - if (i < 20) { - for (uint256 j = tier2IdStart; j < tier2IdEnd; j += 1) { - bytes32 expectedURIHash = keccak256(abi.encodePacked(baseURITier2, j.toString())); - - if (tokenURIHash == expectedURIHash) { - detected = true; - } - - if (detected) { - break; - } - } - } else { - for (uint256 k = tier1IdStart; k < tier1IdEnd; k += 1) { - bytes32 expectedURIHash = keccak256(abi.encodePacked(baseURITier1, k.toString())); - - if (tokenURIHash == expectedURIHash) { - detected = true; - } - - if (detected) { - break; - } - } - } - - assertEq(detected, true); - } - } - - event URIReveal(uint256 tokenId, string uri); - - //////////////////////////////////////////////// - // // - // getTokensInTierLen tests // - // // - //////////////////////////////////////////////// - - function test_state_getTokensInTierLen() public { - // Lazy mint tokens: 3 different tiers - vm.startPrank(dropAdmin); - - // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. - tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); - // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. - tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); - // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. - tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); - - vm.stopPrank(); - - /** - * Claim tokens. - * - Order of priority: [tier2, tier1] - * - Total quantity: 25. [20 from tier2, 5 from tier1] - */ - - string[] memory tiers = new string[](2); - tiers[0] = tier2; - tiers[1] = tier1; - - uint256 claimQuantity = 25; - - _setupClaimSignature(tiers, claimQuantity); - - vm.warp(claimRequest.validityStartTimestamp); - - vm.prank(claimer); - tieredDrop.claimWithSignature(claimRequest, claimSignature); - - assertEq(tieredDrop.getTokensInTierLen(), 2); - - for (uint256 i = 0; i < 5; i += 1) { - _setupClaimSignature(tiers, 1); - - vm.warp(claimRequest.validityStartTimestamp); - - vm.prank(claimer); - tieredDrop.claimWithSignature(claimRequest, claimSignature); - } - - assertEq(tieredDrop.getTokensInTierLen(), 7); - } - - //////////////////////////////////////////////// - // // - // getTokensInTier tests // - // // - //////////////////////////////////////////////// - - function test_state_getTokensInTier() public { - // Lazy mint tokens: 3 different tiers - vm.startPrank(dropAdmin); - - // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. - tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); - // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. - tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); - // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. - tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); - - vm.stopPrank(); - - /** - * Claim tokens. - * - Order of priority: [tier2, tier1] - * - Total quantity: 25. [20 from tier2, 5 from tier1] - */ - - string[] memory tiers = new string[](2); - tiers[0] = tier2; - tiers[1] = tier1; - - uint256 claimQuantity = 25; - - _setupClaimSignature(tiers, claimQuantity); - - vm.warp(claimRequest.validityStartTimestamp); - - vm.prank(claimer); - tieredDrop.claimWithSignature(claimRequest, claimSignature); - - TieredDrop.TokenRange[] memory rangesTier1 = tieredDrop.getTokensInTier(tier1, 0, 2); - assertEq(rangesTier1.length, 1); - - TieredDrop.TokenRange[] memory rangesTier2 = tieredDrop.getTokensInTier(tier2, 0, 2); - assertEq(rangesTier2.length, 1); - - assertEq(rangesTier1[0].startIdInclusive, 20); - assertEq(rangesTier1[0].endIdNonInclusive, 25); - assertEq(rangesTier2[0].startIdInclusive, 0); - assertEq(rangesTier2[0].endIdNonInclusive, 20); - } - - //////////////////////////////////////////////// - // // - // getTierForToken tests // - // // - //////////////////////////////////////////////// - - function test_state_getTierForToken() public { - // Lazy mint tokens: 3 different tiers - vm.startPrank(dropAdmin); - - // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. - tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); - // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. - tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); - // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. - tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); - - vm.stopPrank(); - - /** - * Claim tokens. - * - Order of priority: [tier2, tier1] - * - Total quantity: 25. [20 from tier2, 5 from tier1] - */ - - string[] memory tiers = new string[](2); - tiers[0] = tier2; - tiers[1] = tier1; - - uint256 claimQuantity = 25; - - _setupClaimSignature(tiers, claimQuantity); - - vm.warp(claimRequest.validityStartTimestamp); - - vm.prank(claimer); - tieredDrop.claimWithSignature(claimRequest, claimSignature); - - /** - * Check token URIs for tokens of tiers: - * - Tier 2: token IDs 0 -> 19 mapped one-to-one to metadata IDs 10 -> 29 - * - Tier 1: token IDs 20 -> 24 mapped one-to-one to metadata IDs 0 -> 4 - */ - - uint256 tier2Id = 10; - uint256 tier1Id = 0; - - for (uint256 i = 0; i < claimQuantity; i += 1) { - if (i < 20) { - string memory tierForToken = tieredDrop.getTierForToken(i); - assertEq(tierForToken, tier2); - - tier2Id += 1; - } else { - string memory tierForToken = tieredDrop.getTierForToken(i); - assertEq(tierForToken, tier1); - - tier1Id += 1; - } - } - } - - //////////////////////////////////////////////// - // // - // getMetadataForAllTiers tests // - // // - //////////////////////////////////////////////// - - // function test_state_getMetadataForAllTiers() public { - // // Lazy mint tokens: 3 different tiers - // vm.startPrank(dropAdmin); - - // // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. - // tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); - // // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. - // tieredDrop.lazyMint(quantityTier2, baseURITier2, tier2, ""); - // // Tier 3: tokenIds assigned 30 -> 60 non-inclusive. - // tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); - - // vm.stopPrank(); - - // TieredDrop.TierMetadata[] memory metadataForAllTiers = tieredDrop.getMetadataForAllTiers(); - - // // Tier 1 - // assertEq(metadataForAllTiers[0].tier, tier1); - - // TieredDrop.TokenRange[] memory ranges1 = metadataForAllTiers[0].ranges; - // assertEq(ranges1.length, 1); - // assertEq(ranges1[0].startIdInclusive, 0); - // assertEq(ranges1[0].endIdNonInclusive, 10); - - // string[] memory baseURIs1 = metadataForAllTiers[0].baseURIs; - // assertEq(baseURIs1.length, 1); - // assertEq(baseURIs1[0], baseURITier1); - - // // Tier 2 - // assertEq(metadataForAllTiers[1].tier, tier2); - - // TieredDrop.TokenRange[] memory ranges2 = metadataForAllTiers[1].ranges; - // assertEq(ranges2.length, 1); - // assertEq(ranges2[0].startIdInclusive, 10); - // assertEq(ranges2[0].endIdNonInclusive, 30); - - // string[] memory baseURIs2 = metadataForAllTiers[1].baseURIs; - // assertEq(baseURIs2.length, 1); - // assertEq(baseURIs2[0], baseURITier2); - - // // Tier 3 - // assertEq(metadataForAllTiers[2].tier, tier3); - - // TieredDrop.TokenRange[] memory ranges3 = metadataForAllTiers[2].ranges; - // assertEq(ranges3.length, 1); - // assertEq(ranges3[0].startIdInclusive, 30); - // assertEq(ranges3[0].endIdNonInclusive, 60); - - // string[] memory baseURIs3 = metadataForAllTiers[2].baseURIs; - // assertEq(baseURIs3.length, 1); - // assertEq(baseURIs3[0], baseURITier3); - // } - - //////////////////////////////////////////////// - // // - // audit tests // - // // - //////////////////////////////////////////////// - - function test_state_claimWithSignature_IssueH1() public { - // Lazy mint tokens: 3 different tiers - vm.startPrank(dropAdmin); - - // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. - tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); - // Tier 2: tokenIds assigned 10 -> 20 non-inclusive. - tieredDrop.lazyMint(10, baseURITier2, tier2, ""); - // Tier 3: tokenIds assigned 20 -> 50 non-inclusive. - tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); - - // Tier 2: tokenIds assigned 50 -> 60 non-inclusive. - tieredDrop.lazyMint(quantityTier2 - 10, baseURITier2, tier2, ""); - - vm.stopPrank(); - - string[] memory tiers = new string[](2); - tiers[0] = tier2; - tiers[1] = tier1; - - uint256 claimQuantity = 25; - - _setupClaimSignature(tiers, claimQuantity); - - assertEq(tieredDrop.hasRole(keccak256("MINTER_ROLE"), deployerSigner), true); - - vm.warp(claimRequest.validityStartTimestamp); - vm.prank(claimer); - tieredDrop.claimWithSignature(claimRequest, claimSignature); - assertEq(tieredDrop.balanceOf(claimer), claimQuantity); - - for (uint256 i = 0; i < claimQuantity; i += 1) { - // Outputs: - // Checking 0 baseURI2/10 - // Checking 1 baseURI2/11 - // Checking 2 baseURI2/12 - // Checking 3 baseURI2/13 - // Checking 4 baseURI2/14 - // Checking 5 baseURI2/15 - // Checking 6 baseURI2/16 - // Checking 7 baseURI2/17 - // Checking 8 baseURI2/18 - // Checking 9 baseURI2/19 - // Checking 10 baseURI3/50 - // Checking 11 baseURI3/51 - // Checking 12 baseURI3/52 - // Checking 13 baseURI3/53 - // Checking 14 baseURI3/54 - // Checking 15 baseURI3/55 - // Checking 16 baseURI3/56 - // Checking 17 baseURI3/57 - // Checking 18 baseURI3/58 - // Checking 19 baseURI3/59 - // Checking 20 baseURI1/0 - // Checking 21 baseURI1/1 - // Checking 22 baseURI1/2 - // Checking 23 baseURI1/3 - // Checking 24 baseURI1/4 - console.log("Checking", i, tieredDrop.tokenURI(i)); - } - } - - function test_state_claimWithSignature_IssueH1_2() public { - // Lazy mint tokens: 3 different tiers - vm.startPrank(dropAdmin); - - // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. - tieredDrop.lazyMint(quantityTier1, baseURITier1, tier1, ""); - // Tier 2: tokenIds assigned 10 -> 20 non-inclusive. - tieredDrop.lazyMint(1, baseURITier2, tier2, ""); // 10 -> 11 - tieredDrop.lazyMint(9, baseURITier2, tier2, ""); // 11 -> 20 - // Tier 3: tokenIds assigned 20 -> 50 non-inclusive. - tieredDrop.lazyMint(quantityTier3, baseURITier3, tier3, ""); - - // Tier 2: tokenIds assigned 50 -> 60 non-inclusive. - tieredDrop.lazyMint(quantityTier2 - 10, baseURITier2, tier2, ""); - - vm.stopPrank(); - - string[] memory tiers = new string[](2); - tiers[0] = tier2; - tiers[1] = tier1; - - uint256[3] memory claimQuantities = [uint256(1), uint256(3), uint256(21)]; - uint256 claimedCount = 0; - for (uint256 loop = 0; loop < 3; loop++) { - uint256 claimQuantity = claimQuantities[loop]; - uint256 offset = claimedCount; - - _setupClaimSignature(tiers, claimQuantity); - - assertEq(tieredDrop.hasRole(keccak256("MINTER_ROLE"), deployerSigner), true); - - vm.warp(claimRequest.validityStartTimestamp); - vm.prank(claimer); - tieredDrop.claimWithSignature(claimRequest, claimSignature); - - claimedCount += claimQuantity; - assertEq(tieredDrop.balanceOf(claimer), claimedCount); - - for (uint256 i = offset; i < claimQuantity + (offset); i += 1) { - // Outputs: - // Checking 0 baseURI2/10 - // Checking 1 baseURI2/11 - // Checking 2 baseURI2/12 - // Checking 3 baseURI2/13 - // Checking 4 baseURI2/14 - // Checking 5 baseURI2/15 - // Checking 6 baseURI2/16 - // Checking 7 baseURI2/17 - // Checking 8 baseURI2/18 - // Checking 9 baseURI2/19 - // Checking 10 baseURI3/50 - // Checking 11 baseURI3/51 - // Checking 12 baseURI3/52 - // Checking 13 baseURI3/53 - // Checking 14 baseURI3/54 - // Checking 15 baseURI3/55 - // Checking 16 baseURI3/56 - // Checking 17 baseURI3/57 - // Checking 18 baseURI3/58 - // Checking 19 baseURI3/59 - // Checking 20 baseURI1/0 - // Checking 21 baseURI1/1 - // Checking 22 baseURI1/2 - // Checking 23 baseURI1/3 - // Checking 24 baseURI1/4 - console.log("Checking", i, tieredDrop.tokenURI(i)); - } - } - } -} - -// contract TieredDropBechmarkTest is BaseTest { -// using Strings for uint256; - -// TieredDrop public tieredDrop; - -// address internal dropAdmin; -// address internal claimer; - -// // Signature params -// address internal deployerSigner; -// bytes32 internal typehashGenericRequest; -// bytes32 internal nameHash; -// bytes32 internal versionHash; -// bytes32 internal typehashEip712; -// bytes32 internal domainSeparator; - -// // Lazy mint variables -// uint256 internal quantityTier1 = 10; -// string internal tier1 = "tier1"; -// string internal baseURITier1 = "baseURI1/"; -// string internal placeholderURITier1 = "placeholderURI1/"; -// bytes internal keyTier1 = "tier1_key"; - -// uint256 internal quantityTier2 = 20; -// string internal tier2 = "tier2"; -// string internal baseURITier2 = "baseURI2/"; -// string internal placeholderURITier2 = "placeholderURI2/"; -// bytes internal keyTier2 = "tier2_key"; - -// uint256 internal quantityTier3 = 30; -// string internal tier3 = "tier3"; -// string internal baseURITier3 = "baseURI3/"; -// string internal placeholderURITier3 = "placeholderURI3/"; -// bytes internal keyTier3 = "tier3_key"; - -// function setUp() public virtual override { -// super.setUp(); - -// dropAdmin = getActor(1); -// claimer = getActor(2); - -// // Deploy implementation. -// address tieredDropImpl = address(new TieredDrop()); - -// // Deploy proxy pointing to implementaion. -// vm.prank(dropAdmin); -// tieredDrop = TieredDrop( -// address( -// new TWProxy( -// tieredDropImpl, -// abi.encodeCall( -// TieredDrop.initialize, -// (dropAdmin, "Tiered Drop", "TD", "ipfs://", new address[](0), dropAdmin, dropAdmin, 0) -// ) -// ) -// ) -// ); - -// // ====== signature params - -// deployerSigner = signer; -// vm.prank(dropAdmin); -// tieredDrop.grantRole(keccak256("MINTER_ROLE"), deployerSigner); - -// typehashGenericRequest = keccak256( -// "GenericRequest(uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid,bytes data)" -// ); -// nameHash = keccak256(bytes("SignatureAction")); -// versionHash = keccak256(bytes("1")); -// typehashEip712 = keccak256( -// "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" -// ); -// domainSeparator = keccak256( -// abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tieredDrop)) -// ); - -// // ====== - -// // Lazy mint tokens: 3 different tiers -// vm.startPrank(dropAdmin); - -// // Tier 1: tokenIds assigned 0 -> 10 non-inclusive. -// tieredDrop.lazyMint(totalQty, baseURITier1, tier1, ""); -// // Tier 2: tokenIds assigned 10 -> 30 non-inclusive. -// tieredDrop.lazyMint(totalQty, baseURITier2, tier2, ""); - -// vm.stopPrank(); - -// /** -// * Claim tokens. -// * - Order of priority: [tier2, tier1] -// * - Total quantity: 25. [20 from tier2, 5 from tier1] -// */ - -// string[] memory tiers = new string[](2); -// tiers[0] = tier2; -// tiers[1] = tier1; - -// uint256 claimQuantity = totalQty; - -// for (uint256 i = 0; i < claimQuantity; i += 1) { -// _setupClaimSignature(tiers, 1); - -// vm.warp(claimRequest.validityStartTimestamp); - -// vm.prank(claimer); -// tieredDrop.claimWithSignature(claimRequest, claimSignature); -// } -// } - -// TieredDrop.GenericRequest internal claimRequest; -// bytes internal claimSignature; - -// uint256 internal nonce; - -// function _setupClaimSignature(string[] memory _orderedTiers, uint256 _totalQuantity) internal { -// claimRequest.validityStartTimestamp = 1000; -// claimRequest.validityEndTimestamp = 2000; -// claimRequest.uid = keccak256(abi.encodePacked(nonce)); -// nonce += 1; -// claimRequest.data = abi.encode( -// _orderedTiers, -// claimer, -// address(0), -// 0, -// dropAdmin, -// _totalQuantity, -// 0, -// NATIVE_TOKEN -// ); - -// bytes memory encodedRequest = abi.encode( -// typehashGenericRequest, -// claimRequest.validityStartTimestamp, -// claimRequest.validityEndTimestamp, -// claimRequest.uid, -// keccak256(bytes(claimRequest.data)) -// ); - -// bytes32 structHash = keccak256(encodedRequest); -// bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); - -// (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); -// claimSignature = abi.encodePacked(r, s, v); -// } - -// // What does it take to exhaust the 550mil RPC view fn gas limit ? - -// // 10_000: 67 mil gas (67,536,754) -// uint256 internal totalQty = 10_000; - -// function test_banchmark_getTokensInTier() public view { -// tieredDrop.getTokensInTier(tier1, 0, totalQty); -// } - -// function test_banchmark_getTokensInTier_ten() public view { -// tieredDrop.getTokensInTier(tier1, 0, 10); -// } - -// function test_banchmark_getTokensInTier_hundred() public view { -// tieredDrop.getTokensInTier(tier1, 0, 100); -// } -// } diff --git a/src/test/benchmark/PackBenchmark.t.sol b/src/test/benchmark/PackBenchmark.t.sol deleted file mode 100644 index 5048713b0..000000000 --- a/src/test/benchmark/PackBenchmark.t.sol +++ /dev/null @@ -1,222 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -import { Pack, IERC2981Upgradeable, IERC721Receiver, IERC1155Upgradeable } from "contracts/prebuilts/pack/Pack.sol"; -import { IPack } from "contracts/prebuilts/interface/IPack.sol"; -import { ITokenBundle } from "contracts/extension/interface/ITokenBundle.sol"; - -// Test imports -import { MockERC20 } from "../mocks/MockERC20.sol"; -import { Wallet } from "../utils/Wallet.sol"; -import "../utils/BaseTest.sol"; - -contract PackBenchmarkTest is BaseTest { - /// @notice Emitted when a set of packs is created. - event PackCreated(uint256 indexed packId, address recipient, uint256 totalPacksCreated); - - /// @notice Emitted when a pack is opened. - event PackOpened( - uint256 indexed packId, - address indexed opener, - uint256 numOfPacksOpened, - ITokenBundle.Token[] rewardUnitsDistributed - ); - - Pack internal pack; - - Wallet internal tokenOwner; - string internal packUri; - ITokenBundle.Token[] internal packContents; - ITokenBundle.Token[] internal additionalContents; - uint256[] internal numOfRewardUnits; - uint256[] internal additionalContentsRewardUnits; - - function setUp() public override { - super.setUp(); - - pack = Pack(payable(getContract("Pack"))); - - tokenOwner = getWallet(); - packUri = "ipfs://"; - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 0, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 0, - totalAmount: 100 - }) - ); - numOfRewardUnits.push(20); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - numOfRewardUnits.push(50); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 1, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 2, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - numOfRewardUnits.push(100); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 3, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 4, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 5, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 1, - totalAmount: 500 - }) - ); - numOfRewardUnits.push(50); - - erc20.mint(address(tokenOwner), 2000 ether); - erc721.mint(address(tokenOwner), 6); - erc1155.mint(address(tokenOwner), 0, 100); - erc1155.mint(address(tokenOwner), 1, 500); - - // additional contents, to check `addPackContents` - additionalContents.push( - ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 2, - totalAmount: 200 - }) - ); - additionalContentsRewardUnits.push(50); - - additionalContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - additionalContentsRewardUnits.push(100); - - tokenOwner.setAllowanceERC20(address(erc20), address(pack), type(uint256).max); - tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), true); - tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), true); - - vm.prank(deployer); - pack.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); - } - - /*/////////////////////////////////////////////////////////////// - Benchmark: Pack - //////////////////////////////////////////////////////////////*/ - - function test_benchmark_pack_createPack() public { - vm.pauseGasMetering(); - address recipient = address(1); - - vm.prank(address(tokenOwner)); - vm.resumeGasMetering(); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - function test_benchmark_pack_addPackContents() public { - vm.pauseGasMetering(); - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, packContents.length); - - erc20.mint(address(tokenOwner), 1000 ether); - erc1155.mint(address(tokenOwner), 2, 200); - - vm.prank(address(tokenOwner)); - vm.resumeGasMetering(); - pack.addPackContents(packId, additionalContents, additionalContentsRewardUnits, recipient); - } - - function test_benchmark_pack_openPack() public { - vm.pauseGasMetering(); - vm.warp(1000); - uint256 packId = pack.nextTokenIdToMint(); - uint256 packsToOpen = 3; - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); - - vm.prank(recipient, recipient); - vm.resumeGasMetering(); - pack.openPack(packId, packsToOpen); - } -} diff --git a/src/test/benchmark/PackVRFDirectBenchmark.t.sol b/src/test/benchmark/PackVRFDirectBenchmark.t.sol deleted file mode 100644 index b20af8f7d..000000000 --- a/src/test/benchmark/PackVRFDirectBenchmark.t.sol +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -import { PackVRFDirect, IERC2981Upgradeable, IERC721Receiver, IERC1155Upgradeable } from "contracts/prebuilts/pack/PackVRFDirect.sol"; -import { IPack } from "contracts/prebuilts/interface/IPack.sol"; -import { ITokenBundle } from "contracts/extension/interface/ITokenBundle.sol"; - -// Test imports -import { MockERC20 } from "../mocks/MockERC20.sol"; -import { Wallet } from "../utils/Wallet.sol"; -import "../utils/BaseTest.sol"; - -contract PackVRFDirectBenchmarkTest is BaseTest { - /// @notice Emitted when a set of packs is created. - event PackCreated(uint256 indexed packId, address recipient, uint256 totalPacksCreated); - - /// @notice Emitted when the opening of a pack is requested. - event PackOpenRequested(address indexed opener, uint256 indexed packId, uint256 amountToOpen, uint256 requestId); - - /// @notice Emitted when Chainlink VRF fulfills a random number request. - event PackRandomnessFulfilled(uint256 indexed packId, uint256 indexed requestId); - - /// @notice Emitted when a pack is opened. - event PackOpened( - uint256 indexed packId, - address indexed opener, - uint256 numOfPacksOpened, - ITokenBundle.Token[] rewardUnitsDistributed - ); - - PackVRFDirect internal pack; - - Wallet internal tokenOwner; - string internal packUri; - ITokenBundle.Token[] internal packContents; - ITokenBundle.Token[] internal additionalContents; - uint256[] internal numOfRewardUnits; - uint256[] internal additionalContentsRewardUnits; - - function setUp() public virtual override { - super.setUp(); - - pack = PackVRFDirect(payable(getContract("PackVRFDirect"))); - - tokenOwner = getWallet(); - packUri = "ipfs://"; - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 0, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 0, - totalAmount: 100 - }) - ); - numOfRewardUnits.push(20); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - numOfRewardUnits.push(50); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 1, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 2, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - numOfRewardUnits.push(100); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 3, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 4, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 5, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 1, - totalAmount: 500 - }) - ); - numOfRewardUnits.push(50); - - erc20.mint(address(tokenOwner), 2000 ether); - erc721.mint(address(tokenOwner), 6); - erc1155.mint(address(tokenOwner), 0, 100); - erc1155.mint(address(tokenOwner), 1, 500); - - // additional contents, to check `addPackContents` - additionalContents.push( - ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 2, - totalAmount: 200 - }) - ); - additionalContentsRewardUnits.push(50); - - additionalContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - additionalContentsRewardUnits.push(100); - - tokenOwner.setAllowanceERC20(address(erc20), address(pack), type(uint256).max); - tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), true); - tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), true); - - vm.prank(deployer); - pack.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); - } - - /*/////////////////////////////////////////////////////////////// - Benchmark: PackVRFDirect - //////////////////////////////////////////////////////////////*/ - - function test_benchmark_packvrf_createPack() public { - vm.pauseGasMetering(); - address recipient = address(1); - vm.prank(address(tokenOwner)); - vm.resumeGasMetering(); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - function test_benchmark_packvrf_openPackAndClaimRewards() public { - vm.pauseGasMetering(); - vm.warp(1000); - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); - - vm.prank(recipient, recipient); - vm.resumeGasMetering(); - } - - function test_benchmark_packvrf_openPack() public { - vm.pauseGasMetering(); - vm.warp(1000); - uint256 packId = pack.nextTokenIdToMint(); - uint256 packsToOpen = 3; - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); - - vm.prank(recipient, recipient); - vm.resumeGasMetering(); - pack.openPack(packId, packsToOpen); - } -} diff --git a/src/test/benchmark/SignatureDropBenchmark.t.sol b/src/test/benchmark/SignatureDropBenchmark.t.sol deleted file mode 100644 index 1c6607374..000000000 --- a/src/test/benchmark/SignatureDropBenchmark.t.sol +++ /dev/null @@ -1,214 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -import { SignatureDrop, IDropSinglePhase, IDelayedReveal, ISignatureMintERC721, ERC721AUpgradeable, IPermissions, ILazyMint } from "contracts/prebuilts/signature-drop/SignatureDrop.sol"; - -// Test imports -import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; -import "../utils/BaseTest.sol"; - -contract SignatureDropBenchmarkTest is BaseTest { - using Strings for uint256; - using Strings for address; - - event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); - event TokenURIRevealed(uint256 indexed index, string revealedURI); - event TokensMintedWithSignature( - address indexed signer, - address indexed mintedTo, - uint256 indexed tokenIdMinted, - SignatureDrop.MintRequest mintRequest - ); - - SignatureDrop public sigdrop; - address internal deployerSigner; - bytes32 internal typehashMintRequest; - bytes32 internal nameHash; - bytes32 internal versionHash; - bytes32 internal typehashEip712; - bytes32 internal domainSeparator; - - bytes private emptyEncodedBytes = abi.encode("", ""); - - using stdStorage for StdStorage; - - function setUp() public override { - super.setUp(); - deployerSigner = signer; - sigdrop = SignatureDrop(getContract("SignatureDrop")); - - erc20.mint(deployerSigner, 1_000 ether); - vm.deal(deployerSigner, 1_000 ether); - - typehashMintRequest = keccak256( - "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" - ); - nameHash = keccak256(bytes("SignatureMintERC721")); - versionHash = keccak256(bytes("1")); - typehashEip712 = keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ); - domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(sigdrop))); - } - - /*/////////////////////////////////////////////////////////////// - SignatureDrop benchmark - //////////////////////////////////////////////////////////////*/ - - function test_benchmark_signatureDrop_claim_five_tokens() public { - vm.pauseGasMetering(); - vm.warp(1); - - address receiver = getActor(0); - bytes32[] memory proofs = new bytes32[](0); - - SignatureDrop.AllowlistProof memory alp; - alp.proof = proofs; - - SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerWallet = 100; - - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - - vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions[0], false); - - vm.prank(getActor(5), getActor(5)); - vm.resumeGasMetering(); - sigdrop.claim(receiver, 5, address(0), 0, alp, ""); - } - - function test_benchmark_signatureDrop_setClaimConditions() public { - vm.pauseGasMetering(); - vm.warp(1); - bytes32[] memory proofs = new bytes32[](0); - - SignatureDrop.AllowlistProof memory alp; - alp.proof = proofs; - - SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - conditions[0].maxClaimableSupply = 100; - conditions[0].quantityLimitPerWallet = 100; - - vm.prank(deployerSigner); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - - vm.prank(deployerSigner); - vm.resumeGasMetering(); - sigdrop.setClaimConditions(conditions[0], false); - } - - function test_benchmark_signatureDrop_lazyMint() public { - vm.pauseGasMetering(); - vm.prank(deployerSigner); - vm.resumeGasMetering(); - sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - } - - function test_benchmark_signatureDrop_lazyMint_for_delayed_reveal() public { - vm.pauseGasMetering(); - uint256 amountToLazyMint = 100; - string memory baseURI = "ipfs://"; - bytes memory encryptedBaseURI = "encryptedBaseURI://"; - bytes32 provenanceHash = bytes32("whatever"); - - vm.prank(deployerSigner); - vm.resumeGasMetering(); - sigdrop.lazyMint(amountToLazyMint, baseURI, abi.encode(encryptedBaseURI, provenanceHash)); - } - - function test_benchmark_signatureDrop_reveal() public { - vm.pauseGasMetering(); - - bytes memory key = "key"; - uint256 amountToLazyMint = 100; - bytes memory secretURI = "ipfs://"; - string memory placeholderURI = "abcd://"; - bytes memory encryptedURI = sigdrop.encryptDecrypt(secretURI, key); - bytes32 provenanceHash = keccak256(abi.encodePacked(secretURI, key, block.chainid)); - - vm.prank(deployerSigner); - sigdrop.lazyMint(amountToLazyMint, placeholderURI, abi.encode(encryptedURI, provenanceHash)); - - vm.prank(deployerSigner); - vm.resumeGasMetering(); - sigdrop.reveal(0, key); - } - - // function test_benchmark_signatureDrop_claim_one_token() public { - // vm.pauseGasMetering(); - // vm.warp(1); - - // address receiver = getActor(0); - // bytes32[] memory proofs = new bytes32[](0); - - // SignatureDrop.AllowlistProof memory alp; - // alp.proof = proofs; - - // SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - // conditions[0].maxClaimableSupply = 100; - // conditions[0].quantityLimitPerWallet = 100; - - // vm.prank(deployerSigner); - // sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - - // vm.prank(deployerSigner); - // sigdrop.setClaimConditions(conditions[0], false); - - // vm.prank(getActor(5), getActor(5)); - // vm.resumeGasMetering(); - // sigdrop.claim(receiver, 1, address(0), 0, alp, ""); - // } - - // function test_benchmark_signatureDrop_claim_two_tokens() public { - // vm.pauseGasMetering(); - // vm.warp(1); - - // address receiver = getActor(0); - // bytes32[] memory proofs = new bytes32[](0); - - // SignatureDrop.AllowlistProof memory alp; - // alp.proof = proofs; - - // SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - // conditions[0].maxClaimableSupply = 100; - // conditions[0].quantityLimitPerWallet = 100; - - // vm.prank(deployerSigner); - // sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - - // vm.prank(deployerSigner); - // sigdrop.setClaimConditions(conditions[0], false); - - // vm.prank(getActor(5), getActor(5)); - // vm.resumeGasMetering(); - // sigdrop.claim(receiver, 2, address(0), 0, alp, ""); - // } - - // function test_benchmark_signatureDrop_claim_three_tokens() public { - // vm.pauseGasMetering(); - // vm.warp(1); - - // address receiver = getActor(0); - // bytes32[] memory proofs = new bytes32[](0); - - // SignatureDrop.AllowlistProof memory alp; - // alp.proof = proofs; - - // SignatureDrop.ClaimCondition[] memory conditions = new SignatureDrop.ClaimCondition[](1); - // conditions[0].maxClaimableSupply = 100; - // conditions[0].quantityLimitPerWallet = 100; - - // vm.prank(deployerSigner); - // sigdrop.lazyMint(100, "ipfs://", emptyEncodedBytes); - - // vm.prank(deployerSigner); - // sigdrop.setClaimConditions(conditions[0], false); - - // vm.prank(getActor(5), getActor(5)); - // vm.resumeGasMetering(); - // sigdrop.claim(receiver, 3, address(0), 0, alp, ""); - // } -} diff --git a/src/test/pack/Pack.t.sol b/src/test/pack/Pack.t.sol deleted file mode 100644 index 37fdae635..000000000 --- a/src/test/pack/Pack.t.sol +++ /dev/null @@ -1,1253 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -import { Pack, IERC2981Upgradeable, IERC721Receiver, IERC1155Upgradeable } from "contracts/prebuilts/pack/Pack.sol"; -import { IPack } from "contracts/prebuilts/interface/IPack.sol"; -import { ITokenBundle } from "contracts/extension/interface/ITokenBundle.sol"; -import { CurrencyTransferLib } from "contracts/lib/CurrencyTransferLib.sol"; - -// Test imports -import { MockERC20 } from "../mocks/MockERC20.sol"; -import { Wallet } from "../utils/Wallet.sol"; -import "../utils/BaseTest.sol"; - -contract PackTest is BaseTest { - /// @notice Emitted when a set of packs is created. - event PackCreated(uint256 indexed packId, address recipient, uint256 totalPacksCreated); - - /// @notice Emitted when a pack is opened. - event PackOpened( - uint256 indexed packId, - address indexed opener, - uint256 numOfPacksOpened, - ITokenBundle.Token[] rewardUnitsDistributed - ); - - Pack internal pack; - - Wallet internal tokenOwner; - string internal packUri; - ITokenBundle.Token[] internal packContents; - ITokenBundle.Token[] internal additionalContents; - uint256[] internal numOfRewardUnits; - uint256[] internal additionalContentsRewardUnits; - - function setUp() public override { - super.setUp(); - - pack = Pack(payable(getContract("Pack"))); - - tokenOwner = getWallet(); - packUri = "ipfs://"; - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 0, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 0, - totalAmount: 100 - }) - ); - numOfRewardUnits.push(20); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - numOfRewardUnits.push(50); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 1, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 2, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - numOfRewardUnits.push(100); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 3, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 4, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 5, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 1, - totalAmount: 500 - }) - ); - numOfRewardUnits.push(50); - - erc20.mint(address(tokenOwner), 2000 ether); - erc721.mint(address(tokenOwner), 6); - erc1155.mint(address(tokenOwner), 0, 100); - erc1155.mint(address(tokenOwner), 1, 500); - - // additional contents, to check `addPackContents` - additionalContents.push( - ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 2, - totalAmount: 200 - }) - ); - additionalContentsRewardUnits.push(50); - - additionalContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - additionalContentsRewardUnits.push(100); - - tokenOwner.setAllowanceERC20(address(erc20), address(pack), type(uint256).max); - tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), true); - tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), true); - - vm.prank(deployer); - pack.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); - } - - /*/////////////////////////////////////////////////////////////// - Unit tests: Miscellaneous - //////////////////////////////////////////////////////////////*/ - - function test_revert_addPackContents_RandomAccountGrief() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - // random address tries to transfer zero amount - address randomAccount = address(0x123); - vm.prank(randomAccount); - pack.safeTransferFrom(randomAccount, address(567), packId, 0, ""); // zero transfer - - // canUpdatePack should remain true, since no packs were transferred - assertTrue(pack.canUpdatePack(packId)); - - erc20.mint(address(tokenOwner), 1000 ether); - erc1155.mint(address(tokenOwner), 2, 200); - - vm.prank(address(tokenOwner)); - // Should not revert - pack.addPackContents(packId, additionalContents, additionalContentsRewardUnits, recipient); - } - - function test_checkForwarders() public { - assertFalse(pack.isTrustedForwarder(eoaForwarder)); - assertFalse(pack.isTrustedForwarder(forwarder)); - } - - /*/////////////////////////////////////////////////////////////// - Unit tests: `createPack` - //////////////////////////////////////////////////////////////*/ - - function test_interface() public pure { - console2.logBytes4(type(IERC20).interfaceId); - console2.logBytes4(type(IERC721).interfaceId); - console2.logBytes4(type(IERC1155).interfaceId); - } - - function test_supportsInterface() public { - assertEq(pack.supportsInterface(type(IERC2981Upgradeable).interfaceId), true); - assertEq(pack.supportsInterface(type(IERC721Receiver).interfaceId), true); - assertEq(pack.supportsInterface(type(IERC1155Receiver).interfaceId), true); - assertEq(pack.supportsInterface(type(IERC1155Upgradeable).interfaceId), true); - } - - /** - * note: Testing state changes; token owner calls `createPack` to pack owned tokens. - */ - function test_state_createPack() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - assertEq(packId + 1, pack.nextTokenIdToMint()); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, packContents.length); - for (uint256 i = 0; i < packed.length; i += 1) { - assertEq(packed[i].assetContract, packContents[i].assetContract); - assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); - assertEq(packed[i].tokenId, packContents[i].tokenId); - assertEq(packed[i].totalAmount, packContents[i].totalAmount); - } - - assertEq(packUri, pack.uri(packId)); - } - - /* - * note: Testing state changes; token owner calls `createPack` to pack native tokens. - */ - function test_state_createPack_nativeTokens() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - vm.deal(address(tokenOwner), 100 ether); - packContents.push( - ITokenBundle.Token({ - assetContract: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 20 ether - }) - ); - numOfRewardUnits.push(20); - - vm.prank(address(tokenOwner)); - pack.createPack{ value: 20 ether }(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - assertEq(packId + 1, pack.nextTokenIdToMint()); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, packContents.length); - for (uint256 i = 0; i < packed.length; i += 1) { - assertEq(packed[i].assetContract, packContents[i].assetContract); - assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); - assertEq(packed[i].tokenId, packContents[i].tokenId); - assertEq(packed[i].totalAmount, packContents[i].totalAmount); - } - - assertEq(packUri, pack.uri(packId)); - } - - /** - * note: Testing state changes; token owner calls `createPack` to pack owned tokens. - * Only assets with ASSET_ROLE can be packed. - */ - function test_state_createPack_withAssetRoleRestriction() public { - vm.startPrank(deployer); - pack.revokeRole(keccak256("ASSET_ROLE"), address(0)); - for (uint256 i = 0; i < packContents.length; i += 1) { - if (!pack.hasRole(keccak256("ASSET_ROLE"), packContents[i].assetContract)) { - pack.grantRole(keccak256("ASSET_ROLE"), packContents[i].assetContract); - } - } - vm.stopPrank(); - - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - assertEq(packId + 1, pack.nextTokenIdToMint()); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, packContents.length); - for (uint256 i = 0; i < packed.length; i += 1) { - assertEq(packed[i].assetContract, packContents[i].assetContract); - assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); - assertEq(packed[i].tokenId, packContents[i].tokenId); - assertEq(packed[i].totalAmount, packContents[i].totalAmount); - } - - assertEq(packUri, pack.uri(packId)); - } - - /** - * note: Testing event emission; token owner calls `createPack` to pack owned tokens. - */ - function test_event_createPack_PackCreated() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectEmit(true, true, true, true); - emit PackCreated(packId, recipient, 226); - - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - vm.stopPrank(); - } - - /** - * note: Testing token balances; token owner calls `createPack` to pack owned tokens. - */ - function test_balances_createPack() public { - // ERC20 balance - assertEq(erc20.balanceOf(address(tokenOwner)), 2000 ether); - assertEq(erc20.balanceOf(address(pack)), 0); - - // ERC721 balance - assertEq(erc721.ownerOf(0), address(tokenOwner)); - assertEq(erc721.ownerOf(1), address(tokenOwner)); - assertEq(erc721.ownerOf(2), address(tokenOwner)); - assertEq(erc721.ownerOf(3), address(tokenOwner)); - assertEq(erc721.ownerOf(4), address(tokenOwner)); - assertEq(erc721.ownerOf(5), address(tokenOwner)); - - // ERC1155 balance - assertEq(erc1155.balanceOf(address(tokenOwner), 0), 100); - assertEq(erc1155.balanceOf(address(pack), 0), 0); - - assertEq(erc1155.balanceOf(address(tokenOwner), 1), 500); - assertEq(erc1155.balanceOf(address(pack), 1), 0); - - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(1); - - vm.prank(address(tokenOwner)); - (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - // ERC20 balance - assertEq(erc20.balanceOf(address(tokenOwner)), 0); - assertEq(erc20.balanceOf(address(pack)), 2000 ether); - - // ERC721 balance - assertEq(erc721.ownerOf(0), address(pack)); - assertEq(erc721.ownerOf(1), address(pack)); - assertEq(erc721.ownerOf(2), address(pack)); - assertEq(erc721.ownerOf(3), address(pack)); - assertEq(erc721.ownerOf(4), address(pack)); - assertEq(erc721.ownerOf(5), address(pack)); - - // ERC1155 balance - assertEq(erc1155.balanceOf(address(tokenOwner), 0), 0); - assertEq(erc1155.balanceOf(address(pack), 0), 100); - - assertEq(erc1155.balanceOf(address(tokenOwner), 1), 0); - assertEq(erc1155.balanceOf(address(pack), 1), 500); - - // Pack wrapped token balance - assertEq(pack.balanceOf(address(recipient), packId), totalSupply); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack owned tokens. - * Only assets with ASSET_ROLE can be packed, but assets being packed don't have that role. - */ - function test_revert_createPack_access_ASSET_ROLE() public { - vm.prank(deployer); - pack.revokeRole(keccak256("ASSET_ROLE"), address(0)); - - address recipient = address(0x123); - - vm.prank(address(tokenOwner)); - vm.expectRevert( - abi.encodeWithSelector( - Permissions.PermissionsUnauthorizedAccount.selector, - address(erc721), - keccak256("ASSET_ROLE") - ) - ); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack owned tokens, without MINTER_ROLE. - */ - function test_revert_createPack_access_MINTER_ROLE() public { - vm.prank(address(tokenOwner)); - pack.renounceRole(keccak256("MINTER_ROLE"), address(tokenOwner)); - - address recipient = address(0x123); - - vm.prank(address(tokenOwner)); - vm.expectRevert( - abi.encodeWithSelector( - Permissions.PermissionsUnauthorizedAccount.selector, - address(tokenOwner), - keccak256("MINTER_ROLE") - ) - ); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` with insufficient value when packing native tokens. - */ - function test_revert_createPack_nativeTokens_insufficientValue() public { - address recipient = address(0x123); - - vm.deal(address(tokenOwner), 100 ether); - - packContents.push( - ITokenBundle.Token({ - assetContract: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 20 ether - }) - ); - numOfRewardUnits.push(1); - - vm.prank(address(tokenOwner)); - vm.expectRevert( - abi.encodeWithSelector(CurrencyTransferLib.CurrencyTransferLibMismatchedValue.selector, 0, 20 ether) - ); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC20 tokens. - */ - function test_revert_createPack_notOwner_ERC20() public { - tokenOwner.transferERC20(address(erc20), address(0x12), 1000 ether); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC20: transfer amount exceeds balance"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC721 tokens. - */ - function test_revert_createPack_notOwner_ERC721() public { - tokenOwner.transferERC721(address(erc721), address(0x12), 0); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC721: caller is not token owner or approved"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC1155 tokens. - */ - function test_revert_createPack_notOwner_ERC1155() public { - tokenOwner.transferERC1155(address(erc1155), address(0x12), 0, 100, ""); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC1155: insufficient balance for transfer"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC20 tokens. - */ - function test_revert_createPack_notApprovedTransfer_ERC20() public { - tokenOwner.setAllowanceERC20(address(erc20), address(pack), 0); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC20: insufficient allowance"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC721 tokens. - */ - function test_revert_createPack_notApprovedTransfer_ERC721() public { - tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), false); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC721: caller is not token owner or approved"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC1155 tokens. - */ - function test_revert_createPack_notApprovedTransfer_ERC1155() public { - tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), false); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC1155: caller is not token owner or approved"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` with invalid token-type. - */ - function test_revert_createPack_invalidTokenType() public { - ITokenBundle.Token[] memory invalidContent = new ITokenBundle.Token[](1); - uint256[] memory rewardUnits = new uint256[](1); - - invalidContent[0] = ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1 - }); - rewardUnits[0] = 1; - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("!TokenType"); - pack.createPack(invalidContent, rewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` with total-amount as 0. - */ - function test_revert_createPack_zeroTotalAmount() public { - ITokenBundle.Token[] memory invalidContent = new ITokenBundle.Token[](1); - uint256[] memory rewardUnits = new uint256[](1); - - invalidContent[0] = ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 0 - }); - rewardUnits[0] = 10; - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("0 amt"); - pack.createPack(invalidContent, rewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` with no tokens to pack. - */ - function test_revert_createPack_noTokensToPack() public { - ITokenBundle.Token[] memory emptyContent; - uint256[] memory rewardUnits; - - address recipient = address(0x123); - - bytes memory err = "!Len"; - vm.startPrank(address(tokenOwner)); - vm.expectRevert(err); - pack.createPack(emptyContent, rewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` with unequal length of contents and rewardUnits. - */ - function test_revert_createPack_invalidRewardUnits() public { - uint256[] memory rewardUnits; - - address recipient = address(0x123); - - bytes memory err = "!Len"; - vm.startPrank(address(tokenOwner)); - vm.expectRevert(err); - pack.createPack(packContents, rewardUnits, packUri, 0, 1, recipient); - } - - /*/////////////////////////////////////////////////////////////// - Unit tests: `addPackContents` - //////////////////////////////////////////////////////////////*/ - - /** - * note: Testing state changes; token owner calls `addPackContents` to pack more tokens. - */ - function test_state_addPackContents() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, packContents.length); - for (uint256 i = 0; i < packed.length; i += 1) { - assertEq(packed[i].assetContract, packContents[i].assetContract); - assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); - assertEq(packed[i].tokenId, packContents[i].tokenId); - assertEq(packed[i].totalAmount, packContents[i].totalAmount); - } - - erc20.mint(address(tokenOwner), 1000 ether); - erc1155.mint(address(tokenOwner), 2, 200); - - vm.prank(address(tokenOwner)); - pack.addPackContents(packId, additionalContents, additionalContentsRewardUnits, recipient); - - (packed, ) = pack.getPackContents(packId); - assertEq(packed.length, packContents.length + additionalContents.length); - for (uint256 i = packContents.length; i < packed.length; i += 1) { - assertEq(packed[i].assetContract, additionalContents[i - packContents.length].assetContract); - assertEq(uint256(packed[i].tokenType), uint256(additionalContents[i - packContents.length].tokenType)); - assertEq(packed[i].tokenId, additionalContents[i - packContents.length].tokenId); - assertEq(packed[i].totalAmount, additionalContents[i - packContents.length].totalAmount); - } - } - - /** - * note: Testing token balances; token owner calls `addPackContents` to pack more tokens - * in an already existing pack. - */ - function test_balances_addPackContents() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(1); - - vm.prank(address(tokenOwner)); - (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - // ERC20 balance - assertEq(erc20.balanceOf(address(tokenOwner)), 0); - assertEq(erc20.balanceOf(address(pack)), 2000 ether); - - // ERC721 balance - assertEq(erc721.ownerOf(0), address(pack)); - assertEq(erc721.ownerOf(1), address(pack)); - assertEq(erc721.ownerOf(2), address(pack)); - assertEq(erc721.ownerOf(3), address(pack)); - assertEq(erc721.ownerOf(4), address(pack)); - assertEq(erc721.ownerOf(5), address(pack)); - - // ERC1155 balance - assertEq(erc1155.balanceOf(address(tokenOwner), 0), 0); - assertEq(erc1155.balanceOf(address(pack), 0), 100); - - assertEq(erc1155.balanceOf(address(tokenOwner), 1), 0); - assertEq(erc1155.balanceOf(address(pack), 1), 500); - - // Pack wrapped token balance - assertEq(pack.balanceOf(address(recipient), packId), totalSupply); - - erc20.mint(address(tokenOwner), 1000 ether); - erc1155.mint(address(tokenOwner), 2, 200); - - vm.prank(address(tokenOwner)); - (uint256 newTotalSupply, uint256 additionalSupply) = pack.addPackContents( - packId, - additionalContents, - additionalContentsRewardUnits, - recipient - ); - - // ERC20 balance after adding more tokens - assertEq(erc20.balanceOf(address(tokenOwner)), 0); - assertEq(erc20.balanceOf(address(pack)), 3000 ether); - - // ERC1155 balance after adding more tokens - assertEq(erc1155.balanceOf(address(tokenOwner), 2), 0); - assertEq(erc1155.balanceOf(address(pack), 2), 200); - - // Pack wrapped token balance - assertEq(pack.balanceOf(address(recipient), packId), newTotalSupply); - assertEq(totalSupply + additionalSupply, newTotalSupply); - } - - /** - * note: Testing revert condition; non-creator calls `addPackContents`. - */ - function test_revert_addPackContents_NotMinterRole() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - address randomAccount = address(0x123); - - vm.prank(randomAccount); - vm.expectRevert( - abi.encodeWithSelector( - Permissions.PermissionsUnauthorizedAccount.selector, - randomAccount, - keccak256("MINTER_ROLE") - ) - ); - pack.addPackContents(packId, additionalContents, additionalContentsRewardUnits, recipient); - } - - /** - * note: Testing revert condition; adding tokens to non-existent pack. - */ - function test_revert_addPackContents_PackNonExistent() public { - vm.prank(address(tokenOwner)); - vm.expectRevert("!Allowed"); - pack.addPackContents(0, packContents, numOfRewardUnits, address(1)); - } - - /** - * note: Testing revert condition; adding tokens after packs have been distributed. - */ - function test_revert_addPackContents_CantUpdateAnymore() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - vm.prank(recipient); - pack.safeTransferFrom(recipient, address(567), packId, 1, ""); - - vm.prank(address(tokenOwner)); - vm.expectRevert("!Allowed"); - pack.addPackContents(packId, additionalContents, additionalContentsRewardUnits, recipient); - } - - /** - * note: Testing revert condition; adding tokens with a different recipient. - */ - function test_revert_addPackContents_NotRecipient() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - address randomRecipient = address(0x12345); - - bytes memory err = "!Bal"; - vm.expectRevert(err); - vm.prank(address(tokenOwner)); - pack.addPackContents(packId, additionalContents, additionalContentsRewardUnits, randomRecipient); - } - - /*/////////////////////////////////////////////////////////////// - Unit tests: `openPack` - //////////////////////////////////////////////////////////////*/ - - /** - * note: Testing state changes; pack owner calls `openPack` to redeem underlying rewards. - */ - function test_state_openPack() public { - vm.warp(1000); - uint256 packId = pack.nextTokenIdToMint(); - uint256 packsToOpen = 3; - address recipient = address(1); - - vm.prank(address(tokenOwner)); - (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); - - vm.prank(recipient, recipient); - ITokenBundle.Token[] memory rewardUnits = pack.openPack(packId, packsToOpen); - console2.log("total reward units: ", rewardUnits.length); - - for (uint256 i = 0; i < rewardUnits.length; i++) { - console2.log("----- reward unit number: ", i, "------"); - console2.log("asset contract: ", rewardUnits[i].assetContract); - console2.log("token type: ", uint256(rewardUnits[i].tokenType)); - console2.log("tokenId: ", rewardUnits[i].tokenId); - if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { - console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); - } else { - console2.log("total amount: ", rewardUnits[i].totalAmount); - } - console2.log(""); - } - - assertEq(packUri, pack.uri(packId)); - assertEq(pack.totalSupply(packId), totalSupply - packsToOpen); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, packContents.length); - } - - /** - * note: Total amount should get updated correctly -- reduce perUnitAmount from totalAmount of the token content, for each reward - */ - function test_state_openPack_totalAmounts_ERC721() public { - vm.warp(1000); - uint256 packId = pack.nextTokenIdToMint(); - uint256 packsToOpen = 1; - address recipient = address(1); - - erc721.mint(address(tokenOwner), 6); - - ITokenBundle.Token[] memory tempContents = new ITokenBundle.Token[](1); - uint256[] memory tempNumRewardUnits = new uint256[](1); - - tempContents[0] = ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 0, - totalAmount: 1 - }); - tempNumRewardUnits[0] = 1; - - vm.prank(address(tokenOwner)); - (, uint256 totalSupply) = pack.createPack(tempContents, tempNumRewardUnits, packUri, 0, 1, recipient); - - vm.prank(recipient, recipient); - ITokenBundle.Token[] memory rewardUnits = pack.openPack(packId, packsToOpen); - - assertEq(packUri, pack.uri(packId)); - assertEq(pack.totalSupply(packId), totalSupply - packsToOpen); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, tempContents.length); - assertEq(packed[0].totalAmount, tempContents[0].totalAmount - rewardUnits[0].totalAmount); - } - - /** - * note: Total amount should get updated correctly -- reduce perUnitAmount from totalAmount of the token content, for each reward - */ - function test_state_openPack_totalAmounts_ERC1155() public { - vm.warp(1000); - uint256 packId = pack.nextTokenIdToMint(); - uint256 packsToOpen = 1; - address recipient = address(1); - - erc1155.mint(address(tokenOwner), 0, 100); - - ITokenBundle.Token[] memory tempContents = new ITokenBundle.Token[](1); - uint256[] memory tempNumRewardUnits = new uint256[](1); - - tempContents[0] = ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 0, - totalAmount: 100 - }); - tempNumRewardUnits[0] = 10; - - vm.prank(address(tokenOwner)); - (, uint256 totalSupply) = pack.createPack(tempContents, tempNumRewardUnits, packUri, 0, 1, recipient); - - vm.prank(recipient, recipient); - ITokenBundle.Token[] memory rewardUnits = pack.openPack(packId, packsToOpen); - - assertEq(packUri, pack.uri(packId)); - assertEq(pack.totalSupply(packId), totalSupply - packsToOpen); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, tempContents.length); - assertEq(packed[0].totalAmount, tempContents[0].totalAmount - rewardUnits[0].totalAmount); - } - - /** - * note: Total amount should get updated correctly -- reduce perUnitAmount from totalAmount of the token content, for each reward - */ - function test_state_openPack_totalAmounts_ERC20() public { - vm.warp(1000); - uint256 packId = pack.nextTokenIdToMint(); - uint256 packsToOpen = 1; - address recipient = address(1); - - erc20.mint(address(tokenOwner), 2000 ether); - - ITokenBundle.Token[] memory tempContents = new ITokenBundle.Token[](1); - uint256[] memory tempNumRewardUnits = new uint256[](1); - - tempContents[0] = ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }); - tempNumRewardUnits[0] = 50; - - vm.prank(address(tokenOwner)); - (, uint256 totalSupply) = pack.createPack(tempContents, tempNumRewardUnits, packUri, 0, 1, recipient); - - vm.prank(recipient, recipient); - ITokenBundle.Token[] memory rewardUnits = pack.openPack(packId, packsToOpen); - - assertEq(packUri, pack.uri(packId)); - assertEq(pack.totalSupply(packId), totalSupply - packsToOpen); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, tempContents.length); - assertEq(packed[0].totalAmount, tempContents[0].totalAmount - rewardUnits[0].totalAmount); - } - - /** - * note: Testing event emission; pack owner calls `openPack` to open owned packs. - */ - function test_event_openPack_PackOpened() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - ITokenBundle.Token[] memory emptyRewardUnitsForTestingEvent; - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - vm.expectEmit(true, true, false, false); - emit PackOpened(packId, recipient, 1, emptyRewardUnitsForTestingEvent); - - vm.prank(recipient, recipient); - pack.openPack(packId, 1); - } - - function test_balances_openPack() public { - uint256 packId = pack.nextTokenIdToMint(); - uint256 packsToOpen = 3; - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); - - // ERC20 balance - assertEq(erc20.balanceOf(address(recipient)), 0); - assertEq(erc20.balanceOf(address(pack)), 2000 ether); - - // ERC721 balance - assertEq(erc721.ownerOf(0), address(pack)); - assertEq(erc721.ownerOf(1), address(pack)); - assertEq(erc721.ownerOf(2), address(pack)); - assertEq(erc721.ownerOf(3), address(pack)); - assertEq(erc721.ownerOf(4), address(pack)); - assertEq(erc721.ownerOf(5), address(pack)); - - // ERC1155 balance - assertEq(erc1155.balanceOf(address(recipient), 0), 0); - assertEq(erc1155.balanceOf(address(pack), 0), 100); - - assertEq(erc1155.balanceOf(address(recipient), 1), 0); - assertEq(erc1155.balanceOf(address(pack), 1), 500); - - vm.prank(recipient, recipient); - ITokenBundle.Token[] memory rewardUnits = pack.openPack(packId, packsToOpen); - console2.log("total reward units: ", rewardUnits.length); - - uint256 erc20Amount; - uint256[] memory erc1155Amounts = new uint256[](2); - uint256 erc721Amount; - - for (uint256 i = 0; i < rewardUnits.length; i++) { - console2.log("----- reward unit number: ", i, "------"); - console2.log("asset contract: ", rewardUnits[i].assetContract); - console2.log("token type: ", uint256(rewardUnits[i].tokenType)); - console2.log("tokenId: ", rewardUnits[i].tokenId); - if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { - console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); - console.log("balance of recipient: ", erc20.balanceOf(address(recipient)) / 1 ether, "ether"); - erc20Amount += rewardUnits[i].totalAmount; - } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC1155) { - console2.log("total amount: ", rewardUnits[i].totalAmount); - console.log("balance of recipient: ", erc1155.balanceOf(address(recipient), rewardUnits[i].tokenId)); - erc1155Amounts[rewardUnits[i].tokenId] += rewardUnits[i].totalAmount; - } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC721) { - console2.log("total amount: ", rewardUnits[i].totalAmount); - console.log("balance of recipient: ", erc721.balanceOf(address(recipient))); - erc721Amount += rewardUnits[i].totalAmount; - } - console2.log(""); - } - - assertEq(erc20.balanceOf(address(recipient)), erc20Amount); - assertEq(erc721.balanceOf(address(recipient)), erc721Amount); - - for (uint256 i = 0; i < erc1155Amounts.length; i += 1) { - assertEq(erc1155.balanceOf(address(recipient), i), erc1155Amounts[i]); - } - } - - /** - * note: Testing revert condition; caller of `openPack` is not EOA. - */ - function test_revert_openPack_notEOA() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - vm.startPrank(recipient, address(27)); - string memory err = "!EOA"; - vm.expectRevert(bytes(err)); - pack.openPack(packId, 1); - } - - /** - * note: Testing revert condition; pack owner calls `openPack` to open more than owned packs. - */ - function test_revert_openPack_openMoreThanOwned() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - vm.prank(address(tokenOwner)); - (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - bytes memory err = "!Bal"; - vm.startPrank(recipient, recipient); - vm.expectRevert(err); - pack.openPack(packId, totalSupply + 1); - } - - /** - * note: Testing revert condition; pack owner calls `openPack` before start timestamp. - */ - function test_revert_openPack_openBeforeStart() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 1000, 1, recipient); - - vm.startPrank(recipient, recipient); - vm.expectRevert("cant open"); - pack.openPack(packId, 1); - } - - /** - * note: Testing revert condition; pack owner calls `openPack` with pack-id non-existent or not owned. - */ - function test_revert_openPack_invalidPackId() public { - address recipient = address(0x123); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - bytes memory err = "!Bal"; - vm.startPrank(recipient, recipient); - vm.expectRevert(err); - pack.openPack(2, 1); - } - - /*/////////////////////////////////////////////////////////////// - Fuzz testing - //////////////////////////////////////////////////////////////*/ - - uint256 internal constant MAX_TOKENS = 2000; - - function getTokensToPack( - uint256 len - ) internal returns (ITokenBundle.Token[] memory tokensToPack, uint256[] memory rewardUnits) { - vm.assume(len < MAX_TOKENS); - tokensToPack = new ITokenBundle.Token[](len); - rewardUnits = new uint256[](len); - - for (uint256 i = 0; i < len; i += 1) { - uint256 random = uint256(keccak256(abi.encodePacked(len + i))) % MAX_TOKENS; - uint256 selector = random % 4; - - if (selector == 0) { - tokensToPack[i] = ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: (random + 1) * 10 ether - }); - rewardUnits[i] = random + 1; - - erc20.mint(address(tokenOwner), tokensToPack[i].totalAmount); - } else if (selector == 1) { - uint256 tokenId = erc721.nextTokenIdToMint(); - - tokensToPack[i] = ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: tokenId, - totalAmount: 1 - }); - rewardUnits[i] = 1; - - erc721.mint(address(tokenOwner), 1); - } else if (selector == 2) { - tokensToPack[i] = ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: random, - totalAmount: (random + 1) * 10 - }); - rewardUnits[i] = random + 1; - - erc1155.mint(address(tokenOwner), tokensToPack[i].tokenId, tokensToPack[i].totalAmount); - } else if (selector == 3) { - tokensToPack[i] = ITokenBundle.Token({ - assetContract: address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 5 ether - }); - rewardUnits[i] = 5; - } - } - } - - function checkBalances( - ITokenBundle.Token[] memory rewardUnits, - address - ) - internal - pure - returns (uint256 nativeTokenAmount, uint256 erc20Amount, uint256[] memory erc1155Amounts, uint256 erc721Amount) - { - erc1155Amounts = new uint256[](MAX_TOKENS); - - for (uint256 i = 0; i < rewardUnits.length; i++) { - // console2.log("----- reward unit number: ", i, "------"); - // console2.log("asset contract: ", rewardUnits[i].assetContract); - // console2.log("token type: ", uint256(rewardUnits[i].tokenType)); - // console2.log("tokenId: ", rewardUnits[i].tokenId); - if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { - if (rewardUnits[i].assetContract == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { - // console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); - // console.log("balance of recipient: ", address(recipient).balance); - nativeTokenAmount += rewardUnits[i].totalAmount; - } else { - // console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); - // console.log("balance of recipient: ", erc20.balanceOf(address(recipient)) / 1 ether, "ether"); - erc20Amount += rewardUnits[i].totalAmount; - } - } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC1155) { - // console2.log("total amount: ", rewardUnits[i].totalAmount); - // console.log("balance of recipient: ", erc1155.balanceOf(address(recipient), rewardUnits[i].tokenId)); - erc1155Amounts[rewardUnits[i].tokenId] += rewardUnits[i].totalAmount; - } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC721) { - // console2.log("total amount: ", rewardUnits[i].totalAmount); - // console.log("balance of recipient: ", erc721.balanceOf(address(recipient))); - erc721Amount += rewardUnits[i].totalAmount; - } - // console2.log(""); - } - } - - function test_fuzz_state_createPack(uint256 x, uint128 y) public { - (ITokenBundle.Token[] memory tokensToPack, uint256[] memory rewardUnits) = getTokensToPack(x); - if (tokensToPack.length == 0) { - return; - } - - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - uint256 totalRewardUnits; - uint256 nativeTokenPacked; - - for (uint256 i = 0; i < tokensToPack.length; i += 1) { - totalRewardUnits += rewardUnits[i]; - if (tokensToPack[i].assetContract == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { - nativeTokenPacked += tokensToPack[i].totalAmount; - } - } - vm.deal(address(tokenOwner), nativeTokenPacked); - vm.assume(y > 0 && totalRewardUnits % y == 0); - - vm.prank(address(tokenOwner)); - (, uint256 totalSupply) = pack.createPack{ value: nativeTokenPacked }( - tokensToPack, - rewardUnits, - packUri, - 0, - y, - recipient - ); - console2.log("total supply: ", totalSupply); - console2.log("total reward units: ", totalRewardUnits); - - assertEq(packId + 1, pack.nextTokenIdToMint()); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, tokensToPack.length); - for (uint256 i = 0; i < packed.length; i += 1) { - assertEq(packed[i].assetContract, tokensToPack[i].assetContract); - assertEq(uint256(packed[i].tokenType), uint256(tokensToPack[i].tokenType)); - assertEq(packed[i].tokenId, tokensToPack[i].tokenId); - assertEq(packed[i].totalAmount, tokensToPack[i].totalAmount); - } - - assertEq(packUri, pack.uri(packId)); - } - - /*/////////////////////////////////////////////////////////////// - Scenario/Exploit tests - //////////////////////////////////////////////////////////////*/ - /** - * note: Testing revert condition; token owner calls `createPack` to pack owned tokens. - */ - function test_revert_createPack_reentrancy() public { - MaliciousERC20 malERC20 = new MaliciousERC20(payable(address(pack))); - ITokenBundle.Token[] memory content = new ITokenBundle.Token[](1); - uint256[] memory rewards = new uint256[](1); - - malERC20.mint(address(tokenOwner), 10 ether); - content[0] = ITokenBundle.Token({ - assetContract: address(malERC20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 10 ether - }); - rewards[0] = 10; - - tokenOwner.setAllowanceERC20(address(malERC20), address(pack), 10 ether); - - address recipient = address(0x123); - - vm.prank(address(deployer)); - pack.grantRole(keccak256("MINTER_ROLE"), address(malERC20)); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ReentrancyGuard: reentrant call"); - pack.createPack(content, rewards, packUri, 0, 1, recipient); - } -} - -contract MaliciousERC20 is MockERC20, ITokenBundle { - Pack public pack; - - constructor(address payable _pack) { - pack = Pack(_pack); - } - - function transferFrom(address from, address to, uint256 amount) public override returns (bool) { - ITokenBundle.Token[] memory content = new ITokenBundle.Token[](1); - uint256[] memory rewards = new uint256[](1); - - address recipient = address(0x123); - pack.createPack(content, rewards, "", 0, 1, recipient); - return super.transferFrom(from, to, amount); - } -} diff --git a/src/test/pack/PackVRFDirect.t.sol b/src/test/pack/PackVRFDirect.t.sol deleted file mode 100644 index 55a620e19..000000000 --- a/src/test/pack/PackVRFDirect.t.sol +++ /dev/null @@ -1,1055 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -import { PackVRFDirect, IERC2981Upgradeable, IERC721Receiver, IERC1155Upgradeable } from "contracts/prebuilts/pack/PackVRFDirect.sol"; -import { IPack } from "contracts/prebuilts/interface/IPack.sol"; -import { ITokenBundle } from "contracts/extension/interface/ITokenBundle.sol"; -import { CurrencyTransferLib } from "contracts/lib/CurrencyTransferLib.sol"; - -// Test imports -import { MockERC20 } from "../mocks/MockERC20.sol"; -import { Wallet } from "../utils/Wallet.sol"; -import "../utils/BaseTest.sol"; - -contract PackVRFDirectTest is BaseTest { - /// @notice Emitted when a set of packs is created. - event PackCreated(uint256 indexed packId, address recipient, uint256 totalPacksCreated); - - /// @notice Emitted when the opening of a pack is requested. - event PackOpenRequested(address indexed opener, uint256 indexed packId, uint256 amountToOpen, uint256 requestId); - - /// @notice Emitted when Chainlink VRF fulfills a random number request. - event PackRandomnessFulfilled(uint256 indexed packId, uint256 indexed requestId); - - /// @notice Emitted when a pack is opened. - event PackOpened( - uint256 indexed packId, - address indexed opener, - uint256 numOfPacksOpened, - ITokenBundle.Token[] rewardUnitsDistributed - ); - - PackVRFDirect internal pack; - - Wallet internal tokenOwner; - string internal packUri; - ITokenBundle.Token[] internal packContents; - ITokenBundle.Token[] internal additionalContents; - uint256[] internal numOfRewardUnits; - uint256[] internal additionalContentsRewardUnits; - - function setUp() public virtual override { - super.setUp(); - - pack = PackVRFDirect(payable(getContract("PackVRFDirect"))); - - tokenOwner = getWallet(); - packUri = "ipfs://"; - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 0, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 0, - totalAmount: 100 - }) - ); - numOfRewardUnits.push(20); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - numOfRewardUnits.push(50); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 1, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 2, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - numOfRewardUnits.push(100); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 3, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 4, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: 5, - totalAmount: 1 - }) - ); - numOfRewardUnits.push(1); - - packContents.push( - ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 1, - totalAmount: 500 - }) - ); - numOfRewardUnits.push(50); - - erc20.mint(address(tokenOwner), 2000 ether); - erc721.mint(address(tokenOwner), 6); - erc1155.mint(address(tokenOwner), 0, 100); - erc1155.mint(address(tokenOwner), 1, 500); - - // additional contents, to check `addPackContents` - additionalContents.push( - ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: 2, - totalAmount: 200 - }) - ); - additionalContentsRewardUnits.push(50); - - additionalContents.push( - ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1000 ether - }) - ); - additionalContentsRewardUnits.push(100); - - tokenOwner.setAllowanceERC20(address(erc20), address(pack), type(uint256).max); - tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), true); - tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), true); - - vm.prank(deployer); - pack.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); - } - - /*/////////////////////////////////////////////////////////////// - Unit tests: `createPack` - //////////////////////////////////////////////////////////////*/ - - function test_interface() public pure { - console2.logBytes4(type(IERC20).interfaceId); - console2.logBytes4(type(IERC721).interfaceId); - console2.logBytes4(type(IERC1155).interfaceId); - } - - function test_supportsInterface() public { - assertEq(pack.supportsInterface(type(IERC2981Upgradeable).interfaceId), true); - assertEq(pack.supportsInterface(type(IERC721Receiver).interfaceId), true); - assertEq(pack.supportsInterface(type(IERC1155Receiver).interfaceId), true); - assertEq(pack.supportsInterface(type(IERC1155Upgradeable).interfaceId), true); - } - - /** - * note: Testing state changes; token owner calls `createPack` to pack owned tokens. - */ - function test_state_createPack() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - assertEq(packId + 1, pack.nextTokenIdToMint()); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, packContents.length); - for (uint256 i = 0; i < packed.length; i += 1) { - assertEq(packed[i].assetContract, packContents[i].assetContract); - assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); - assertEq(packed[i].tokenId, packContents[i].tokenId); - assertEq(packed[i].totalAmount, packContents[i].totalAmount); - } - - assertEq(packUri, pack.uri(packId)); - } - - /* - * note: Testing state changes; token owner calls `createPack` to pack native tokens. - */ - function test_state_createPack_nativeTokens() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - vm.deal(address(tokenOwner), 100 ether); - packContents.push( - ITokenBundle.Token({ - assetContract: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 20 ether - }) - ); - numOfRewardUnits.push(20); - - vm.prank(address(tokenOwner)); - pack.createPack{ value: 20 ether }(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - assertEq(packId + 1, pack.nextTokenIdToMint()); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, packContents.length); - for (uint256 i = 0; i < packed.length; i += 1) { - assertEq(packed[i].assetContract, packContents[i].assetContract); - assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); - assertEq(packed[i].tokenId, packContents[i].tokenId); - assertEq(packed[i].totalAmount, packContents[i].totalAmount); - } - - assertEq(packUri, pack.uri(packId)); - } - - /** - * note: Testing event emission; token owner calls `createPack` to pack owned tokens. - */ - function test_event_createPack_PackCreated() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectEmit(true, true, true, true); - emit PackCreated(packId, recipient, 226); - - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - vm.stopPrank(); - } - - /** - * note: Testing token balances; token owner calls `createPack` to pack owned tokens. - */ - function test_balances_createPack() public { - // ERC20 balance - assertEq(erc20.balanceOf(address(tokenOwner)), 2000 ether); - assertEq(erc20.balanceOf(address(pack)), 0); - - // ERC721 balance - assertEq(erc721.ownerOf(0), address(tokenOwner)); - assertEq(erc721.ownerOf(1), address(tokenOwner)); - assertEq(erc721.ownerOf(2), address(tokenOwner)); - assertEq(erc721.ownerOf(3), address(tokenOwner)); - assertEq(erc721.ownerOf(4), address(tokenOwner)); - assertEq(erc721.ownerOf(5), address(tokenOwner)); - - // ERC1155 balance - assertEq(erc1155.balanceOf(address(tokenOwner), 0), 100); - assertEq(erc1155.balanceOf(address(pack), 0), 0); - - assertEq(erc1155.balanceOf(address(tokenOwner), 1), 500); - assertEq(erc1155.balanceOf(address(pack), 1), 0); - - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(1); - - vm.prank(address(tokenOwner)); - (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - // ERC20 balance - assertEq(erc20.balanceOf(address(tokenOwner)), 0); - assertEq(erc20.balanceOf(address(pack)), 2000 ether); - - // ERC721 balance - assertEq(erc721.ownerOf(0), address(pack)); - assertEq(erc721.ownerOf(1), address(pack)); - assertEq(erc721.ownerOf(2), address(pack)); - assertEq(erc721.ownerOf(3), address(pack)); - assertEq(erc721.ownerOf(4), address(pack)); - assertEq(erc721.ownerOf(5), address(pack)); - - // ERC1155 balance - assertEq(erc1155.balanceOf(address(tokenOwner), 0), 0); - assertEq(erc1155.balanceOf(address(pack), 0), 100); - - assertEq(erc1155.balanceOf(address(tokenOwner), 1), 0); - assertEq(erc1155.balanceOf(address(pack), 1), 500); - - // Pack wrapped token balance - assertEq(pack.balanceOf(address(recipient), packId), totalSupply); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack owned tokens, without MINTER_ROLE. - */ - function test_revert_createPack_access_MINTER_ROLE() public { - vm.prank(address(tokenOwner)); - pack.renounceRole(keccak256("MINTER_ROLE"), address(tokenOwner)); - - address recipient = address(0x123); - - vm.prank(address(tokenOwner)); - vm.expectRevert( - abi.encodeWithSelector( - Permissions.PermissionsUnauthorizedAccount.selector, - address(tokenOwner), - keccak256("MINTER_ROLE") - ) - ); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` with insufficient value when packing native tokens. - */ - function test_revert_createPack_nativeTokens_insufficientValue() public { - address recipient = address(0x123); - - vm.deal(address(tokenOwner), 100 ether); - - packContents.push( - ITokenBundle.Token({ - assetContract: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 20 ether - }) - ); - numOfRewardUnits.push(1); - - vm.prank(address(tokenOwner)); - vm.expectRevert( - abi.encodeWithSelector(CurrencyTransferLib.CurrencyTransferLibMismatchedValue.selector, 0, 20 ether) - ); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC20 tokens. - */ - function test_revert_createPack_notOwner_ERC20() public { - tokenOwner.transferERC20(address(erc20), address(0x12), 1000 ether); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC20: transfer amount exceeds balance"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC721 tokens. - */ - function test_revert_createPack_notOwner_ERC721() public { - tokenOwner.transferERC721(address(erc721), address(0x12), 0); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC721: caller is not token owner or approved"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC1155 tokens. - */ - function test_revert_createPack_notOwner_ERC1155() public { - tokenOwner.transferERC1155(address(erc1155), address(0x12), 0, 100, ""); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC1155: insufficient balance for transfer"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC20 tokens. - */ - function test_revert_createPack_notApprovedTransfer_ERC20() public { - tokenOwner.setAllowanceERC20(address(erc20), address(pack), 0); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC20: insufficient allowance"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC721 tokens. - */ - function test_revert_createPack_notApprovedTransfer_ERC721() public { - tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), false); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC721: caller is not token owner or approved"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC1155 tokens. - */ - function test_revert_createPack_notApprovedTransfer_ERC1155() public { - tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), false); - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ERC1155: caller is not token owner or approved"); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` with invalid token-type. - */ - function test_revert_createPack_invalidTokenType() public { - ITokenBundle.Token[] memory invalidContent = new ITokenBundle.Token[](1); - uint256[] memory rewardUnits = new uint256[](1); - - invalidContent[0] = ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 1 - }); - rewardUnits[0] = 1; - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("!TokenType"); - pack.createPack(invalidContent, rewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` with total-amount as 0. - */ - function test_revert_createPack_zeroTotalAmount() public { - ITokenBundle.Token[] memory invalidContent = new ITokenBundle.Token[](1); - uint256[] memory rewardUnits = new uint256[](1); - - invalidContent[0] = ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 0 - }); - rewardUnits[0] = 10; - - address recipient = address(0x123); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("0 amt"); - pack.createPack(invalidContent, rewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` with no tokens to pack. - */ - function test_revert_createPack_noTokensToPack() public { - ITokenBundle.Token[] memory emptyContent; - uint256[] memory rewardUnits; - - address recipient = address(0x123); - - bytes memory err = "!Len"; - vm.startPrank(address(tokenOwner)); - vm.expectRevert(err); - pack.createPack(emptyContent, rewardUnits, packUri, 0, 1, recipient); - } - - /** - * note: Testing revert condition; token owner calls `createPack` with unequal length of contents and rewardUnits. - */ - function test_revert_createPack_invalidRewardUnits() public { - uint256[] memory rewardUnits; - - address recipient = address(0x123); - - bytes memory err = "!Len"; - vm.startPrank(address(tokenOwner)); - vm.expectRevert(err); - pack.createPack(packContents, rewardUnits, packUri, 0, 1, recipient); - } - - /*/////////////////////////////////////////////////////////////// - Unit tests: `openPackAndClaimRewards` - //////////////////////////////////////////////////////////////*/ - - /** - * note: Testing state changes; pack owner calls `openPack` to redeem underlying rewards. - */ - function test_state_openPackAndClaimRewards() public { - vm.warp(1000); - uint256 packId = pack.nextTokenIdToMint(); - uint256 packsToOpen = 3; - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); - - vm.prank(recipient, recipient); - uint256 requestId = pack.openPackAndClaimRewards(packId, packsToOpen, 2_500_000); - console2.log("request ID for opening pack:", requestId); - - uint256[] memory randomValues = new uint256[](1); - randomValues[0] = 12345678; - - ITokenBundle.Token[] memory emptyRewardUnitsForTestingEvent; - - vm.expectEmit(true, true, false, false); - emit PackOpened(packId, recipient, 1, emptyRewardUnitsForTestingEvent); - - vm.prank(vrfV2Wrapper); - pack.rawFulfillRandomWords(requestId, randomValues); - - assertFalse(pack.canClaimRewards(recipient)); - } - - /** - * note: Testing state changes; pack owner calls `openPack` to redeem underlying rewards. - */ - function test_state_openPackAndClaimRewards_lowGasFailsafe() public { - vm.warp(1000); - uint256 packId = pack.nextTokenIdToMint(); - uint256 packsToOpen = 3; - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); - - vm.prank(recipient, recipient); - uint256 requestId = pack.openPackAndClaimRewards(packId, packsToOpen, 2); - console2.log("request ID for opening pack:", requestId); - - uint256[] memory randomValues = new uint256[](1); - randomValues[0] = 12345678; - - // check state before - assertFalse(pack.canClaimRewards(recipient)); - console.log(pack.canClaimRewards(recipient)); - - // mock the call with low gas, causing revert in _claimRewards - vm.prank(vrfV2Wrapper); - pack.rawFulfillRandomWords{ gas: 100_000 }(requestId, randomValues); - - // check state after - assertTrue(pack.canClaimRewards(recipient)); - console.log(pack.canClaimRewards(recipient)); - } - - /** - * note: Cannot open pack again while a previous openPack request is in flight. - */ - function test_revert_openPackAndClaimRewards_ReqInFlight() public { - vm.warp(1000); - uint256 packId = pack.nextTokenIdToMint(); - uint256 packsToOpen = 3; - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); - - vm.prank(recipient, recipient); - uint256 requestId = pack.openPackAndClaimRewards(packId, packsToOpen, 2_500_000); - console2.log("request ID for opening pack:", requestId); - - vm.expectRevert("ReqInFlight"); - - vm.prank(recipient, recipient); - pack.openPackAndClaimRewards(packId, packsToOpen, 2_500_000); - - vm.expectRevert("ReqInFlight"); - - vm.prank(recipient, recipient); - pack.openPack(packId, packsToOpen); - } - - /*/////////////////////////////////////////////////////////////// - Unit tests: `openPack` - //////////////////////////////////////////////////////////////*/ - - /** - * note: Testing state changes; pack owner calls `openPack` to redeem underlying rewards. - */ - function test_state_openPack() public { - vm.warp(1000); - uint256 packId = pack.nextTokenIdToMint(); - uint256 packsToOpen = 3; - address recipient = address(1); - - vm.prank(address(tokenOwner)); - (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); - - vm.prank(recipient, recipient); - uint256 requestId = pack.openPack(packId, packsToOpen); - console2.log("request ID for opening pack:", requestId); - - uint256[] memory randomValues = new uint256[](1); - randomValues[0] = 12345678; - - vm.prank(vrfV2Wrapper); - pack.rawFulfillRandomWords(requestId, randomValues); - - vm.prank(recipient, recipient); - ITokenBundle.Token[] memory rewardUnits = pack.claimRewards(); - console2.log("total reward units: ", rewardUnits.length); - - for (uint256 i = 0; i < rewardUnits.length; i++) { - console2.log("----- reward unit number: ", i, "------"); - console2.log("asset contract: ", rewardUnits[i].assetContract); - console2.log("token type: ", uint256(rewardUnits[i].tokenType)); - console2.log("tokenId: ", rewardUnits[i].tokenId); - if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { - console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); - } else { - console2.log("total amount: ", rewardUnits[i].totalAmount); - } - console2.log(""); - } - - assertEq(packUri, pack.uri(packId)); - assertEq(pack.totalSupply(packId), totalSupply - packsToOpen); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, packContents.length); - } - - /** - * note: Testing event emission; pack owner calls `openPack` to open owned packs. - */ - function test_event_openPack() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - vm.expectEmit(true, true, false, false); - emit PackOpenRequested(recipient, packId, 1, VRFV2Wrapper(vrfV2Wrapper).lastRequestId()); - - vm.prank(recipient, recipient); - uint256 requestId = pack.openPack(packId, 1); - - vm.expectEmit(true, true, false, true); - emit PackRandomnessFulfilled(packId, requestId); - - uint256[] memory randomValues = new uint256[](1); - randomValues[0] = 12345678; - - vm.prank(vrfV2Wrapper); - pack.rawFulfillRandomWords(requestId, randomValues); - - ITokenBundle.Token[] memory emptyRewardUnitsForTestingEvent; - - vm.expectEmit(true, true, false, false); - emit PackOpened(packId, recipient, 1, emptyRewardUnitsForTestingEvent); - - vm.prank(recipient, recipient); - pack.claimRewards(); - } - - function test_balances_openPack() public { - uint256 packId = pack.nextTokenIdToMint(); - uint256 packsToOpen = 3; - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); - - // ERC20 balance - assertEq(erc20.balanceOf(address(recipient)), 0); - assertEq(erc20.balanceOf(address(pack)), 2000 ether); - - // ERC721 balance - assertEq(erc721.ownerOf(0), address(pack)); - assertEq(erc721.ownerOf(1), address(pack)); - assertEq(erc721.ownerOf(2), address(pack)); - assertEq(erc721.ownerOf(3), address(pack)); - assertEq(erc721.ownerOf(4), address(pack)); - assertEq(erc721.ownerOf(5), address(pack)); - - // ERC1155 balance - assertEq(erc1155.balanceOf(address(recipient), 0), 0); - assertEq(erc1155.balanceOf(address(pack), 0), 100); - - assertEq(erc1155.balanceOf(address(recipient), 1), 0); - assertEq(erc1155.balanceOf(address(pack), 1), 500); - - vm.prank(recipient, recipient); - uint256 requestId = pack.openPack(packId, packsToOpen); - console2.log("request ID for opening pack:", requestId); - - uint256[] memory randomValues = new uint256[](1); - randomValues[0] = 12345678; - - vm.prank(vrfV2Wrapper); - pack.rawFulfillRandomWords(requestId, randomValues); - - vm.prank(recipient, recipient); - ITokenBundle.Token[] memory rewardUnits = pack.claimRewards(); - console2.log("total reward units: ", rewardUnits.length); - - uint256 erc20Amount; - uint256[] memory erc1155Amounts = new uint256[](2); - uint256 erc721Amount; - - for (uint256 i = 0; i < rewardUnits.length; i++) { - console2.log("----- reward unit number: ", i, "------"); - console2.log("asset contract: ", rewardUnits[i].assetContract); - console2.log("token type: ", uint256(rewardUnits[i].tokenType)); - console2.log("tokenId: ", rewardUnits[i].tokenId); - if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { - console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); - console.log("balance of recipient: ", erc20.balanceOf(address(recipient)) / 1 ether, "ether"); - erc20Amount += rewardUnits[i].totalAmount; - } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC1155) { - console2.log("total amount: ", rewardUnits[i].totalAmount); - console.log("balance of recipient: ", erc1155.balanceOf(address(recipient), rewardUnits[i].tokenId)); - erc1155Amounts[rewardUnits[i].tokenId] += rewardUnits[i].totalAmount; - } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC721) { - console2.log("total amount: ", rewardUnits[i].totalAmount); - console.log("balance of recipient: ", erc721.balanceOf(address(recipient))); - erc721Amount += rewardUnits[i].totalAmount; - } - console2.log(""); - } - - assertEq(erc20.balanceOf(address(recipient)), erc20Amount); - assertEq(erc721.balanceOf(address(recipient)), erc721Amount); - - for (uint256 i = 0; i < erc1155Amounts.length; i += 1) { - assertEq(erc1155.balanceOf(address(recipient), i), erc1155Amounts[i]); - } - } - - /** - * note: Testing revert condition; caller of `openPack` is not EOA. - */ - function test_revert_openPack_notEOA() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - vm.startPrank(recipient, address(27)); - string memory err = "!EOA"; - vm.expectRevert(bytes(err)); - pack.openPack(packId, 1); - } - - /** - * note: Cannot open pack again while a previous openPack request is in flight. - */ - function test_revert_openPack_ReqInFlight() public { - vm.warp(1000); - uint256 packId = pack.nextTokenIdToMint(); - uint256 packsToOpen = 3; - address recipient = address(1); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); - - vm.prank(recipient, recipient); - uint256 requestId = pack.openPack(packId, packsToOpen); - console2.log("request ID for opening pack:", requestId); - - vm.expectRevert("ReqInFlight"); - - vm.prank(recipient, recipient); - pack.openPack(packId, packsToOpen); - - vm.expectRevert("ReqInFlight"); - - vm.prank(recipient, recipient); - pack.openPackAndClaimRewards(packId, packsToOpen, 2_500_000); - } - - /** - * note: Testing revert condition; pack owner calls `openPack` to open more than owned packs. - */ - function test_revert_openPack_openMoreThanOwned() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - - vm.prank(address(tokenOwner)); - (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - bytes memory err = "!Bal"; - vm.startPrank(recipient, recipient); - vm.expectRevert(err); - pack.openPack(packId, totalSupply + 1); - } - - /** - * note: Testing revert condition; pack owner calls `openPack` before start timestamp. - */ - function test_revert_openPack_openBeforeStart() public { - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 1000, 1, recipient); - - vm.startPrank(recipient, recipient); - vm.expectRevert("!Open"); - pack.openPack(packId, 1); - } - - /** - * note: Testing revert condition; pack owner calls `openPack` with pack-id non-existent or not owned. - */ - function test_revert_openPack_invalidPackId() public { - address recipient = address(0x123); - - vm.prank(address(tokenOwner)); - pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); - - bytes memory err = "!Bal"; - vm.startPrank(recipient, recipient); - vm.expectRevert(err); - pack.openPack(2, 1); - } - - /*/////////////////////////////////////////////////////////////// - Fuzz testing - //////////////////////////////////////////////////////////////*/ - - uint256 internal constant MAX_TOKENS = 2000; - - function getTokensToPack( - uint256 len - ) internal returns (ITokenBundle.Token[] memory tokensToPack, uint256[] memory rewardUnits) { - vm.assume(len < MAX_TOKENS); - tokensToPack = new ITokenBundle.Token[](len); - rewardUnits = new uint256[](len); - - for (uint256 i = 0; i < len; i += 1) { - uint256 random = uint256(keccak256(abi.encodePacked(len + i))) % MAX_TOKENS; - uint256 selector = random % 4; - - if (selector == 0) { - tokensToPack[i] = ITokenBundle.Token({ - assetContract: address(erc20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: (random + 1) * 10 ether - }); - rewardUnits[i] = random + 1; - - erc20.mint(address(tokenOwner), tokensToPack[i].totalAmount); - } else if (selector == 1) { - uint256 tokenId = erc721.nextTokenIdToMint(); - - tokensToPack[i] = ITokenBundle.Token({ - assetContract: address(erc721), - tokenType: ITokenBundle.TokenType.ERC721, - tokenId: tokenId, - totalAmount: 1 - }); - rewardUnits[i] = 1; - - erc721.mint(address(tokenOwner), 1); - } else if (selector == 2) { - tokensToPack[i] = ITokenBundle.Token({ - assetContract: address(erc1155), - tokenType: ITokenBundle.TokenType.ERC1155, - tokenId: random, - totalAmount: (random + 1) * 10 - }); - rewardUnits[i] = random + 1; - - erc1155.mint(address(tokenOwner), tokensToPack[i].tokenId, tokensToPack[i].totalAmount); - } else if (selector == 3) { - tokensToPack[i] = ITokenBundle.Token({ - assetContract: address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 5 ether - }); - rewardUnits[i] = 5; - } - } - } - - function checkBalances( - ITokenBundle.Token[] memory rewardUnits, - address - ) - internal - pure - returns (uint256 nativeTokenAmount, uint256 erc20Amount, uint256[] memory erc1155Amounts, uint256 erc721Amount) - { - erc1155Amounts = new uint256[](MAX_TOKENS); - - for (uint256 i = 0; i < rewardUnits.length; i++) { - // console2.log("----- reward unit number: ", i, "------"); - // console2.log("asset contract: ", rewardUnits[i].assetContract); - // console2.log("token type: ", uint256(rewardUnits[i].tokenType)); - // console2.log("tokenId: ", rewardUnits[i].tokenId); - if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { - if (rewardUnits[i].assetContract == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { - // console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); - // console.log("balance of recipient: ", address(recipient).balance); - nativeTokenAmount += rewardUnits[i].totalAmount; - } else { - // console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); - // console.log("balance of recipient: ", erc20.balanceOf(address(recipient)) / 1 ether, "ether"); - erc20Amount += rewardUnits[i].totalAmount; - } - } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC1155) { - // console2.log("total amount: ", rewardUnits[i].totalAmount); - // console.log("balance of recipient: ", erc1155.balanceOf(address(recipient), rewardUnits[i].tokenId)); - erc1155Amounts[rewardUnits[i].tokenId] += rewardUnits[i].totalAmount; - } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC721) { - // console2.log("total amount: ", rewardUnits[i].totalAmount); - // console.log("balance of recipient: ", erc721.balanceOf(address(recipient))); - erc721Amount += rewardUnits[i].totalAmount; - } - // console2.log(""); - } - } - - function test_fuzz_state_createPack(uint256 x, uint128 y) public { - (ITokenBundle.Token[] memory tokensToPack, uint256[] memory rewardUnits) = getTokensToPack(x); - if (tokensToPack.length == 0) { - return; - } - - uint256 packId = pack.nextTokenIdToMint(); - address recipient = address(0x123); - uint256 totalRewardUnits; - uint256 nativeTokenPacked; - - for (uint256 i = 0; i < tokensToPack.length; i += 1) { - totalRewardUnits += rewardUnits[i]; - if (tokensToPack[i].assetContract == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { - nativeTokenPacked += tokensToPack[i].totalAmount; - } - } - vm.deal(address(tokenOwner), nativeTokenPacked); - vm.assume(y > 0 && totalRewardUnits % y == 0); - - vm.prank(address(tokenOwner)); - (, uint256 totalSupply) = pack.createPack{ value: nativeTokenPacked }( - tokensToPack, - rewardUnits, - packUri, - 0, - y, - recipient - ); - console2.log("total supply: ", totalSupply); - console2.log("total reward units: ", totalRewardUnits); - - assertEq(packId + 1, pack.nextTokenIdToMint()); - - (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); - assertEq(packed.length, tokensToPack.length); - for (uint256 i = 0; i < packed.length; i += 1) { - assertEq(packed[i].assetContract, tokensToPack[i].assetContract); - assertEq(uint256(packed[i].tokenType), uint256(tokensToPack[i].tokenType)); - assertEq(packed[i].tokenId, tokensToPack[i].tokenId); - assertEq(packed[i].totalAmount, tokensToPack[i].totalAmount); - } - - assertEq(packUri, pack.uri(packId)); - } - - /*/////////////////////////////////////////////////////////////// - Scenario/Exploit tests - //////////////////////////////////////////////////////////////*/ - /** - * note: Testing revert condition; token owner calls `createPack` to pack owned tokens. - */ - function test_revert_createPack_reentrancy() public { - MaliciousERC20 malERC20 = new MaliciousERC20(payable(address(pack))); - ITokenBundle.Token[] memory content = new ITokenBundle.Token[](1); - uint256[] memory rewards = new uint256[](1); - - malERC20.mint(address(tokenOwner), 10 ether); - content[0] = ITokenBundle.Token({ - assetContract: address(malERC20), - tokenType: ITokenBundle.TokenType.ERC20, - tokenId: 0, - totalAmount: 10 ether - }); - rewards[0] = 10; - - tokenOwner.setAllowanceERC20(address(malERC20), address(pack), 10 ether); - - address recipient = address(0x123); - - vm.prank(address(deployer)); - pack.grantRole(keccak256("MINTER_ROLE"), address(malERC20)); - - vm.startPrank(address(tokenOwner)); - vm.expectRevert("ReentrancyGuard: reentrant call"); - pack.createPack(content, rewards, packUri, 0, 1, recipient); - } -} - -contract MaliciousERC20 is MockERC20, ITokenBundle { - Pack public pack; - - constructor(address payable _pack) { - pack = Pack(_pack); - } - - function transferFrom(address from, address to, uint256 amount) public override returns (bool) { - ITokenBundle.Token[] memory content = new ITokenBundle.Token[](1); - uint256[] memory rewards = new uint256[](1); - - address recipient = address(0x123); - pack.createPack(content, rewards, "", 0, 1, recipient); - return super.transferFrom(from, to, amount); - } -} diff --git a/src/test/utils/BaseTest.sol b/src/test/utils/BaseTest.sol index a26ff27ad..2f40341fc 100644 --- a/src/test/utils/BaseTest.sol +++ b/src/test/utils/BaseTest.sol @@ -13,12 +13,9 @@ import { MockERC1155, IERC1155 } from "../mocks/MockERC1155.sol"; import { MockERC721NonBurnable } from "../mocks/MockERC721NonBurnable.sol"; import { MockERC1155NonBurnable } from "../mocks/MockERC1155NonBurnable.sol"; import { Forwarder } from "contracts/infra/forwarder/Forwarder.sol"; -import { ForwarderEOAOnly } from "contracts/infra/forwarder/ForwarderEOAOnly.sol"; import { TWRegistry } from "contracts/infra/TWRegistry.sol"; import { TWFactory } from "contracts/infra/TWFactory.sol"; import { Multiwrap } from "contracts/prebuilts/multiwrap/Multiwrap.sol"; -import { Pack } from "contracts/prebuilts/pack/Pack.sol"; -import { PackVRFDirect } from "contracts/prebuilts/pack/PackVRFDirect.sol"; import { Split } from "contracts/prebuilts/split/Split.sol"; import { DropERC20 } from "contracts/prebuilts/drop/DropERC20.sol"; import { DropERC721 } from "contracts/prebuilts/drop/DropERC721.sol"; @@ -28,7 +25,6 @@ import { TokenERC721 } from "contracts/prebuilts/token/TokenERC721.sol"; import { TokenERC1155 } from "contracts/prebuilts/token/TokenERC1155.sol"; import { Marketplace } from "contracts/prebuilts/marketplace-legacy/Marketplace.sol"; import { VoteERC20 } from "contracts/prebuilts/vote/VoteERC20.sol"; -import { SignatureDrop } from "contracts/prebuilts/signature-drop/SignatureDrop.sol"; import { ContractPublisher } from "contracts/infra/ContractPublisher.sol"; import { IContractPublisher } from "contracts/infra/interface/IContractPublisher.sol"; import { AirdropERC721 } from "contracts/prebuilts/unaudited/airdrop/AirdropERC721.sol"; @@ -65,7 +61,6 @@ abstract contract BaseTest is DSTest, Test { WETH9 public weth; address public forwarder; - address public eoaForwarder; address public registry; address public factory; address public fee; @@ -115,7 +110,6 @@ abstract contract BaseTest is DSTest, Test { erc1155NonBurnable = new MockERC1155NonBurnable(); weth = new WETH9(); forwarder = address(new Forwarder()); - eoaForwarder = address(new ForwarderEOAOnly()); registry = address(new TWRegistry(forwarders())); factory = address(new TWFactory(forwarders(), registry)); contractPublisher = address(new ContractPublisher(factoryAdmin, forwarders(), new MockContractPublisher())); @@ -132,23 +126,16 @@ abstract contract BaseTest is DSTest, Test { TWFactory(factory).addImplementation(address(new DropERC721())); TWFactory(factory).addImplementation(address(new MockContract(bytes32("DropERC1155"), 1))); TWFactory(factory).addImplementation(address(new DropERC1155())); - TWFactory(factory).addImplementation(address(new MockContract(bytes32("SignatureDrop"), 1))); - TWFactory(factory).addImplementation(address(new SignatureDrop())); TWFactory(factory).addImplementation(address(new MockContract(bytes32("Marketplace"), 1))); TWFactory(factory).addImplementation(address(new Marketplace(address(weth)))); TWFactory(factory).addImplementation(address(new Split())); TWFactory(factory).addImplementation(address(new Multiwrap(address(weth)))); - TWFactory(factory).addImplementation(address(new MockContract(bytes32("Pack"), 1))); TWFactory(factory).addImplementation(address(new MockContract(bytes32("AirdropERC721"), 1))); TWFactory(factory).addImplementation(address(new AirdropERC721())); TWFactory(factory).addImplementation(address(new MockContract(bytes32("AirdropERC20"), 1))); TWFactory(factory).addImplementation(address(new AirdropERC20())); TWFactory(factory).addImplementation(address(new MockContract(bytes32("AirdropERC1155"), 1))); TWFactory(factory).addImplementation(address(new AirdropERC1155())); - TWFactory(factory).addImplementation( - address(new PackVRFDirect(address(weth), eoaForwarder, linkToken, vrfV2Wrapper)) - ); - TWFactory(factory).addImplementation(address(new Pack(address(weth)))); TWFactory(factory).addImplementation(address(new VoteERC20())); TWFactory(factory).addImplementation(address(new MockContract(bytes32("NFTStake"), 1))); TWFactory(factory).addImplementation(address(new NFTStake(address(weth)))); @@ -257,24 +244,6 @@ abstract contract BaseTest is DSTest, Test { ) ) ); - deployContractProxy( - "SignatureDrop", - abi.encodeCall( - SignatureDrop.initialize, - ( - signer, - NAME, - SYMBOL, - CONTRACT_URI, - forwarders(), - saleRecipient, - royaltyRecipient, - royaltyBps, - platformFeeBps, - platformFeeRecipient - ) - ) - ); deployContractProxy( "Marketplace", abi.encodeCall( @@ -289,21 +258,6 @@ abstract contract BaseTest is DSTest, Test { (deployer, NAME, SYMBOL, CONTRACT_URI, forwarders(), royaltyRecipient, royaltyBps) ) ); - deployContractProxy( - "Pack", - abi.encodeCall( - Pack.initialize, - (deployer, NAME, SYMBOL, CONTRACT_URI, forwarders(), royaltyRecipient, royaltyBps) - ) - ); - - deployContractProxy( - "PackVRFDirect", - abi.encodeCall( - PackVRFDirect.initialize, - (deployer, NAME, SYMBOL, CONTRACT_URI, forwarders(), royaltyRecipient, royaltyBps) - ) - ); deployContractProxy( "AirdropERC721", diff --git a/src/test/utils/ChainlinkVRF.sol b/src/test/utils/ChainlinkVRF.sol deleted file mode 100644 index cd5eaf76a..000000000 --- a/src/test/utils/ChainlinkVRF.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: Apache 2.0 -pragma solidity ^0.8.0; - -contract Link { - function transferAndCall(address, uint256, bytes calldata) external returns (bool) {} -} - -contract VRFV2Wrapper { - uint256 private nextId = 5; - - function lastRequestId() external view returns (uint256 id) { - id = nextId; - } - - function calculateRequestPrice(uint32 _callbackGasLimit) external pure returns (uint256) { - return _callbackGasLimit; - } -}