Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature/nft drop/cli refactoring #31

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
00ba65e
initial commit with TODOs for NFT project
Jul 25, 2024
84a8ad7
resolve todos in .sol
Jul 25, 2024
620fca5
create dir before file save
Aug 11, 2024
97cf5c6
Merge pull request #2 from LostBodyStore/bugifx/mkdir_before_file_save
nerewarin Aug 11, 2024
83570cb
Merge remote-tracking branch 'refs/remotes/origin/master' into featur…
Aug 11, 2024
dbb8b89
initial nft drop generation
Aug 12, 2024
79574e9
fix nft drop generation and make version auto-incrementation.
Aug 12, 2024
0d1ba6a
fix nft drop - fix docstring
Aug 12, 2024
3ef048c
fix NFTMerkleDrop.sol, make hardhat test run ok
Aug 12, 2024
1db8a27
nft_drop.js - support Validation and wipe mode
Aug 12, 2024
11771e8
nft_drop.js - 1. Properly format and save Merkle proofs as hex strings.
Aug 13, 2024
eec4452
fix NFTContract, initial deploy_nft.js version
Aug 13, 2024
b4bf442
add deploy_nft.js documentation
Aug 13, 2024
7b0f21c
merkle leaves are now in format account->[tokenIds] instead of tokenI…
Aug 15, 2024
4f07f38
fix deploy_nft.js and try to claim nft
Aug 15, 2024
10f2a0e
another probe to run claim_nft.js
Aug 15, 2024
f24a960
fix networks setup for polygon, polygonAmoy and Sepolia
Aug 17, 2024
b8dec86
rename nft_drop.js/execute to generate_nft_drop with settings as argu…
Aug 17, 2024
ec2f0e1
extract DropResult structure. continue working on nft drop test
Aug 18, 2024
ca8b794
working on NFTMerkleDrop.test.js - nft transfers reverts on claim now
Aug 18, 2024
c7a3b36
working on NFTMerkleDrop.test.js - add more checks. nft transfers are…
Aug 18, 2024
9e719ec
fixed ERC721IncorrectOwner on claiming test, connect nftDrop from rec…
Aug 18, 2024
f42e202
finally fixed NFTMerkleDrop.behavior.js
Aug 18, 2024
2ebdf22
lint:fix and make all tests passed
Aug 18, 2024
5f892e3
rename cli to nft:createDrop, run with yarn.
Aug 18, 2024
bf64278
move utils to gen_nft_lib.js
Aug 18, 2024
6da75e8
nft_drop.js refactoring... still bad
Aug 18, 2024
ee8cc6e
final nft_drop.js fixes both for hardhat test and cli modes
Aug 18, 2024
d63eea2
lint js
Aug 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions contracts/MyERC721Token.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Import specific contracts rather than globally importing entire files.
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract MyERC721Token is ERC721, ERC721URIStorage, Ownable {
constructor(string memory name, string memory symbol, address initialOwner)
ERC721(name, symbol)
Ownable(initialOwner)
{
_mint(msg.sender, 0);
// hash QmUt8uq3GwXjrGDm2We5FEX2rzU3fSEaMTkrHoSKdkRBXR
_setTokenURI(0, "https://gateway.pinata.cloud/ipfs/QmdFAGkcP8zpQW2Crka2KJwdtkuBc4e9eq4E4X2sBwFY2X");

_mint(msg.sender, 1);
// hash QmX3rshx3RJRKYUoE5n42jvkoSYehNppukaq3c1trWeEoF
_setTokenURI(1, "https://gateway.pinata.cloud/ipfs/QmSBAapfuRb7wPtZ6a8Qrwhm3AMsBQcn6oVJaDH9ZXubrF");
}

function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) {
require(ownerOf(tokenId) != address(0), "Nonexistent token");
return super.tokenURI(tokenId);
}

