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(taikoon): migrate Taikoon NFT smart contracts here #16849

Merged
merged 26 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1992970
brought over contracts, tests and scripts; getting everything to run now
bearni95 Apr 25, 2024
b9b56ba
refreshed gitignore
bearni95 Apr 25, 2024
570e770
removed lib from tree
bearni95 Apr 25, 2024
175e4a4
tracking pnpm-lock.yaml main
bearni95 Apr 25, 2024
2d89594
missing deps added to enable compiling
bearni95 Apr 25, 2024
d3335f1
format & mint
bearni95 Apr 25, 2024
cb4baca
cleared lint warnings
bearni95 Apr 25, 2024
d44c463
updated pnpm; passing tests and build
bearni95 Apr 25, 2024
6817f27
removed .gitmodules
bearni95 Apr 25, 2024
71eaf4c
fixed typo leafs > leaves
bearni95 Apr 25, 2024
2442adc
added build step to gha
bearni95 Apr 25, 2024
7a522a2
fixed merkle whitelist generation script under new location
bearni95 Apr 25, 2024
5f55717
localhost deployment re-enabled
bearni95 Apr 25, 2024
30ec660
added holesky deployment
bearni95 Apr 25, 2024
311d129
restored ipfs deploy script
bearni95 Apr 25, 2024
c7e5731
updated pnpm-lock.yaml
bearni95 Apr 25, 2024
3e2099f
ran linter
bearni95 Apr 25, 2024
8b42156
simplified tests for taikoonToken.t.sol
bearni95 Apr 25, 2024
ae80196
updated forge-std version
bearni95 Apr 25, 2024
9e6f6e9
added compile step to gha
bearni95 Apr 25, 2024
bacd1d1
fixed gha typo
bearni95 Apr 25, 2024
3bd7dad
upgraded foundry toolchain version on gha
bearni95 Apr 25, 2024
ae94276
upgraded foundry toolchain version on gha
bearni95 Apr 25, 2024
dee3ea0
incorporated upgradeable constructor
bearni95 Apr 25, 2024
3217861
updated test command on gha
bearni95 Apr 25, 2024
7e0753e
fixed test script
bearni95 Apr 25, 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
15 changes: 15 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[submodule "packages/taikoon/lib/forge-std"]
path = packages/taikoon/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "packages/taikoon/lib/openzeppelin-contracts"]
path = packages/taikoon/lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "packages/taikoon/lib/openzeppelin-contracts-upgradeable"]
path = packages/taikoon/lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
[submodule "packages/taikoon/lib/murky"]
path = packages/taikoon/lib/murky
url = https://github.com/dmfxyz/murky
[submodule "packages/taikoon/lib/openzeppelin-foundry-upgrades"]
path = packages/taikoon/lib/openzeppelin-foundry-upgrades
url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades
11 changes: 11 additions & 0 deletions packages/taikoon/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
LOCALHOST_PRIVATE_KEY=
LOCALHOST_ADDRESS=
HOLESKY_PRIVATE_KEY=
HOLESKY_ADDRESS=
DEVNET_PRIVATE_KEY=
DEVNET_ADDRESS=
SEPOLIA_PRIVATE_KEY=
SEPOLIA_ADDRESS=
KATLA_PRIVATE_KEY=
KATLA_ADDRESS=
IPFS_BASE_URI=
10 changes: 10 additions & 0 deletions packages/taikoon/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,13 @@ broadcast

#coverage
lcov.info

# Custom
broadcast
.states
coverage
data/original
data/images
data/metadata
lcov.info
lib
74 changes: 74 additions & 0 deletions packages/taikoon/contracts/MerkleWhitelist.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import { ContextUpgradeable } from
"@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";

