diff --git a/contracts/feature/meta-tx/Drop.sol b/contracts/feature/Drop.sol similarity index 89% rename from contracts/feature/meta-tx/Drop.sol rename to contracts/feature/Drop.sol index 7073372bf..7194ff439 100644 --- a/contracts/feature/meta-tx/Drop.sol +++ b/contracts/feature/Drop.sol @@ -1,13 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import "../interface/IDrop.sol"; -import "../../lib/MerkleProof.sol"; -import "./ExecutionContext.sol"; -import "@openzeppelin/contracts-upgradeable/utils/structs/BitMapsUpgradeable.sol"; +import "./interface/IDrop.sol"; +import "../lib/MerkleProof.sol"; +import "../lib/TWBitMaps.sol"; -abstract contract Drop is IDrop, ExecutionContext { - using BitMapsUpgradeable for BitMapsUpgradeable.BitMap; +abstract contract Drop is IDrop { + using TWBitMaps for TWBitMaps.BitMap; /*/////////////////////////////////////////////////////////////// State variables @@ -44,7 +43,7 @@ abstract contract Drop is IDrop, ExecutionContext { // Verify inclusion in allowlist. (bool validMerkleProof, uint256 merkleProofIndex) = verifyClaimMerkleProof( activeConditionId, - _msgSender(), + _dropMsgSender(), _quantity, _allowlistProof ); @@ -54,7 +53,7 @@ abstract contract Drop is IDrop, ExecutionContext { verifyClaim( activeConditionId, - _msgSender(), + _dropMsgSender(), _quantity, _currency, _pricePerToken, @@ -74,7 +73,7 @@ abstract contract Drop is IDrop, ExecutionContext { // claimCondition.supplyClaimed += _quantity; // lastClaimTimestamp[activeConditionId][_msgSender()] = block.timestamp; claimCondition.conditions[activeConditionId].supplyClaimed += _quantity; - claimCondition.lastClaimTimestamp[activeConditionId][_msgSender()] = block.timestamp; + claimCondition.lastClaimTimestamp[activeConditionId][_dropMsgSender()] = block.timestamp; // If there's a price, collect price. collectPriceOnClaim(_quantity, _currency, _pricePerToken); @@ -82,17 +81,19 @@ abstract contract Drop is IDrop, ExecutionContext { // Mint the relevant NFTs to claimer. uint256 startTokenId = transferTokensOnClaim(_receiver, _quantity); - emit TokensClaimed(activeConditionId, _msgSender(), _receiver, startTokenId, _quantity); + emit TokensClaimed(activeConditionId, _dropMsgSender(), _receiver, startTokenId, _quantity); _afterClaim(_receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); } /// @dev Lets a contract admin set claim conditions. - function setClaimConditions( - ClaimCondition[] calldata _conditions, - bool _resetClaimEligibility, - bytes memory - ) external virtual override { + function setClaimConditions(ClaimCondition[] calldata _conditions, bool _resetClaimEligibility) + external + virtual + override + { + require(_canSetClaimConditions(), "Not authorized"); + uint256 existingStartIndex = claimCondition.currentStartId; uint256 existingPhaseCount = claimCondition.count; @@ -179,13 +180,6 @@ abstract contract Drop is IDrop, ExecutionContext { "exceed max claimable supply." ); - // uint256 timestampOfLastClaim = lastClaimTimestamp[conditionId][_claimer]; - // uint256 timestampOfLastClaim = claimCondition.lastClaimTimestamp[_conditionId][_claimer]; - // require( - // timestampOfLastClaim == 0 || - // block.timestamp >= timestampOfLastClaim + currentClaimPhase.waitTimeInSecondsBetweenClaims, - // "cannot claim." - // ); (uint256 lastClaimTimestamp, uint256 nextValidClaimTimestamp) = getClaimTimestamp(_conditionId, _claimer); require(lastClaimTimestamp == 0 || block.timestamp >= nextValidClaimTimestamp, "cannot claim."); } @@ -206,7 +200,6 @@ abstract contract Drop is IDrop, ExecutionContext { keccak256(abi.encodePacked(_claimer, _allowlistProof.maxQuantityInAllowlist)) ); require(validMerkleProof, "not in whitelist."); - // require(!usedAllowlistSpot[conditionId].get(merkleProofIndex), "proof claimed."); require(!claimCondition.usedAllowlistSpot[_conditionId].get(merkleProofIndex), "proof claimed."); require( _allowlistProof.maxQuantityInAllowlist == 0 || _quantity <= _allowlistProof.maxQuantityInAllowlist, @@ -250,9 +243,14 @@ abstract contract Drop is IDrop, ExecutionContext { } } - /*/////////////////////////////////////////////////////////////// - Virtual functions: to be implemented in derived contract - //////////////////////////////////////////////////////////////*/ + /*//////////////////////////////////////////////////////////////////// + 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( @@ -274,6 +272,10 @@ abstract contract Drop is IDrop, ExecutionContext { bytes memory _data ) internal virtual {} + /*/////////////////////////////////////////////////////////////// + Virtual functions: to be implemented in derived contract + //////////////////////////////////////////////////////////////*/ + /// @dev Collects and distributes the primary sale value of NFTs being claimed. function collectPriceOnClaim( uint256 _quantityToClaim, @@ -286,4 +288,7 @@ abstract contract Drop is IDrop, ExecutionContext { internal virtual returns (uint256 startTokenId); + + /// @dev Determine what wallet can update claim conditions + function _canSetClaimConditions() internal virtual returns (bool); } diff --git a/contracts/feature/DropSinglePhase.sol b/contracts/feature/DropSinglePhase.sol index 8c839d5da..dd38f3871 100644 --- a/contracts/feature/DropSinglePhase.sol +++ b/contracts/feature/DropSinglePhase.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.0; import "./interface/IDropSinglePhase.sol"; import "../lib/MerkleProof.sol"; -import "@openzeppelin/contracts-upgradeable/utils/structs/BitMapsUpgradeable.sol"; +import "../lib/TWBitMaps.sol"; abstract contract DropSinglePhase is IDropSinglePhase { - using BitMapsUpgradeable for BitMapsUpgradeable.BitMap; + using TWBitMaps for TWBitMaps.BitMap; /*/////////////////////////////////////////////////////////////// State variables @@ -32,7 +32,7 @@ abstract contract DropSinglePhase is IDropSinglePhase { * @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; + mapping(bytes32 => TWBitMaps.BitMap) private usedAllowlistSpot; /*/////////////////////////////////////////////////////////////// Drop logic @@ -60,7 +60,7 @@ abstract contract DropSinglePhase is IDropSinglePhase { // Verify inclusion in allowlist. (bool validMerkleProof, uint256 merkleProofIndex) = verifyClaimMerkleProof( - msg.sender, + _dropMsgSender(), _quantity, _allowlistProof ); @@ -68,7 +68,7 @@ abstract contract DropSinglePhase is IDropSinglePhase { // Verify claim validity. If not valid, revert. bool toVerifyMaxQuantityPerTransaction = _allowlistProof.maxQuantityInAllowlist == 0; - verifyClaim(msg.sender, _quantity, _currency, _pricePerToken, toVerifyMaxQuantityPerTransaction); + verifyClaim(_dropMsgSender(), _quantity, _currency, _pricePerToken, toVerifyMaxQuantityPerTransaction); if (validMerkleProof && _allowlistProof.maxQuantityInAllowlist > 0) { /** @@ -80,7 +80,7 @@ abstract contract DropSinglePhase is IDropSinglePhase { // Update contract state. claimCondition.supplyClaimed += _quantity; - lastClaimTimestamp[activeConditionId][msg.sender] = block.timestamp; + lastClaimTimestamp[activeConditionId][_dropMsgSender()] = block.timestamp; // If there's a price, collect price. collectPriceOnClaim(_quantity, _currency, _pricePerToken); @@ -88,7 +88,7 @@ abstract contract DropSinglePhase is IDropSinglePhase { // Mint the relevant NFTs to claimer. uint256 startTokenId = transferTokensOnClaim(_receiver, _quantity); - emit TokensClaimed(claimCondition, msg.sender, _receiver, _quantity, startTokenId); + emit TokensClaimed(claimCondition, _dropMsgSender(), _receiver, _quantity, startTokenId); _afterClaim(_receiver, _quantity, _currency, _pricePerToken, _allowlistProof, _data); } @@ -104,7 +104,7 @@ abstract contract DropSinglePhase is IDropSinglePhase { if (_resetClaimEligibility) { supplyClaimedAlready = 0; - targetConditionId = keccak256(abi.encodePacked(msg.sender, block.number)); + targetConditionId = keccak256(abi.encodePacked(_dropMsgSender(), block.number)); } require(supplyClaimedAlready <= _condition.maxClaimableSupply, "max supply claimed already"); @@ -181,9 +181,14 @@ abstract contract DropSinglePhase is IDropSinglePhase { } } - /*/////////////////////////////////////////////////////////////// - Virtual functions: to be implemented in derived contract - //////////////////////////////////////////////////////////////*/ + /*//////////////////////////////////////////////////////////////////// + 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( @@ -205,6 +210,10 @@ abstract contract DropSinglePhase is IDropSinglePhase { bytes memory _data ) internal virtual {} + /*/////////////////////////////////////////////////////////////// + Virtual functions: to be implemented in derived contract + //////////////////////////////////////////////////////////////*/ + /// @dev Collects and distributes the primary sale value of NFTs being claimed. function collectPriceOnClaim( uint256 _quantityToClaim, diff --git a/contracts/feature/interface/IClaimCondition.sol b/contracts/feature/interface/IClaimCondition.sol index 49811a794..c4cbd0707 100644 --- a/contracts/feature/interface/IClaimCondition.sol +++ b/contracts/feature/interface/IClaimCondition.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import "@openzeppelin/contracts-upgradeable/utils/structs/BitMapsUpgradeable.sol"; +import "../../lib/TWBitMaps.sol"; /** * Thirdweb's 'Drop' contracts are distribution mechanisms for tokens. @@ -75,6 +75,6 @@ interface IClaimCondition { uint256 count; mapping(uint256 => ClaimCondition) conditions; mapping(uint256 => mapping(address => uint256)) lastClaimTimestamp; - mapping(uint256 => BitMapsUpgradeable.BitMap) usedAllowlistSpot; + mapping(uint256 => TWBitMaps.BitMap) usedAllowlistSpot; } } diff --git a/contracts/feature/interface/IDrop.sol b/contracts/feature/interface/IDrop.sol index 65d903714..63881c459 100644 --- a/contracts/feature/interface/IDrop.sol +++ b/contracts/feature/interface/IDrop.sol @@ -47,11 +47,6 @@ interface IDrop is IClaimCondition { * @param resetClaimEligibility Whether to reset `limitLastClaimTimestamp` and `limitMerkleProofClaim` values when setting new * claim conditions. * - * @param data Arbitrary bytes data that can be leveraged in the implementation of this interface. */ - function setClaimConditions( - ClaimCondition[] calldata phases, - bool resetClaimEligibility, - bytes memory data - ) external; + function setClaimConditions(ClaimCondition[] calldata phases, bool resetClaimEligibility) external; } diff --git a/contracts/feature/interface/ILazyMint.sol b/contracts/feature/interface/ILazyMint.sol index eed33ade5..924268c97 100644 --- a/contracts/feature/interface/ILazyMint.sol +++ b/contracts/feature/interface/ILazyMint.sol @@ -5,6 +5,6 @@ interface ILazyMint { function lazyMint( uint256 amount, string calldata baseURIForTokens, - bytes calldata encryptedBaseURI + bytes calldata extraData ) external returns (uint256 batchId); } diff --git a/contracts/lib/TWBitMaps.sol b/contracts/lib/TWBitMaps.sol new file mode 100644 index 000000000..6c6a71874 --- /dev/null +++ b/contracts/lib/TWBitMaps.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/structs/BitMaps.sol) +pragma solidity ^0.8.0; + +/** + * @dev Library for managing uint256 to bool mapping in a compact and efficient way, providing the keys are sequential. + * Largelly inspired by Uniswap's https://github.com/Uniswap/merkle-distributor/blob/master/contracts/MerkleDistributor.sol[merkle-distributor]. + */ +library TWBitMaps { + struct BitMap { + mapping(uint256 => uint256) _data; + } + + /** + * @dev Returns whether the bit at `index` is set. + */ + function get(BitMap storage bitmap, uint256 index) internal view returns (bool) { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + return bitmap._data[bucket] & mask != 0; + } + + /** + * @dev Sets the bit at `index` to the boolean `value`. + */ + function setTo( + BitMap storage bitmap, + uint256 index, + bool value + ) internal { + if (value) { + set(bitmap, index); + } else { + unset(bitmap, index); + } + } + + /** + * @dev Sets the bit at `index`. + */ + function set(BitMap storage bitmap, uint256 index) internal { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + bitmap._data[bucket] |= mask; + } + + /** + * @dev Unsets the bit at `index`. + */ + function unset(BitMap storage bitmap, uint256 index) internal { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + bitmap._data[bucket] &= ~mask; + } +} diff --git a/contracts/signature-drop/SignatureDrop.sol b/contracts/signature-drop/SignatureDrop.sol index ac7cf9000..6e03fbef8 100644 --- a/contracts/signature-drop/SignatureDrop.sol +++ b/contracts/signature-drop/SignatureDrop.sol @@ -25,7 +25,7 @@ import "../feature/Ownable.sol"; import "../feature/DelayedReveal.sol"; import "../feature/LazyMint.sol"; import "../feature/PermissionsEnumerable.sol"; -import "../feature/meta-tx/Drop.sol"; +import "../feature/Drop.sol"; import "../feature/interface/ISignatureMintERC721.sol"; contract SignatureDrop is @@ -325,6 +325,11 @@ contract SignatureDrop is return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); } + /// @dev Returns whether claim conditions can be set in the given execution context. + function _canSetClaimConditions() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + /*/////////////////////////////////////////////////////////////// Miscellaneous //////////////////////////////////////////////////////////////*/ @@ -357,11 +362,15 @@ contract SignatureDrop is } } + function _dropMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + function _msgSender() internal view virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable, ExecutionContext) + override(ContextUpgradeable, ERC2771ContextUpgradeable) returns (address sender) { return ERC2771ContextUpgradeable._msgSender(); @@ -371,7 +380,7 @@ contract SignatureDrop is internal view virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable, ExecutionContext) + override(ContextUpgradeable, ERC2771ContextUpgradeable) returns (bytes calldata) { return ERC2771ContextUpgradeable._msgData(); diff --git a/foundry.toml b/foundry.toml index 5510ea4b2..eedb49b68 100644 --- a/foundry.toml +++ b/foundry.toml @@ -19,8 +19,8 @@ remappings = [ 'contracts/=contracts/', 'erc721a-upgradeable/=node_modules/erc721a-upgradeable/', ] -src = 'src' -test = 'test' +src = 'contracts' +test = 'src/test' verbosity = 0 #ignored_error_codes = [] #fuzz_runs = 256 diff --git a/src/test/SignatureDrop.t.sol b/src/test/SignatureDrop.t.sol index f5f1d9f42..e289601d2 100644 --- a/src/test/SignatureDrop.t.sol +++ b/src/test/SignatureDrop.t.sol @@ -763,7 +763,7 @@ contract SignatureDropTest is BaseTest { vm.prank(deployerSigner); sigdrop.lazyMint(100, "ipfs://", ""); vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions, false, ""); + sigdrop.setClaimConditions(conditions, false); vm.prank(getActor(5), getActor(5)); sigdrop.claim(receiver, 1, address(0), 0, alp, ""); @@ -793,7 +793,7 @@ contract SignatureDropTest is BaseTest { vm.prank(deployerSigner); sigdrop.lazyMint(100, "ipfs://", ""); vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions, false, ""); + sigdrop.setClaimConditions(conditions, false); vm.expectRevert("not enough minted tokens."); vm.prank(getActor(6), getActor(6)); @@ -820,7 +820,7 @@ contract SignatureDropTest is BaseTest { vm.prank(deployerSigner); sigdrop.lazyMint(200, "ipfs://", ""); vm.prank(deployerSigner); - sigdrop.setClaimConditions(conditions, false, ""); + sigdrop.setClaimConditions(conditions, false); vm.prank(getActor(5), getActor(5)); sigdrop.claim(receiver, 100, address(0), 0, alp, ""); @@ -860,22 +860,22 @@ contract SignatureDropTest is BaseTest { conditions[1].startTimestamp = 1; conditions[1].maxClaimableSupply = 10; - sigdrop.setClaimConditions(conditions, false, ""); + sigdrop.setClaimConditions(conditions, false); (currentStartId, count) = sigdrop.claimCondition(); assertEq(currentStartId, 0); assertEq(count, 2); - sigdrop.setClaimConditions(conditions, false, ""); + sigdrop.setClaimConditions(conditions, false); (currentStartId, count) = sigdrop.claimCondition(); assertEq(currentStartId, 0); assertEq(count, 2); - sigdrop.setClaimConditions(conditions, true, ""); + sigdrop.setClaimConditions(conditions, true); (currentStartId, count) = sigdrop.claimCondition(); assertEq(currentStartId, 2); assertEq(count, 2); - sigdrop.setClaimConditions(conditions, true, ""); + sigdrop.setClaimConditions(conditions, true); (currentStartId, count) = sigdrop.claimCondition(); assertEq(currentStartId, 4); assertEq(count, 2); @@ -902,7 +902,7 @@ contract SignatureDropTest is BaseTest { conditions[2].maxClaimableSupply = 31; conditions[2].quantityLimitPerTransaction = 32; conditions[2].waitTimeInSecondsBetweenClaims = 33; - sigdrop.setClaimConditions(conditions, false, ""); + sigdrop.setClaimConditions(conditions, false); vm.expectRevert("!CONDITION."); sigdrop.getActiveClaimConditionId(); @@ -947,14 +947,14 @@ contract SignatureDropTest is BaseTest { vm.prank(deployerSigner); sigdrop.lazyMint(100, "ipfs://", ""); - vm.prank(deployer); - sigdrop.setClaimConditions(conditions, false, ""); + vm.prank(deployerSigner); + sigdrop.setClaimConditions(conditions, false); vm.prank(getActor(5), getActor(5)); sigdrop.claim(receiver, 1, address(0), 0, alp, ""); - vm.prank(deployer); - sigdrop.setClaimConditions(conditions, true, ""); + vm.prank(deployerSigner); + sigdrop.setClaimConditions(conditions, true); vm.prank(getActor(5), getActor(5)); sigdrop.claim(receiver, 1, address(0), 0, alp, "");