function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721URIStorage) returns (bool) {
return super.supportsInterface(interfaceId);
}
}
61 changes: 61 additions & 0 deletions contracts/NFTMerkleDrop.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;
pragma abicoder v1;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

import { INFTMerkleDrop } from "./interfaces/INFTMerkleDrop.sol";

contract NFTMerkleDrop is Ownable, INFTMerkleDrop {
bytes32 public override merkleRoot;
address public nftContract;
mapping(address => bool) public claimed;

constructor(address initialNFTContract, bytes32 merkleRoot_) Ownable(msg.sender) {
nftContract = initialNFTContract;
merkleRoot = merkleRoot_;
}

// Sets the merkle root of the merkle tree
function setMerkleRoot(bytes32 merkleRoot_) external override onlyOwner {
emit MerkelRootUpdated(merkleRoot, merkleRoot_);
merkleRoot = merkleRoot_;
}

// Sets the NFT contract address from which the NFTs will be transferred
function setNFTContract(address nftContract_) external override onlyOwner {
require(nftContract_ != address(0), "Invalid NFT contract address");
emit NFTContractUpdated(nftContract, nftContract_);
nftContract = nftContract_;
}

// Claims the given NFTs to the specified address
function claim(
address account,
uint256[] calldata tokenIds,
bytes32 expectedMerkleRoot,
bytes32[] calldata merkleProof
) external override {
if (merkleRoot != expectedMerkleRoot) revert MerkleRootWasUpdated();

// Check if already claimed
if (claimed[account]) revert NothingToClaim();

// Verify merkle proof
bytes32 leaf = keccak256(abi.encodePacked(account, tokenIds));
if (!MerkleProof.verify(merkleProof, expectedMerkleRoot, leaf)) revert InvalidProof();

// Mark it claimed
claimed[account] = true;

// Send the NFTs
for (uint256 i = 0; i < tokenIds.length; i++) {
IERC721(nftContract).safeTransferFrom(owner(), account, tokenIds[i]);
}

emit Claimed(account, tokenIds);
}
}
37 changes: 37 additions & 0 deletions contracts/interfaces/INFTMerkleDrop.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;
pragma abicoder v1;

// Allows anyone to claim NFTs if they exist in a merkle root.
interface INFTMerkleDrop {
// Event emitted when the NFT contract address is updated
event NFTContractUpdated(address indexed previousNFTContract, address indexed newNFTContract);

// This event is triggered whenever a call to #setMerkleRoot succeeds.
event MerkelRootUpdated(bytes32 oldMerkleRoot, bytes32 newMerkleRoot);

// This event is triggered whenever a call to #claim succeeds.
event Claimed(address indexed account, uint256[] tokenIds);

error InvalidProof();
error NothingToClaim();
error MerkleRootWasUpdated();

// Returns the merkle root of the merkle tree containing cumulative account balances available to claim.
function merkleRoot() external view returns (bytes32);

// Sets the merkle root of the merkle tree containing cumulative account balances available to claim.
function setMerkleRoot(bytes32 merkleRoot_) external;

// Sets the NFT contract address from which the NFTs will be transferred.
function setNFTContract(address nftContract_) external;

// Claim the given NFTs to the given address. Reverts if the inputs are invalid.
function claim(
address account,
uint256[] calldata tokenIds,
bytes32 expectedMerkleRoot,
bytes32[] calldata merkleProof
) external;
}
79 changes: 79 additions & 0 deletions deploy/deploy_nft.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Deploys the NFTMerkleDrop contract to the specified network.
*
* This script deploys the NFTMerkleDrop smart contract to a blockchain network
* and verifies it on Etherscan. It uses the specified network and merkle root
* to initialize the contract. If the contract has already been deployed and
* verified on Etherscan, the script will acknowledge this and provide the
* Etherscan link to the verified contract.
*
* Prerequisites:
* - Ensure that the following environment variables are set in your `.env` file:
* - `PRIVATE_KEY`: The private key of the deployer's account.
* - `POLYGONSCAN_API_KEY`: The API key for verifying contracts on PolygonScan (for Polygon networks).
* - `INFURA_API_KEY`: The Infura API key for connecting to the network (used for Polygon and other supported networks).
*
* Example Usage:
* - Deploying to Polygon Amoy network with a specific NFT contract and merkle root from input/0.json:
* `npx hardhat deploy:nft --network polygonAmoy --n 0x16B9563f4105a873e756479FC9716ab71E419b7D --r 0x877f9206c3851f0b52f6db59bf278d09`
*
* Expected Output:
* - Deploys the NFTMerkleDrop contract to the specified network.
* - If the contract is already verified on Etherscan, it will acknowledge this and provide the Etherscan link.
* - Outputs the deployed contract address.
*
* Example Output:
* - Deploying NFTMerkleDrop to network ID 80002 with merkleRoot 0x00000000000000000000000000000000877f9206c3851f0b52f6db59bf278d09
* - The contract 0x293c897d9C4c67Ba09cC3f2ad4691D6445809515 has already been verified on Etherscan.
* - https://amoy.polygonscan.com/address/0x293c897d9C4c67Ba09cC3f2ad4691D6445809515#code
* - NFTMerkleDrop deployed to: 0x293c897d9C4c67Ba09cC3f2ad4691D6445809515
*/