/// @title MerkleWhitelist
/// @dev Merkle Tree Whitelist
/// @custom:security-contact [email protected]
contract MerkleWhitelist is ContextUpgradeable {
event RootUpdated(bytes32 _root);
event MintConsumed(address _minter, uint256 _mintAmount);

error MINTS_EXCEEDED();
error INVALID_PROOF();
error INVALID_TOKEN_AMOUNT();

/// @notice Merkle Tree Root
bytes32 public root;
/// @notice Tracker for minted leaves
mapping(bytes32 leaf => bool hasMinted) public minted;

uint256[48] private __gap;

/// @notice Contract initializer
/// @param _root Merkle Tree root
function initialize(bytes32 _root) external initializer {
__Context_init();
root = _root;
}

/// @notice Check if a wallet can free mint
/// @param _minter Address of the minter
/// @param _maxMints Max amount of free mints
/// @return Whether the wallet can mint
function canMint(address _minter, uint256 _maxMints) public view returns (bool) {
bytes32 _leaf = leaf(_minter, _maxMints);
return !minted[_leaf];
}

/// @notice Generate a leaf from the minter and mint counts
/// @param _minter Address of the minter
/// @param _maxMints Max amount of free mints
/// @return The leaf hash
function leaf(address _minter, uint256 _maxMints) public pure returns (bytes32) {
return keccak256(bytes.concat(keccak256(abi.encode(_minter, _maxMints))));
}

/// @notice Internal initializer
/// @param _root Merkle Tree root
function __MerkleWhitelist_init(bytes32 _root) internal initializer {
__Context_init();
root = _root;
}

/// @notice Update the merkle tree's root
/// @param _root The new root
function _updateRoot(bytes32 _root) internal {
root = _root;
emit RootUpdated(_root);
}

/// @notice Permanently consume mints from the minter
/// @param _proof Merkle proof
/// @param _maxMints Max amount of free mints
function _consumeMint(bytes32[] calldata _proof, uint256 _maxMints) internal {
if (!canMint(_msgSender(), _maxMints)) revert MINTS_EXCEEDED();
bytes32 _leaf = leaf(_msgSender(), _maxMints);
if (!MerkleProof.verify(_proof, root, _leaf)) revert INVALID_PROOF();
minted[_leaf] = true;
emit MintConsumed(_msgSender(), _maxMints);
}
}
120 changes: 117 additions & 3 deletions packages/taikoon/contracts/TaikoonToken.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,118 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.24;
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

contract TaikoonToken { }
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { Ownable2StepUpgradeable } from
"@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import { ERC721EnumerableUpgradeable } from
"@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
import { UUPSUpgradeable } from
"@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

import { MerkleWhitelist } from "./MerkleWhitelist.sol";

/// @title TaikoonToken
/// @dev The Taikoons ERC-721 token
/// @custom:security-contact [email protected]
contract TaikoonToken is
ERC721EnumerableUpgradeable,
UUPSUpgradeable,
Ownable2StepUpgradeable,
MerkleWhitelist
{
/// @notice The current supply
uint256 private _totalSupply;
// Base URI required to interact with IPFS
string private _baseURIExtended;

uint256[48] private __gap;

error MAX_MINTS_EXCEEDED();
error MAX_SUPPLY_REACHED();
error MINTER_NOT_WHITELISTED();
error TOKEN_NOT_MINTED();

/// @notice Contract initializer
/// @param _rootURI Base URI for the token metadata
/// @param _merkleRoot Merkle tree root for the whitelist
function initialize(string memory _rootURI, bytes32 _merkleRoot) external initializer {
__ERC721_init("Taikoon", "TKOON");
__Ownable_init(_msgSender());
__MerkleWhitelist_init(_merkleRoot);
_baseURIExtended = _rootURI;
}

/// @notice Update the whitelist's merkle root
/// @param _root New merkle root
function updateRoot(bytes32 _root) external onlyOwner {
_updateRoot(_root);
}

/// @notice Mint a token, handling the free vs paid internally
/// @param _proof Merkle proof validating the minter
/// @param _maxMints The amount of tokens to mint
/// @return tokenIds The minted token ids
function mint(
bytes32[] calldata _proof,
uint256 _maxMints
)
external
returns (uint256[] memory)
{
if (!canMint(_msgSender(), _maxMints)) revert MINTER_NOT_WHITELISTED();

_consumeMint(_proof, _maxMints);
return _batchMint(_msgSender(), _maxMints);
}

/// @notice Mint method for the owner
/// @param _to The address to mint to
/// @param _amount The amount of tokens to mint
/// @return tokenIds The minted token ids
function mint(address _to, uint256 _amount) external onlyOwner returns (uint256[] memory) {
return _batchMint(_to, _amount);
}

/// @notice Get the tokenURI of a particular tokenId
/// @param _tokenId The token ID
/// @return The token URI
function tokenURI(uint256 _tokenId) public view override returns (string memory) {
if (_tokenId > _totalSupply) revert TOKEN_NOT_MINTED();
return string.concat(_baseURI(), "/", Strings.toString(_tokenId), ".json");
}

/// @notice Get the current total supply
/// @return The total supply
function totalSupply() public view override returns (uint256) {
return _totalSupply;
}

/// @notice Get the max supply of Taikoons
/// @return The max supply
function maxSupply() public pure returns (uint256) {
return 888;
}
/// @notice Calculate the amount of free and paid mints
/// @return The base URI for the token metadata

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

/// @notice Internal method to authorize an upgrade
function _authorizeUpgrade(address) internal virtual override onlyOwner { }

/// @notice Internal method to batch mint tokens
/// @param _to The address to mint to
/// @param _amount The amount of tokens to mint
/// @return tokenIds The minted token ids
function _batchMint(address _to, uint256 _amount) private returns (uint256[] memory tokenIds) {
if (_totalSupply + _amount > maxSupply()) revert MAX_SUPPLY_REACHED();
tokenIds = new uint256[](_amount);

for (uint256 i; i < _amount; ++i) {
tokenIds[i] = ++_totalSupply;
_mint(_to, tokenIds[i]);
}
}
}
95 changes: 95 additions & 0 deletions packages/taikoon/data/script/add-images-ipfs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
const path = require('path')
const fs = require('fs')
const fsPromises = fs.promises

