From 7b4bf073f0840b00a0cfd0175a08dc957dc702fe Mon Sep 17 00:00:00 2001 From: bezerrablockchain Date: Mon, 21 Oct 2024 17:26:59 -0400 Subject: [PATCH] feat: added External Contributors NFT collection --- .../ExternalContributorsEcosystemPartner.sol | 28 +++++++ .../ExternalContributorsEcosystemPartner.ts | 35 +++++++++ params/ExtContributorsEP/mainnet.json | 9 +++ params/ExtContributorsEP/testnet.json | 21 +++++ test/ExtContributorsEP.test.ts | 78 +++++++++++++++++++ 5 files changed, 171 insertions(+) create mode 100644 contracts/NFT/ExternalContributorsEcosystemPartner.sol create mode 100644 ignition/modules/ExternalContributorsEcosystemPartner.ts create mode 100644 params/ExtContributorsEP/mainnet.json create mode 100644 params/ExtContributorsEP/testnet.json create mode 100644 test/ExtContributorsEP.test.ts diff --git a/contracts/NFT/ExternalContributorsEcosystemPartner.sol b/contracts/NFT/ExternalContributorsEcosystemPartner.sol new file mode 100644 index 0000000..a17a0d8 --- /dev/null +++ b/contracts/NFT/ExternalContributorsEcosystemPartner.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.0.0 +pragma solidity ^0.8.20; + +import {ERC721UpgradableAirdroppable} from "./ERC721UpgradableAirdroppable.sol"; + +contract ExternalContributorsEcosystemPartner is ERC721UpgradableAirdroppable { + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + string calldata contractName, + string calldata symbol, + address initialOwner, + uint256 maxNftSupply + ) public initializer { + __ERC721Airdroppable_init(maxNftSupply); + __ERC721UpgradableBase_init(contractName, symbol, initialOwner); + } + + function _authorizeUpgrade(address newImplementation) internal virtual override onlyOwner {} + + function _baseURI() internal pure override returns (string memory) { + return "ipfs://"; + } +} diff --git a/ignition/modules/ExternalContributorsEcosystemPartner.ts b/ignition/modules/ExternalContributorsEcosystemPartner.ts new file mode 100644 index 0000000..a24cb73 --- /dev/null +++ b/ignition/modules/ExternalContributorsEcosystemPartner.ts @@ -0,0 +1,35 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' + +export const extContributersEpProxyModule = buildModule('ExtContributorsEP', m => { + // deploy implementation + const implementation = m.contract('ExternalContributorsEcosystemPartner', [], { id: 'Implementation' }) + + // initializer parameters + const contractName = m.getParameter('contractName') + const contractSymbol = m.getParameter('symbol') + const deployer = m.getAccount(0) + const maxNftSupply = m.getParameter('maxNftSupply') + + // deploy proxy + const proxy = m.contract('ERC1967Proxy', [ + implementation, + m.encodeFunctionCall( + implementation, + 'initialize', + [contractName, contractSymbol, deployer, maxNftSupply], + { id: 'Proxy' }, + ), + ]) + const ExtContributorsEP = m.contractAt('ExternalContributorsEcosystemPartner', proxy, { + id: 'Contract', + }) + + // Airdrop + const ipfsCids = m.getParameter('ipfsCids') + const airdropAddresses = m.getParameter('airdropAddresses') + m.call(ExtContributorsEP, 'airdrop', [ipfsCids, airdropAddresses]) + + return { ExtContributorsEP } +}) + +export default extContributersEpProxyModule diff --git a/params/ExtContributorsEP/mainnet.json b/params/ExtContributorsEP/mainnet.json new file mode 100644 index 0000000..1cd02da --- /dev/null +++ b/params/ExtContributorsEP/mainnet.json @@ -0,0 +1,9 @@ +{ + "ExtContributorsEP": { + "contractName": "ExternalContributorsEcosystemPartner", + "symbol": "ECEP", + "maxNftSupply": 5, + "ipfsCids": [], + "airdropAddresses": [] + } +} diff --git a/params/ExtContributorsEP/testnet.json b/params/ExtContributorsEP/testnet.json new file mode 100644 index 0000000..7b1e087 --- /dev/null +++ b/params/ExtContributorsEP/testnet.json @@ -0,0 +1,21 @@ +{ + "ExtContributorsEP": { + "contractName": "ExternalContributorsEcosystemPartner", + "symbol": "ECEP", + "maxNftSupply": 5, + "ipfsCids": [ + "QmeGNJxhSToWYvHucUMX4D2fX8Uk2aL8CujwFuGw2Moy9U", + "QmPFcgjgqCMoEbBmkbnvwaknENTH3WckjCGYekQhXJFVzn", + "Qme6NCykZUUv78jXydTgmmehHThVXpJHFxAbZvEJkkPk5o", + "QmUrqydS9kLYDJtv6dY7suhT5m5UPxJe1K1yq9b7QoY1VD", + "QmcbYfu8CuhHRfRmDHwc8P8vqpgnYSCuzLcwtr5n4afMKb" + ], + "airdropAddresses": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x90F79bf6EB2c4f870365E785982E1f101E93b906", + "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65", + "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc" + ] + } +} diff --git a/test/ExtContributorsEP.test.ts b/test/ExtContributorsEP.test.ts new file mode 100644 index 0000000..de3c99f --- /dev/null +++ b/test/ExtContributorsEP.test.ts @@ -0,0 +1,78 @@ +import { expect } from 'chai' +import hre, { ethers, ignition } from 'hardhat' +import { ExternalContributorsEcosystemPartner } from '../typechain-types' +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers' +import { extContributersEpProxyModule } from '../ignition/modules/ExternalContributorsEcosystemPartner' +import deployParams from '../params/ExtContributorsEP/testnet.json' + +describe('ExternalContributorsEcosystemPartner NFT', () => { + let deployer: SignerWithAddress + let alice: SignerWithAddress + const oldGangsters: SignerWithAddress[] = [] + let extContEP: ExternalContributorsEcosystemPartner + + before(async () => { + ;[deployer, alice] = await ethers.getSigners() + const contract = await ignition.deploy(extContributersEpProxyModule, { + parameters: deployParams, + }) + // impersonating airdrop receivers + for (let i = 0; i < deployParams.ExtContributorsEP.airdropAddresses.length; i++) { + const senderAddr = deployParams.ExtContributorsEP.airdropAddresses[i] + await hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [senderAddr], + }) + const sender = await ethers.getSigner(senderAddr) + oldGangsters.push(sender) + } + extContEP = contract.ExtContributorsEP as unknown as ExternalContributorsEcosystemPartner + }) + + describe('Upon deployment', () => { + it('should set up proper NFT name and symbol', async () => { + expect(await extContEP.connect(deployer).name()).to.equal(deployParams.ExtContributorsEP.contractName) + expect(await extContEP.symbol()).to.equal(deployParams.ExtContributorsEP.symbol) + }) + + it('should have performed airdrop during deployment', async () => { + const ipfsCids = deployParams.ExtContributorsEP.ipfsCids + for (let i = 0; i < oldGangsters.length; i++) { + expect(await extContEP.ownerOf(i + 1)).to.equal(oldGangsters[i].address) + expect(await extContEP.tokenURI(i + 1)).to.equal(`ipfs://${ipfsCids[i]}`) + } + }) + }) + + describe('Transfer functionality is disabled', () => { + it('transfers should be forbidden after airdrop', async () => { + await Promise.all( + oldGangsters.map(async (sender, i) => { + await expect( + extContEP.connect(sender).transferFrom(sender.address, alice.address, i + 1), + ).to.be.revertedWithCustomError(extContEP, 'TransfersDisabled') + }), + ) + }) + + it('approvals should be forbidden', async () => { + await Promise.all( + oldGangsters.map(async (sender, i) => { + await expect( + extContEP.connect(sender).approve(alice.address, i + 1), + ).to.be.revertedWithCustomError(extContEP, 'TransfersDisabled') + }), + ) + }) + + it('setApprovalForAll should be forbidden', async () => { + await Promise.all( + oldGangsters.map(async sender => { + await expect( + extContEP.connect(sender).setApprovalForAll(alice.address, true), + ).to.be.revertedWithCustomError(extContEP, 'TransfersDisabled') + }), + ) + }) + }) +})