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(nfts): taikoons upgradeable v2 #17580

Merged
merged 11 commits into from
Jun 14, 2024
136 changes: 136 additions & 0 deletions packages/nfts/contracts/taikoon/TaikoonTokenV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";

import { ERC721EnumerableUpgradeable } from
"@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";

import { MerkleWhitelist } from "./MerkleWhitelist.sol";
import { IMinimalBlacklist } from "@taiko/blacklist/IMinimalBlacklist.sol";

/// @title TaikoonToken
/// @dev The Taikoons V1 ERC-721 token
/// @custom:security-contact [email protected]
contract TaikoonTokenV2 is ERC721EnumerableUpgradeable, MerkleWhitelist {
bearni95 marked this conversation as resolved.
Show resolved Hide resolved
/// @notice The current supply
uint256 private _totalSupply;
/// @notice Base URI required to interact with IPFS
string private _baseURIExtended;
/// @notice Gap for upgrade safety
uint256[47] 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(
address _owner,
string memory _rootURI,
bytes32 _merkleRoot,
IMinimalBlacklist _blacklistAddress
)
external
initializer
{
__ERC721_init("Taikoon", "TKOON");
__MerkleWhitelist_init(_owner, _merkleRoot, _blacklistAddress);
_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 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]);
}
}

/// @dev V2 code additions

/// @notice Contract initializer
/// @param _rootURI Base URI for the token metadata
function initializeV2(string memory _rootURI) external {
_baseURIExtended = _rootURI;
}

/// @notice Update the base URI
/// @param _rootURI The new base URI
/// @dev Only the owner can update the base URI
function updateBaseURI(string memory _rootURI) public onlyOwner {
_baseURIExtended = _rootURI;
}

/// @notice Get the base URI
/// @return The base URI
function baseURI() public view returns (string memory) {
return _baseURIExtended;
}
}
1 change: 1 addition & 0 deletions packages/nfts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ remappings = [
"murky/=node_modules/murky/src/",
"solidity-stringutils/=node_modules/solidity-stringutils/",
"@taiko/blacklist/=node_modules/@taiko/supplementary-contracts/contracts/blacklist/",
"openzeppelin-foundry-upgrades/=node_modules/openzeppelin-foundry-upgrades/src/",
]

# Do not change the block_gas_limit value, TaikoL2.t.sol depends on it.
Expand Down
4 changes: 2 additions & 2 deletions packages/nfts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@openzeppelin/contracts": "5.0.2",
"@openzeppelin/contracts-upgradeable": "5.0.2",
"@openzeppelin/merkle-tree": "^1.0.6",
"@taiko/supplementary-contracts": "workspace:*",
"convert-csv-to-json": "^2.46.0",
"dotenv": "^16.4.5",
"ds-test": "github:dapphub/ds-test#e282159d5170298eb2455a6c05280ab5a73a4ef0",
Expand All @@ -55,7 +56,6 @@
"p256-verifier": "github:taikoxyz/p256-verifier#v0.1.0",
"sharp": "^0.33.3",
"solady": "github:Vectorized/solady#v0.0.167",
"solidity-stringutils": "github:Arachnid/solidity-stringutils",
"@taiko/supplementary-contracts": "workspace:*"
"solidity-stringutils": "github:Arachnid/solidity-stringutils"
}
}
63 changes: 47 additions & 16 deletions packages/nfts/test/taikoon/Upgradeable.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ pragma solidity 0.8.24;

import { Test, console } from "forge-std/src/Test.sol";
import { TaikoonToken } from "../../contracts/taikoon/TaikoonToken.sol";
import { TaikoonTokenV2 } from "../../contracts/taikoon/TaikoonTokenV2.sol";
import { Merkle } from "murky/Merkle.sol";
import "forge-std/src/StdJson.sol";
import { UtilsScript } from "../../script/taikoon/sol/Utils.s.sol";
import { MockBlacklist } from "../util/Blacklist.sol";

import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import { ITransparentUpgradeableProxy } from
"@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract UpgradeableTest is Test {
Expand All @@ -16,6 +21,7 @@ contract UpgradeableTest is Test {
UtilsScript public utils;

TaikoonToken public token;
TaikoonTokenV2 public tokenV2;

address public owner = vm.addr(0x5);

Expand All @@ -34,26 +40,51 @@ contract UpgradeableTest is Test {
blacklist = new MockBlacklist();

// create whitelist merkle tree
vm.startPrank(owner);
vm.startBroadcast(owner);
bytes32 root = tree.getRoot(leaves);

// deploy token with empty root
address impl = address(new TaikoonToken());
address proxy = address(
new ERC1967Proxy(
impl,
abi.encodeCall(TaikoonToken.initialize, (address(0), "ipfs://", root, blacklist))
)
token = new TaikoonToken();
address impl = address(token);

ERC1967Proxy proxy = new ERC1967Proxy(
impl, abi.encodeCall(TaikoonToken.initialize, (owner, "ipfs://", root, blacklist))
);
token = TaikoonToken(address(proxy));

// mint tokens on the v1 deployment
token.mint(minters[0], 5);

// upgrade to v2

token.upgradeToAndCall(
address(new TaikoonTokenV2()),
abi.encodeCall(TaikoonTokenV2.initializeV2, ("ipfs://v2//"))
);

tokenV2 = TaikoonTokenV2(address(proxy));

vm.stopBroadcast();
}

function test_upgraded_v2() public view {
assertEq(tokenV2.name(), token.name());
assertEq(tokenV2.symbol(), token.symbol());
assertEq(tokenV2.totalSupply(), token.totalSupply());
assertEq(tokenV2.maxSupply(), token.maxSupply());
}

function test_tokenURI() public view {
assertEq(tokenV2.baseURI(), "ipfs://v2//");
string memory uri = tokenV2.tokenURI(0);
assertEq(uri, "ipfs://v2///0.json");
}

function test_updateBaseURI() public {
vm.startBroadcast(owner);
tokenV2.updateBaseURI("ipfs://test//");
vm.stopBroadcast();

token = TaikoonToken(proxy);
// use the token to calculate leaves
for (uint256 i = 0; i < minters.length; i++) {
leaves[i] = token.leaf(minters[i], FREE_MINTS);
}
// update the root
root = tree.getRoot(leaves);
token.updateRoot(root);
vm.stopPrank();
assertEq(tokenV2.baseURI(), "ipfs://test//");
}
}