// Load Hardhat environment
require('hardhat/config');

const hre = require('hardhat');
const { deployAndGetContract } = require('@1inch/solidity-utils');
const { ethers } = hre;

// Main function to deploy the NFTMerkleDrop contract
async function main ({ nftContract, merkleRoot, deployments, getNamedAccounts }) {
const chainId = await ethers.provider.getNetwork().then(net => net.chainId);
const { deployer } = await getNamedAccounts();

console.log(`Deploying NFTMerkleDrop to network ID ${chainId} with nftContract ${nftContract} and merkleRoot ${merkleRoot}`);

// Setting the gas fees to avoid the 'transaction underpriced' error
const maxFeePerGas = 50e9; // 50 gwei
const maxPriorityFeePerGas = 2e9; // 2 gwei

// Deploy the contract with the constructor arguments
const nftMerkleDrop = await deployAndGetContract({
contractName: 'NFTMerkleDrop',
constructorArgs: [nftContract, merkleRoot],
deployments,
deployer,
overrides: {
maxFeePerGas,
maxPriorityFeePerGas,
},
});

console.log('NFTMerkleDrop deployed to:', await nftMerkleDrop.getAddress());
}

// Allow the script to be used both as a task in Hardhat and as a standalone Node.js script
if (require.main === module) {
const args = {
nftContract: process.argv[2],
merkleRoot: process.argv[3],
};
main(args)
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
}

module.exports = main;
83 changes: 82 additions & 1 deletion hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,76 @@ require('dotenv').config();
const { task } = require('hardhat/config');
const { Networks, getNetwork } = require('@1inch/solidity-utils/hardhat-setup');

