Skip to content

Commit

Permalink
Merge pull request #82 from RootstockCollective/rb-dao-759-nftec
Browse files Browse the repository at this point in the history
  • Loading branch information
shenshin authored Oct 25, 2024
2 parents 86c9046 + 21c25ae commit 2904e61
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 0 deletions.
15 changes: 15 additions & 0 deletions contracts/NFT/ERC721UpgradableAirdroppable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ struct AirdropRecipient {
}
struct AirdroppableStorage {
uint256 _nextTokenId;
bool _locked;
}

interface IAirdroppable {
Expand All @@ -19,6 +20,10 @@ interface IAirdroppable {
}

abstract contract ERC721UpgradableAirdroppable is ERC721UpgradableBase, IAirdroppable {
error AirdropMintingLocked(uint256 numMinted);

event AirDropLocked(uint256 numMinted);

// keccak256(abi.encode(uint256(keccak256("rootstock.storage.ERC721Airdroppable")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant STORAGE_LOCATION =
0xb0b7c350b577073edef3635128030d229b37650766e59f19e264b4b50f30a500;
Expand All @@ -36,6 +41,11 @@ abstract contract ERC721UpgradableAirdroppable is ERC721UpgradableBase, IAirdrop
*/
function airdrop(AirdropRecipient[] calldata receivers) external virtual override onlyOwner {
AirdroppableStorage storage $ = _getStorage();

if ($._locked) {
revert AirdropMintingLocked(totalSupply());
}

uint256 tokenId = $._nextTokenId;
for (uint256 i = 0; i < receivers.length; i++) {
tokenId++;
Expand All @@ -46,4 +56,9 @@ abstract contract ERC721UpgradableAirdroppable is ERC721UpgradableBase, IAirdrop
$._nextTokenId = tokenId;
emit AirdropExecuted(receivers.length);
}

function lockNFTMinting() external onlyOwner {
_getStorage()._locked = true;
emit AirDropLocked(totalSupply());
}
}
51 changes: 51 additions & 0 deletions contracts/NFT/ExternalContributorsEcosystemPartner.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.20;

import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import {ERC721UpgradableAirdroppable} from "./ERC721UpgradableAirdroppable.sol";
import {ERC721UpgradableNonTransferrable} from "./ERC721UpgradableNonTransferrable.sol";

contract ExternalContributorsEcosystemPartner is
ERC721UpgradableAirdroppable,
ERC721UpgradableNonTransferrable
{
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

function initialize(address initialOwner) public initializer {
__ERC721UpgradableBase_init("OGExternalContributorsEcosystemPartner", "OGECEP", initialOwner);
}

function _authorizeUpgrade(address newImplementation) internal virtual override onlyOwner {}

function _baseURI() internal pure override returns (string memory) {
return "ipfs://";
}

/* Overrides required by Solidity */
function approve(
address to,
uint256 tokenId
) public virtual override(IERC721, ERC721Upgradeable, ERC721UpgradableNonTransferrable) {
super.approve(to, tokenId);
}

function setApprovalForAll(
address operator,
bool approved
) public virtual override(IERC721, ERC721Upgradeable, ERC721UpgradableNonTransferrable) {
super.setApprovalForAll(operator, approved);
}

function transferFrom(
address from,
address to,
uint256 tokenId
) public virtual override(IERC721, ERC721Upgradeable, ERC721UpgradableNonTransferrable) {
super.transferFrom(from, to, tokenId);
}
}
22 changes: 22 additions & 0 deletions ignition/modules/ExternalContributorsEcosystemPartner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { buildModule } from '@nomicfoundation/hardhat-ignition/modules'

export const extContributersEpProxyModule = buildModule('ExtContributorsEP', m => {
// deploy implementation
const implementation = m.contract('ExternalContributorsEcosystemPartner', [], { id: 'Implementation' })

const deployer = m.getAccount(0)
// deploy proxy
const proxy = m.contract('ERC1967Proxy', [
implementation,
m.encodeFunctionCall(implementation, 'initialize', [deployer], {
id: 'Proxy',
}),
])
const ExtContributorsEP = m.contractAt('ExternalContributorsEcosystemPartner', proxy, {
id: 'Contract',
})

return { ExtContributorsEP }
})

export default extContributersEpProxyModule
22 changes: 22 additions & 0 deletions params/ExtContributorsEP/airdrop-testnet.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"receiver": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"ipfsCid": "QmeGNJxhSToWYvHucUMX4D2fX8Uk2aL8CujwFuGw2Moy9U"
},
{
"receiver": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
"ipfsCid": "QmPFcgjgqCMoEbBmkbnvwaknENTH3WckjCGYekQhXJFVzn"
},
{
"receiver": "0x90F79bf6EB2c4f870365E785982E1f101E93b906",
"ipfsCid": "Qme6NCykZUUv78jXydTgmmehHThVXpJHFxAbZvEJkkPk5o"
},
{
"receiver": "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",
"ipfsCid": "QmUrqydS9kLYDJtv6dY7suhT5m5UPxJe1K1yq9b7QoY1VD"
},
{
"receiver": "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc",
"ipfsCid": "QmcbYfu8CuhHRfRmDHwc8P8vqpgnYSCuzLcwtr5n4afMKb"
}
]
118 changes: 118 additions & 0 deletions test/ExtContributorsEP.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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 airdropReceivers from '../params/ExtContributorsEP/airdrop-testnet.json'

