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

feat: implement betabuilders nft sc #103

Merged
merged 2 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
48 changes: 48 additions & 0 deletions contracts/NFT/BetaBuildersRootstockCollective.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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 {ERC721AirdroppableUpgradable} from "./ERC721AirdroppableUpgradable.sol";
import {ERC721NonTransferrableUpgradable} from "./ERC721NonTransferrableUpgradable.sol";

contract BetaBuildersRootstockCollective is ERC721AirdroppableUpgradable, ERC721NonTransferrableUpgradable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

function initialize(address initialOwner) public initializer {
__ERC721UpgradableBase_init("BetaBuildersRootstockCollective", "BB", 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, ERC721NonTransferrableUpgradable) {
super.approve(to, tokenId);
}

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

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

export const betaBuildersModule = buildModule('BetaBuilders', m => {
// deploy implementation
const implementation = m.contract('BetaBuildersRootstockCollective', [], { id: 'Implementation' })

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

return { ExtBetaBuildersEP }
})

export default betaBuildersModule
22 changes: 22 additions & 0 deletions params/BetaBuildersModule/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/BetaBuilders.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 { BetaBuildersRootstockCollective } from '../typechain-types'
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'
import { betaBuildersModule } from '../ignition/modules/BetaBuildersModule'
import airdropReceivers from '../params/BetaBuildersModule/airdrop-testnet.json'

describe('BetaBuildersRootstockCollective NFT', () => {
let deployer: SignerWithAddress
let alice: SignerWithAddress
const oldGangsters: SignerWithAddress[] = []
let betaBuildersEP: BetaBuildersRootstockCollective

before(async () => {
;[deployer, alice] = await ethers.getSigners()
const contract = await ignition.deploy(betaBuildersModule)
betaBuildersEP = contract.ExtBetaBuildersEP as unknown as BetaBuildersRootstockCollective
// 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)
oldGangsters.push(account)
}
})

describe('Upon deployment', () => {
it('should set up proper NFT name and symbol', async () => {
expect(await betaBuildersEP.connect(deployer).name()).to.equal('BetaBuildersRootstockCollective')
expect(await betaBuildersEP.symbol()).to.equal('BB')
})

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

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

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

describe('Transfer functionality is disabled', () => {
it('transfers should be forbidden after airdrop', async () => {
await Promise.all(
oldGangsters.map(async (sender, i) => {
await expect(
betaBuildersEP.connect(sender).transferFrom(sender.address, alice.address, i + 1),
).to.be.revertedWithCustomError(betaBuildersEP, 'TransfersDisabled')
}),
)
})

it('approvals should be forbidden', async () => {
await Promise.all(
oldGangsters.map(async (sender, i) => {
await expect(
betaBuildersEP.connect(sender).approve(alice.address, i + 1),
).to.be.revertedWithCustomError(betaBuildersEP, 'TransfersDisabled')
}),
)
})

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