// Helper function to form the metadata JSON object
function populateNFTMetadata(name, description, CID) {
return {
name,
description,
image: CID,
}
}

async function main() {
console.log(`Configuring the IPFS instance...`)
const { create } = await import('ipfs-http-client')
const ipfs = create()
const endpointConfig = await ipfs.getEndpointConfig()
console.log(`IPFS configured to connect via: `)
console.debug(endpointConfig)
console.log(` `)

// Get the images to upload from the local filesystem (/images)
console.log(`Importing images from the images/ directory...`)
const imgDirPath = path.join(path.resolve(__dirname, '..'), 'images')
const filesName = await fsPromises.readdir(imgDirPath, (err) => {
if (err) {
console.log('Import from directory failed: ', err)
}
})
const imagesName = filesName.filter((fileName) => fileName.includes('.png'))
let imagesData = []
for await (const imageName of imagesName) {
let imageFilePath = path.join(path.resolve(__dirname, '..'), 'images', imageName)
let imageData = await fsPromises.readFile(imageFilePath)
imagesData.push(imageData)
}
console.log(`Imported images as buffered data\n`)

// Uploading images to IPFS
console.log(`Uploading image data to IPFS...`)
let imageCIDs = []
let imagesSummary = []
let imageCount = 1
for await (const imageData of imagesData) {
let { cid: imageCID } = await ipfs.add({
content: imageData,
})
imageCIDs.push(imageCID)
imagesSummary.push({ imageCID, imageCount })
console.log(`Image added to IPFS with CID of ${imageCID}`)
imageCount++
}
console.log(` `)



// Add the metadata to IPFS
console.log(`Adding metadata to IPFS...`);
let metadataCIDs = [];
let taikoonId = 0
for await (const imageCID of imageCIDs) {
taikoonId++
const {cid: metadataCID} = await ipfs.add({
// NOTE: You can implement different name & descriptions for each metadata
content: JSON.stringify(populateNFTMetadata(`Taikoon ${taikoonId}`, "A Taikoon", imageCID.toString()))
})

// write into a file
fs.writeFileSync(
path.join(path.resolve(__dirname, '..'), 'metadata', `${taikoonId}.json`),
JSON.stringify(populateNFTMetadata(`Taikoon ${taikoonId}`, "A Taikoon", imageCID.toString()))
)


console.log(path.join(path.resolve(__dirname, '..'), 'metadata', `${taikoonId}.json`))
metadataCIDs.push(metadataCID);
for (let i = 0; i < imagesSummary.length; i ++) {
if (imagesSummary[i].imageCID == imageCID) {
imagesSummary[i].metadataCID = metadataCID
}
};
//console.log(`Metadata with image CID ${imageCID} added to IPFS with CID of ${metadataCID}`);
}
console.log(` `);

fs.writeFileSync(
path.join(path.resolve(__dirname, '..'), 'metadata', 'summary.json'),
JSON.stringify({imagesSummary})
)


}

main()
35 changes: 35 additions & 0 deletions packages/taikoon/data/script/generate-merkle-tree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const { StandardMerkleTree } = require('@openzeppelin/merkle-tree')
const path = require('path')
const fs = require('fs')
const ConvertCsvToJson = require('convert-csv-to-json');

async function main(network) {
const inputFile = path.join(__dirname, `../whitelist/${network}.csv`)
const outputFile = path.join(__dirname, `../whitelist/${network}.json`)
const rawJson = ConvertCsvToJson.fieldDelimiter(',').getJsonFromCsv(inputFile);
//console.log(rawJson)

const values = rawJson.map((entry) => {
return [
entry.address,
entry.freeMints,
]
})

const tree = StandardMerkleTree.of(
values,
["address", "uint256"]
)

fs.writeFileSync(
outputFile,
JSON.stringify(
{...tree.dump(),
root: tree.root
}, null, 2)
)

console.log('Merkle Root:', tree.root)
}

main('hardhat')
Loading
Loading