describe('ExternalContributorsEcosystemPartner NFT', () => {
let deployer: SignerWithAddress
let alice: SignerWithAddress
const orgGangsters: SignerWithAddress[] = []
let extContEP: ExternalContributorsEcosystemPartner

before(async () => {
;[deployer, alice] = await ethers.getSigners()
const contract = await ignition.deploy(extContributersEpProxyModule)
extContEP = contract.ExtContributorsEP as unknown as ExternalContributorsEcosystemPartner
// impersonating airdrop receivers
for (let i = 0; i < airdropReceivers.length; i++) {
const accountAddr = airdropReceivers[i].receiver
await hre.network.provider.request({
method: 'hardhat_impersonateAccount',
params: [accountAddr],
})
const account = await ethers.getSigner(accountAddr)
orgGangsters.push(account)
}
})

describe('Upon deployment', () => {
it('should set up proper NFT name and symbol', async () => {
expect(await extContEP.connect(deployer).name()).to.equal("OGExternalContributorsEcosystemPartner")

Check warning on line 32 in test/ExtContributorsEP.test.ts

View workflow job for this annotation

GitHub Actions / test

Replace `"OGExternalContributorsEcosystemPartner"` with `'OGExternalContributorsEcosystemPartner'`
expect(await extContEP.symbol()).to.equal("OGECEP")

Check warning on line 33 in test/ExtContributorsEP.test.ts

View workflow job for this annotation

GitHub Actions / test

Replace `"OGECEP"` with `'OGECEP'`
})

it('should have zero total supply', async () => {
expect(await extContEP.totalSupply()).to.equal(0)
})

it('should have an owner', async () => {
expect(await extContEP.owner()).to.equal(deployer.address)
})
})

describe('Airdrop', () => {
it('should execute the initial airdrop after deployment', async () => {
await expect(extContEP.connect(deployer).airdrop(airdropReceivers))
.to.emit(extContEP, 'AirdropExecuted')
.withArgs(airdropReceivers.length)
})
it('the Gangsters should own NFTs after the airdrop', async () => {
await Promise.all(
orgGangsters.map(async (gangster, i) => {
expect(await extContEP.balanceOf(gangster.address)).to.equal(1)
// token IDs: 1, 2, 3...
expect(await extContEP.tokenOfOwnerByIndex(gangster.address, 0)).to.equal(i + 1)
}),
)
})
it('should top up total supply after the airdrop', async () => {
expect(await extContEP.totalSupply()).to.equal(airdropReceivers.length)
})
it('non-owner cannot execute airdrop', async () => {
await expect(extContEP.connect(alice).airdrop(airdropReceivers))
.to.be.revertedWithCustomError(extContEP, 'OwnableUnauthorizedAccount')
.withArgs(alice.address)
})
it('should execute the second airdrop to the same addresses', async () => {
await expect(extContEP.connect(deployer).airdrop(airdropReceivers))
.to.emit(extContEP, 'AirdropExecuted')
.withArgs(airdropReceivers.length)
})
it('the Gangsters should own 2 NFTs after the second airdrop', async () => {
await Promise.all(
orgGangsters.map(async (gangster, i) => {
const tokenId = airdropReceivers.length + i + 1
expect(await extContEP.balanceOf(gangster.address)).to.equal(2)
// token IDs: 6, 7, 8...
expect(await extContEP.tokenOfOwnerByIndex(gangster.address, 1)).to.equal(tokenId)
const cid = airdropReceivers[i].ipfsCid
expect(await extContEP.tokenURI(tokenId)).to.equal(`ipfs://${cid}`)
}),
)
})
})

describe('Transfer functionality is disabled', () => {
it('transfers should be forbidden after airdrop', async () => {
await Promise.all(
orgGangsters.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(
orgGangsters.map(async (sender, i) => {
await expect(

Check warning on line 101 in test/ExtContributorsEP.test.ts

View workflow job for this annotation

GitHub Actions / test

Replace `⏎············extContEP.connect(sender).approve(alice.address,·i·+·1),⏎··········).to.be.revertedWithCustomError(extContEP,·'TransfersDisabled'` with `extContEP.connect(sender).approve(alice.address,·i·+·1)).to.be.revertedWithCustomError(⏎············extContEP,⏎············'TransfersDisabled',⏎··········`
extContEP.connect(sender).approve(alice.address, i + 1),
).to.be.revertedWithCustomError(extContEP, 'TransfersDisabled')
}),
)
})

it('setApprovalForAll should be forbidden', async () => {
await Promise.all(
orgGangsters.map(async sender => {
await expect(
extContEP.connect(sender).setApprovalForAll(alice.address, true),
).to.be.revertedWithCustomError(extContEP, 'TransfersDisabled')
}),
)
})
})
})

0 comments on commit 2904e61

Please sign in to comment.