Skip to content

Commit ec47cdb

Browse files
Burn-to-claim drop ERC721 contract (#404)
* burn to claim drop contract * tests * tests for burn and claim * more tests * Move BurnToClaimDropERC721 to unaudited dir --------- Co-authored-by: nkrishang <62195808+nkrishang@users.noreply.github.com> Co-authored-by: Krishang <krishang@thirdweb.com>
1 parent 8d5c47f commit ec47cdb

37 files changed

+4732
-3
lines changed

contracts/dynamic-contracts/eip/ERC721AUpgradeable.sol

Lines changed: 657 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.0;
3+
4+
library BatchMintMetadataStorage {
5+
bytes32 public constant BATCH_MINT_METADATA_STORAGE_POSITION = keccak256("batch.mint.metadata.storage");
6+
7+
struct Data {
8+
/// @dev Largest tokenId of each batch of tokens with the same baseURI.
9+
uint256[] batchIds;
10+
/// @dev Mapping from id of a batch of tokens => to base URI for the respective batch of tokens.
11+
mapping(uint256 => string) baseURI;
12+
}
13+
14+
function batchMintMetadataStorage() internal pure returns (Data storage batchMintMetadataData) {
15+
bytes32 position = BATCH_MINT_METADATA_STORAGE_POSITION;
16+
assembly {
17+
batchMintMetadataData.slot := position
18+
}
19+
}
20+
}
21+
22+
/**
23+
* @title Batch-mint Metadata
24+
* @notice The `BatchMintMetadata` is a contract extension for any base NFT contract. It lets the smart contract
25+
* using this extension set metadata for `n` number of NFTs all at once. This is enabled by storing a single
26+
* base URI for a batch of `n` NFTs, where the metadata for each NFT in a relevant batch is `baseURI/tokenId`.
27+
*/
28+
29+
contract BatchMintMetadata {
30+
/**
31+
* @notice Returns the count of batches of NFTs.
32+
* @dev Each batch of tokens has an in ID and an associated `baseURI`.
33+
* See {batchIds}.
34+
*/
35+
function getBaseURICount() public view returns (uint256) {
36+
BatchMintMetadataStorage.Data storage data = BatchMintMetadataStorage.batchMintMetadataStorage();
37+
return data.batchIds.length;
38+
}
39+
40+
/**
41+
* @notice Returns the ID for the batch of tokens the given tokenId belongs to.
42+
* @dev See {getBaseURICount}.
43+
* @param _index ID of a token.
44+
*/
45+
function getBatchIdAtIndex(uint256 _index) public view returns (uint256) {
46+
BatchMintMetadataStorage.Data storage data = BatchMintMetadataStorage.batchMintMetadataStorage();
47+
48+
if (_index >= getBaseURICount()) {
49+
revert("Invalid index");
50+
}
51+
return data.batchIds[_index];
52+
}
53+
54+
/// @dev Returns the id for the batch of tokens the given tokenId belongs to.
55+
function _getBatchId(uint256 _tokenId) internal view returns (uint256 batchId, uint256 index) {
56+
BatchMintMetadataStorage.Data storage data = BatchMintMetadataStorage.batchMintMetadataStorage();
57+
58+
uint256 numOfTokenBatches = getBaseURICount();
59+
uint256[] memory indices = data.batchIds;
60+
61+
for (uint256 i = 0; i < numOfTokenBatches; i += 1) {
62+
if (_tokenId < indices[i]) {
63+
index = i;
64+
batchId = indices[i];
65+
66+
return (batchId, index);
67+
}
68+
}
69+
70+
revert("Invalid tokenId");
71+
}
72+
73+
/// @dev Returns the baseURI for a token. The intended metadata URI for the token is baseURI + tokenId.
74+
function _getBaseURI(uint256 _tokenId) internal view returns (string memory) {
75+
BatchMintMetadataStorage.Data storage data = BatchMintMetadataStorage.batchMintMetadataStorage();
76+
77+
uint256 numOfTokenBatches = getBaseURICount();
78+
uint256[] memory indices = data.batchIds;
79+
80+
for (uint256 i = 0; i < numOfTokenBatches; i += 1) {
81+
if (_tokenId < indices[i]) {
82+
return data.baseURI[indices[i]];
83+
}
84+
}
85+
revert("Invalid tokenId");
86+
}
87+
88+
/// @dev Sets the base URI for the batch of tokens with the given batchId.
89+
function _setBaseURI(uint256 _batchId, string memory _baseURI) internal {
90+
BatchMintMetadataStorage.Data storage data = BatchMintMetadataStorage.batchMintMetadataStorage();
91+
data.baseURI[_batchId] = _baseURI;
92+
}
93+
94+
/// @dev Mints a batch of tokenIds and associates a common baseURI to all those Ids.
95+
function _batchMintMetadata(
96+
uint256 _startId,
97+
uint256 _amountToMint,
98+
string memory _baseURIForTokens
99+
) internal returns (uint256 nextTokenIdToMint, uint256 batchId) {
100+
batchId = _startId + _amountToMint;
101+
nextTokenIdToMint = batchId;
102+
103+
BatchMintMetadataStorage.Data storage data = BatchMintMetadataStorage.batchMintMetadataStorage();
104+
105+
data.batchIds.push(batchId);
106+
data.baseURI[batchId] = _baseURIForTokens;
107+
}
108+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.0;
3+
4+
/// @author thirdweb
5+
6+
import { ERC1155Burnable } from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol";
7+
import { ERC721Burnable } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
8+
9+
import "../../eip/interface/IERC1155.sol";
10+
import "../../eip/interface/IERC721.sol";
11+
12+
import "../../extension/interface/IBurnToClaim.sol";
13+
14+
library BurnToClaimStorage {
15+
bytes32 public constant BURN_TO_CLAIM_STORAGE_POSITION = keccak256("burn.to.claim.storage");
16+
17+
struct Data {
18+
IBurnToClaim.BurnToClaimInfo burnToClaimInfo;
19+
}
20+
21+
function burnToClaimStorage() internal pure returns (Data storage burnToClaimData) {
22+
bytes32 position = BURN_TO_CLAIM_STORAGE_POSITION;
23+
assembly {
24+
burnToClaimData.slot := position
25+
}
26+
}
27+
}
28+
29+
abstract contract BurnToClaim is IBurnToClaim {
30+
function getBurnToClaimInfo() public view returns (BurnToClaimInfo memory) {
31+
BurnToClaimStorage.Data storage data = BurnToClaimStorage.burnToClaimStorage();
32+
33+
return data.burnToClaimInfo;
34+
}
35+
36+
function setBurnToClaimInfo(BurnToClaimInfo calldata _burnToClaimInfo) external virtual {
37+
require(_canSetBurnToClaim(), "Not authorized.");
38+
39+
BurnToClaimStorage.Data storage data = BurnToClaimStorage.burnToClaimStorage();
40+
data.burnToClaimInfo = _burnToClaimInfo;
41+
}
42+
43+
function verifyBurnToClaim(
44+
address _tokenOwner,
45+
uint256 _tokenId,
46+
uint256 _quantity
47+
) public view virtual {
48+
BurnToClaimInfo memory _burnToClaimInfo = getBurnToClaimInfo();
49+
require(_burnToClaimInfo.originContractAddress != address(0), "Origin contract not set.");
50+
51+
if (_burnToClaimInfo.tokenType == IBurnToClaim.TokenType.ERC721) {
52+
require(_quantity == 1, "Invalid amount");
53+
require(IERC721(_burnToClaimInfo.originContractAddress).ownerOf(_tokenId) == _tokenOwner, "!Owner");
54+
} else if (_burnToClaimInfo.tokenType == IBurnToClaim.TokenType.ERC1155) {
55+
uint256 _eligible1155TokenId = _burnToClaimInfo.tokenId;
56+
57+
require(_tokenId == _eligible1155TokenId, "Invalid token Id");
58+
require(
59+
IERC1155(_burnToClaimInfo.originContractAddress).balanceOf(_tokenOwner, _tokenId) >= _quantity,
60+
"!Balance"
61+
);
62+
}
63+
}
64+
65+
function _burnTokensOnOrigin(
66+
address _tokenOwner,
67+
uint256 _tokenId,
68+
uint256 _quantity
69+
) internal virtual {
70+
BurnToClaimInfo memory _burnToClaimInfo = getBurnToClaimInfo();
71+
72+
if (_burnToClaimInfo.tokenType == IBurnToClaim.TokenType.ERC721) {
73+
ERC721Burnable(_burnToClaimInfo.originContractAddress).burn(_tokenId);
74+
} else if (_burnToClaimInfo.tokenType == IBurnToClaim.TokenType.ERC1155) {
75+
ERC1155Burnable(_burnToClaimInfo.originContractAddress).burn(_tokenOwner, _tokenId, _quantity);
76+
}
77+
}
78+
79+
function _canSetBurnToClaim() internal view virtual returns (bool);
80+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-License-Identifier: Apache 2.0
2+
pragma solidity ^0.8.0;
3+
4+
/// @author thirdweb
5+
6+
import { OperatorFiltererUpgradeable } from "./OperatorFiltererUpgradeable.sol";
7+
8+
abstract contract DefaultOperatorFiltererUpgradeable is OperatorFiltererUpgradeable {
9+
address constant DEFAULT_SUBSCRIPTION = address(0x3cc6CddA760b79bAfa08dF41ECFA224f810dCeB6);
10+
11+
function __DefaultOperatorFilterer_init() internal {
12+
OperatorFiltererUpgradeable.__OperatorFilterer_init(DEFAULT_SUBSCRIPTION, true);
13+
}
14+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.0;
3+
4+
/// @author thirdweb
5+
6+
import "../../extension/interface/IDelayedReveal.sol";
7+
8+
library DelayedRevealStorage {
9+
bytes32 public constant DELAYED_REVEAL_STORAGE_POSITION = keccak256("delayed.reveal.storage");
10+
11+
struct Data {
12+
/// @dev Mapping from tokenId of a batch of tokens => to delayed reveal data.
13+
mapping(uint256 => bytes) encryptedData;
14+
}
15+
16+
function delayedRevealStorage() internal pure returns (Data storage delayedRevealData) {
17+
bytes32 position = DELAYED_REVEAL_STORAGE_POSITION;
18+
assembly {
19+
delayedRevealData.slot := position
20+
}
21+
}
22+
}
23+
24+
/**
25+
* @title Delayed Reveal
26+
* @notice Thirdweb's `DelayedReveal` is a contract extension for base NFT contracts. It lets you create batches of
27+
* 'delayed-reveal' NFTs. You can learn more about the usage of delayed reveal NFTs here - https://blog.thirdweb.com/delayed-reveal-nfts
28+
*/
29+
30+
abstract contract DelayedReveal is IDelayedReveal {
31+
/// @dev Mapping from tokenId of a batch of tokens => to delayed reveal data.
32+
function encryptedData(uint256 _tokenId) public view returns (bytes memory) {
33+
DelayedRevealStorage.Data storage data = DelayedRevealStorage.delayedRevealStorage();
34+
return data.encryptedData[_tokenId];
35+
}
36+
37+
/// @dev Sets the delayed reveal data for a batchId.
38+
function _setEncryptedData(uint256 _batchId, bytes memory _encryptedData) internal {
39+
DelayedRevealStorage.Data storage data = DelayedRevealStorage.delayedRevealStorage();
40+
data.encryptedData[_batchId] = _encryptedData;
41+
}
42+
43+
/**
44+
* @notice Returns revealed URI for a batch of NFTs.
45+
* @dev Reveal encrypted base URI for `_batchId` with caller/admin's `_key` used for encryption.
46+
* Reverts if there's no encrypted URI for `_batchId`.
47+
* See {encryptDecrypt}.
48+
*
49+
* @param _batchId ID of the batch for which URI is being revealed.
50+
* @param _key Secure key used by caller/admin for encryption of baseURI.
51+
*
52+
* @return revealedURI Decrypted base URI.
53+
*/
54+
function getRevealURI(uint256 _batchId, bytes calldata _key) public view returns (string memory revealedURI) {
55+
DelayedRevealStorage.Data storage data = DelayedRevealStorage.delayedRevealStorage();
56+
57+
bytes memory dataForBatch = data.encryptedData[_batchId];
58+
if (dataForBatch.length == 0) {
59+
revert("Nothing to reveal");
60+
}
61+
62+
(bytes memory encryptedURI, bytes32 provenanceHash) = abi.decode(dataForBatch, (bytes, bytes32));
63+
64+
revealedURI = string(encryptDecrypt(encryptedURI, _key));
65+
66+
require(keccak256(abi.encodePacked(revealedURI, _key, block.chainid)) == provenanceHash, "Incorrect key");
67+
}
68+
69+
/**
70+
* @notice Encrypt/decrypt data on chain.
71+
* @dev Encrypt/decrypt given `data` with `key`. Uses inline assembly.
72+
* See: https://ethereum.stackexchange.com/questions/69825/decrypt-message-on-chain
73+
*
74+
* @param data Bytes of data to encrypt/decrypt.
75+
* @param key Secure key used by caller for encryption/decryption.
76+
*
77+
* @return result Output after encryption/decryption of given data.
78+
*/
79+
function encryptDecrypt(bytes memory data, bytes calldata key) public pure override returns (bytes memory result) {
80+
// Store data length on stack for later use
81+
uint256 length = data.length;
82+
83+
// solhint-disable-next-line no-inline-assembly
84+
assembly {
85+
// Set result to free memory pointer
86+
result := mload(0x40)
87+
// Increase free memory pointer by lenght + 32
88+
mstore(0x40, add(add(result, length), 32))
89+
// Set result length
90+
mstore(result, length)
91+
}
92+
93+
// Iterate over the data stepping by 32 bytes
94+
for (uint256 i = 0; i < length; i += 32) {
95+
// Generate hash of the key and offset
96+
bytes32 hash = keccak256(abi.encodePacked(key, i));
97+
98+
bytes32 chunk;
99+
// solhint-disable-next-line no-inline-assembly
100+
assembly {
101+
// Read 32-bytes data chunk
102+
chunk := mload(add(data, add(i, 32)))
103+
}
104+
// XOR the chunk with hash
105+
chunk ^= hash;
106+
// solhint-disable-next-line no-inline-assembly
107+
assembly {
108+
// Write 32-byte encrypted chunk
109+
mstore(add(result, add(i, 32)), chunk)
110+
}
111+
}
112+
}
113+
114+
/**
115+
* @notice Returns whether the relvant batch of NFTs is subject to a delayed reveal.
116+
* @dev Returns `true` if `_batchId`'s base URI is encrypted.
117+
* @param _batchId ID of a batch of NFTs.
118+
*/
119+
function isEncryptedBatch(uint256 _batchId) public view returns (bool) {
120+
DelayedRevealStorage.Data storage data = DelayedRevealStorage.delayedRevealStorage();
121+
return data.encryptedData[_batchId].length > 0;
122+
}
123+
}

0 commit comments

Comments
 (0)