Skip to content

added ERC-721 FOO contract and tests #4

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
105 changes: 105 additions & 0 deletions contracts/Foo721.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.7;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract Foo721 is ERC721, ERC721Enumerable, Ownable {
using Counters for Counters.Counter;

Counters.Counter private _tokenIdCounter;

uint256 public constant MAX_SUPPLY = 100;
uint256 public constant FREE_MINT_COUNT = 5;
uint256 public constant MINT_PRICE = 0.001 ether;
// solhint-disable-next-line var-name-mixedcase
uint256 public immutable MINT_DATE;

mapping(address => bool) public freeMinted;

string public baseURI;

error InsufficientFunds();
error MaxSupplyExceeded();
error AlreadyFreeMinted();
error FreeMintExceeded();
error TransferTxError();
error InvalidDate();

event FreeMinted(address minter);

//solhint-disable-next-line
constructor(uint256 mintDate) ERC721("Foo721", "F721") {
MINT_DATE = mintDate;
}

function setBaseURI(string memory baseURI_) external onlyOwner {
baseURI = baseURI_;
}

function _baseURI() internal view override returns (string memory) {
return baseURI;
}

function mint(address to, uint256 quantity) external payable {
// solhint-disable-next-line not-rely-on-time
if (block.timestamp < MINT_DATE) revert InvalidDate();

if (totalSupply() + quantity > MAX_SUPPLY) revert MaxSupplyExceeded();

if (msg.value < MINT_PRICE * quantity) revert InsufficientFunds();

for (uint256 index = 0; index < quantity; ) {
_tokenIdCounter.increment();

_safeMint(to, _tokenIdCounter.current());

unchecked {
++index;
}
}
}

function freeMint(address to) external {
if (_tokenIdCounter.current() >= FREE_MINT_COUNT)
revert FreeMintExceeded();
if (freeMinted[to]) revert AlreadyFreeMinted();

freeMinted[to] = true;
_tokenIdCounter.increment();

_safeMint(to, _tokenIdCounter.current());

emit FreeMinted(to);
}

function withdraw() external {
// solhint-disable-next-line avoid-low-level-calls
(bool isSuccess, ) = payable(msg.sender).call{
value: address(this).balance
}("");

if (!isSuccess) revert TransferTxError();
}

// The following functions are overrides required by Solidity.
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal override(ERC721, ERC721Enumerable) {
ERC721Enumerable._beforeTokenTransfer(from, to, tokenId);
}

function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable)
returns (bool)
{
return ERC721Enumerable.supportsInterface(interfaceId);
}
}
124 changes: 124 additions & 0 deletions test/Foo721.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import { expect, assert } from 'chai';
import { Contract, ContractFactory, constants } from 'ethers';
import { ethers } from 'hardhat';

const name: string = 'Foo721';

describe(name, () => {
let contract: Contract;
let owner: SignerWithAddress;
let addresses: SignerWithAddress[];
let factory: ContractFactory;
const MINT_PRICE = ethers.utils.parseEther('0.001');

// hooks
before(async () => {
[owner, ...addresses] = await ethers.getSigners();
factory = await ethers.getContractFactory(name);
});

beforeEach(async () => {
const now = Math.floor(+new Date() / 1000);
contract = await factory.deploy(now + 750);
});

// mint tests
it('should MINT successfully', async () => {
await ethers.provider.send('evm_increaseTime', [1000]);

await expect(
contract.mint(addresses[0].address, 1, { value: String(MINT_PRICE) })
)
.to.emit(contract, 'Transfer')
.withArgs(constants.AddressZero, addresses[0].address, 1);

await ethers.provider.send('evm_increaseTime', [-1000]);
});

it('should not MINT if max supply exceeds', async () => {
await ethers.provider.send('evm_increaseTime', [1000]);

await contract.mint(addresses[5].address, 100, {
value: String(MINT_PRICE.mul(100)),
});

await expect(
contract.mint(addresses[7].address, 1, {
value: String(MINT_PRICE.mul(1)),
})
).to.be.revertedWith('MaxSupplyExceeded');
await ethers.provider.send('evm_increaseTime', [-1000]);
});

it('should not MINT if InsufficientFunds', async () => {
await ethers.provider.send('evm_increaseTime', [1000]);

await expect(contract.mint(addresses[0].address, 1)).to.be.revertedWith(
'InsufficientFunds'
);

await ethers.provider.send('evm_increaseTime', [-1000]);
});

it('should Free Mint', async () => {
await ethers.provider.send('evm_increaseTime', [1000]);

await expect(contract.freeMint(addresses[0].address))
.to.emit(contract, 'Transfer')
.withArgs(constants.AddressZero, addresses[0].address, 1);

await ethers.provider.send('evm_increaseTime', [-1000]);
});

it('should Not Free Mint if already Minted', async () => {
await ethers.provider.send('evm_increaseTime', [1000]);

await contract.freeMint(addresses[0].address);

await expect(contract.freeMint(addresses[0].address)).to.be.revertedWith(
'AlreadyFreeMinted'
);

await ethers.provider.send('evm_increaseTime', [-1000]);
});

it('should Not Free Mint if FreeMintExceeded', async () => {
await ethers.provider.send('evm_increaseTime', [1000]);

for (let index = 0; index < 5; index++) {
await contract.freeMint(addresses[index].address);
}

await expect(contract.freeMint(addresses[6].address)).to.be.revertedWith(
'FreeMintExceeded'
);

await ethers.provider.send('evm_increaseTime', [-1000]);
});

// withdraw tests
it('should withdraw eth successfully', async () => {
await ethers.provider.send('evm_increaseTime', [1000]);

await contract.mint(addresses[5].address, 1, {
value: String(MINT_PRICE.mul(1)),
});

const beforeBalance = await ethers.provider.getBalance(owner.address);

const tx = await contract.withdraw();

const afterBalance = await ethers.provider.getBalance(owner.address);

const receipt = await tx.wait();

const fee = receipt.effectiveGasPrice * receipt.gasUsed;

assert.equal(
(await beforeBalance).add(MINT_PRICE).sub(fee).toString(),
afterBalance.toString()
);
await ethers.provider.send('evm_increaseTime', [-1000]);
});
});