const { networks, etherscan } = (new Networks()).registerAll();
function setUpNetworks () {
const networksCollector = new Networks();
const { etherscan } = networksCollector.registerAll();
const customNetworks = {
polygon: {
network: 'polygon',
chainId: 137,
urls: {
rpcURL: `https://polygon-mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`,
etherscanApiURL: 'https://api.polygonscan.com/api',
browserURL: 'https://polygonscan.com/',
},
hardfork: 'london',
},
sepolia: {
network: 'sepolia',
chainId: 11155111, // Sepolia testnet chainId
urls: {
rpcURL: `https://sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`,
etherscanApiURL: 'https://api-sepolia.etherscan.io/api',
browserURL: 'https://sepolia.etherscan.io/',
},
hardfork: 'london',
},
polygonAmoy: {
network: 'polygonAmoy',
chainId: 80002, // Assuming PolygonAmoy testnet chainId is 80002
urls: {
rpcURL: `https://polygon-amoy.infura.io/v3/${process.env.INFURA_API_KEY}`,
etherscanApiURL: 'https://api-amoy.polygonscan.com/api',
browserURL: 'https://amoy.polygonscan.com/',
},
hardfork: 'london',
},
};

// Registering custom networks
Object.entries(customNetworks).forEach(([name, data]) => {
networksCollector.registerCustom(
data.network,
data.chainId,
data.urls.rpcURL,
process.env[`${name.toUpperCase()}_PRIVATE_KEY`] || process.env.PRIVATE_KEY,
data.urls.etherscanApiURL,
data.urls.rpcURL,
data.urls.browserURL,
data.hardfork,
);

etherscan.customChains.push({
network: data.network,
chainId: data.chainId,
urls: {
apiURL: data.urls.etherscanApiURL,
browserURL: data.urls.browserURL,
},
});
});
// Extend etherscan API keys
etherscan.apiKey = {
...etherscan.apiKey,
eth: process.env.ETHERSCAN_API_KEY,
sepolia: process.env.ETHERSCAN_API_KEY,
polygon: process.env.POLYGONSCAN_API_KEY,
polygonAmoy: process.env.POLYGONSCAN_API_KEY,
};
return { networks: networksCollector.networks, etherscan };
}

const { networks, etherscan } = setUpNetworks();

// usage : yarn qr:deploy hardhat --v <version> --r <root> --h <height>
// example : yarn qr:deploy hardhat --v 35 --r 0xc8f9f70ceaa4d05d893e74c933eed42b --h 9
Expand All @@ -28,6 +97,18 @@ task('deploy:qr', 'Deploys contracts with custom parameters')
merkleHeight: taskArgs.h,
});
});
task('deploy:nft', 'Deploys the NFTMerkleDrop contract with custom parameters')
.addParam('n', 'The NFT contract address')
.addParam('r', 'The 16-byte Merkle root')
.setAction(async (taskArgs, hre) => {
const deploymentScript = require('./deploy/deploy_nft.js');
await deploymentScript({
nftContract: taskArgs.n,
merkleRoot: taskArgs.r,
deployments: hre.deployments,
getNamedAccounts: hre.getNamedAccounts,
});
});

module.exports = {
etherscan,
Expand Down
4 changes: 4 additions & 0 deletions input/0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"0": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"1": "0x53d284357ec70ce289d6d64134dfac8e511c8a3d"
}
7 changes: 7 additions & 0 deletions input/1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"0": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"1": "0x53d284357ec70ce289d6d64134dfac8e511c8a3d",
"3": "0x53d284357ec70ce289d6d64134dfac8e511c8a3d",
"4": "0x53d284357ec70ce289d6d64134dfac8e511c8a3d",
"5": "0x53d284357ec70ce289d6d64134dfac8e511c8a3d"
}
4 changes: 4 additions & 0 deletions input/testMapping.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"0": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"1": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,15 @@
"qr:deploy": "hardhat deploy:qr --network",
"qr:create": "node ./src/qrdrop.js -gqlczv",
"qr:check": "node ./src/qrdrop.js -x",
"nft:createDrop": "node ./src/nft_drop/nft_drop.js",
"lint": "yarn run lint:js && yarn run lint:sol",
"lint:fix": "yarn run lint:js:fix && yarn run lint:sol:fix",
"lint:js": "eslint .",
"lint:js:fix": "eslint . --fix",
"lint:sol": "solhint --max-warnings 0 \"contracts/**/*.sol\"",
"lint:sol:fix": "solhint --max-warnings 0 \"contracts/**/*.sol\" --fix",
"test": "hardhat test",
"testAmoy": "hardhat test --network polygonAmoy",
"genqr": "node ./src/qrdrop.js"
}
}
Loading