Skip to content

Commit

Permalink
feat(nfts): kbw party ticket smart contracts (#17808)
Browse files Browse the repository at this point in the history
Co-authored-by: Kenk <kenghin_lim@hotmail.com>
  • Loading branch information
bearni95 and 2manslkh authored Aug 12, 2024
1 parent 90bc01d commit 99ec5cb
Show file tree
Hide file tree
Showing 12 changed files with 734 additions and 1 deletion.
294 changes: 294 additions & 0 deletions packages/nfts/contracts/party-ticket/TaikoPartyTicket.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Pausable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import { ERC721EnumerableUpgradeable } from
"@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
import { AccessControlUpgradeable } from
"@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import { PausableUpgradeable } from
"@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import { IMinimalBlacklist } from "@taiko/blacklist/IMinimalBlacklist.sol";
import { UUPSUpgradeable } from
"@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { Ownable2StepUpgradeable } from
"@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";

/// @title TaikoPartyTicket
/// @dev ERC-721 KBW Raffle & Party Tickets
/// @custom:security-contact security@taiko.xyz
contract TaikoPartyTicket is
ERC721EnumerableUpgradeable,
PausableUpgradeable,
UUPSUpgradeable,
Ownable2StepUpgradeable,
AccessControlUpgradeable
{
event BlacklistUpdated(address _blacklist);

/// @notice Owner role
bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE");
/// @notice Mint fee
uint256 public mintFee;
/// @notice Mint active flag
bool public mintActive;
/// @notice Token ID to winner mapping
mapping(uint256 tokenId => bool isWinner) public winners;
/// @notice Base URI required to interact with IPFS
string public baseURI;
/// @notice Winner base URI required to interact with IPFS
string public winnerBaseURI;
/// @notice Payout address
address public payoutAddress;
/// @notice Internal counter for token IDs
uint256 private _nextTokenId;
/// @notice Blackist address
IMinimalBlacklist public blacklist;
/// @notice Convenience array for winners
uint256[] public winnerIds;
/// @notice Gap for upgrade safety
uint256[42] private __gap;

error INSUFFICIENT_MINT_FEE();
error CANNOT_REVOKE_NON_WINNER();
error ADDRESS_BLACKLISTED();

/// @notice Contract initializer
/// @param _payoutAddress The address to receive mint fees
/// @param _mintFee The fee to mint a ticket
/// @param _baseURI Base URI for the token metadata pre-raffle
/// @param _blacklistAddress The address of the blacklist contract
function initialize(
address _payoutAddress,
uint256 _mintFee,
string memory _baseURI,
IMinimalBlacklist _blacklistAddress
)
external
initializer
{
__ERC721_init("TaikoPartyTicket", "TPT");
__Context_init();
mintFee = _mintFee;
baseURI = _baseURI;
payoutAddress = _payoutAddress;
blacklist = _blacklistAddress;

_grantRole(DEFAULT_ADMIN_ROLE, _msgSender());
_grantRole(OWNER_ROLE, _payoutAddress);

_transferOwnership(_msgSender());
}

/// @notice Modifier to check if an address is blacklisted
/// @param _address The address to check
modifier notBlacklisted(address _address) {
if (blacklist.isBlacklisted(_address)) revert ADDRESS_BLACKLISTED();
_;
}

/// @notice Update the blacklist address
/// @param _blacklist The new blacklist address
function updateBlacklist(IMinimalBlacklist _blacklist) external onlyRole(DEFAULT_ADMIN_ROLE) {
blacklist = _blacklist;
emit BlacklistUpdated(address(_blacklist));
}

/// @notice Get individual token's URI
/// @param tokenId The token ID
/// @return The token URI
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
if (winners[tokenId]) {
return string(abi.encodePacked(baseURI, "/winner.json"));
} else if (paused()) {
return string(abi.encodePacked(baseURI, "/loser.json"));
}
return string(abi.encodePacked(baseURI, "/raffle.json"));
}

/// @notice Checks if a tokenId is a winner
/// @param tokenId The token ID
/// @return Whether the token is a winner
function isWinner(uint256 tokenId) public view returns (bool) {
return winners[tokenId];
}

/// @notice Checks if an address is a winner
/// @param minter The address to check
/// @return Whether the address is a winner
function isWinner(address minter) public view returns (bool) {
for (uint256 i = 0; i < balanceOf(minter); i++) {
if (winners[tokenOfOwnerByIndex(minter, i)]) {
return true;
}
}
return false;
}

/// @notice Set the winners
/// @param _winners The list of winning token ids
function setWinners(uint256[] calldata _winners)
external
whenNotPaused
onlyRole(DEFAULT_ADMIN_ROLE)
{
for (uint256 i = 0; i < _winners.length; i++) {
winners[_winners[i]] = true;
winnerIds.push(_winners[i]);
}
pause();
}

/// @notice Set the base URI
/// @param _baseURI The new base URI
function setBaseURI(string memory _baseURI) external onlyRole(DEFAULT_ADMIN_ROLE) {
baseURI = _baseURI;
}

/// @notice Set the winner base URI
/// @param _winnerBaseURI The new winner base URI
function setWinnerURI(string memory _winnerBaseURI) external onlyRole(DEFAULT_ADMIN_ROLE) {
winnerBaseURI = _winnerBaseURI;
}

/// @notice Mint a raffle ticket
/// @dev Requires a fee to mint
/// @dev Requires the contract to not be paused
function mint() external payable whenNotPaused notBlacklisted(_msgSender()) {
if (msg.value < mintFee) revert INSUFFICIENT_MINT_FEE();
uint256 tokenId = _nextTokenId++;
_safeMint(msg.sender, tokenId);
}

/// @notice Mint multiple raffle tickets
/// @param amount The number of tickets to mint
/// @dev Requires a fee to mint
/// @dev Requires the contract to not be paused
function mint(uint256 amount) external payable whenNotPaused notBlacklisted(_msgSender()) {
if (msg.value < mintFee * amount) revert INSUFFICIENT_MINT_FEE();
for (uint256 i = 0; i < amount; i++) {
uint256 tokenId = _nextTokenId++;
_safeMint(msg.sender, tokenId);
}
}

/// @notice Mint a raffle ticket
/// @param to The address to mint to
/// @dev Requires the contract to not be paused
/// @dev Can only be called by the admin
function mint(address to)
public
whenNotPaused
onlyRole(DEFAULT_ADMIN_ROLE)
notBlacklisted(to)
{
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
}

/// @notice Mint a winner ticket
/// @param to The address to mint to
/// @dev Requires calling as an admin
function mintWinner(address to) public onlyRole(DEFAULT_ADMIN_ROLE) notBlacklisted(to) {
uint256 tokenId = _nextTokenId++;
winners[tokenId] = true;
_safeMint(to, tokenId);
}

/// @notice Revoke a winner's status
/// @param tokenId The ID of the winner to revoke
function revokeWinner(uint256 tokenId) public onlyRole(DEFAULT_ADMIN_ROLE) {
winners[tokenId] = false;

for (uint256 i = 0; i < winnerIds.length; i++) {
if (winnerIds[i] == tokenId) {
winnerIds[i] = winnerIds[winnerIds.length - 1];
winnerIds.pop();
break;
}
}
}

/// @notice Revoke a winner's status
/// @param tokenIds The IDs of the winner to revoke
function revokeWinners(uint256[] calldata tokenIds)
external
whenPaused
onlyRole(DEFAULT_ADMIN_ROLE)
{
for (uint256 i = 0; i < tokenIds.length; i++) {
revokeWinner(tokenIds[i]);
}
}

/// @notice Revoke a winner and replace with a new winner
/// @param revokeId The ID of the winner to revoke
/// @param newWinnerId The ID of the new winner
function revokeAndReplaceWinner(
uint256 revokeId,
uint256 newWinnerId
)
external
whenPaused
onlyRole(DEFAULT_ADMIN_ROLE)
{
if (!winners[revokeId]) revert CANNOT_REVOKE_NON_WINNER();
revokeWinner(revokeId);
winners[newWinnerId] = true;
winnerIds.push(newWinnerId);
}

/// @notice Pause the contract
/// @dev Can only be called by the admin
function pause() public onlyRole(DEFAULT_ADMIN_ROLE) {
_pause();
}

/// @notice Unpause the contract
/// @dev Can only be called by the admin
function unpause() public onlyRole(DEFAULT_ADMIN_ROLE) {
_unpause();
}

/// @notice Withdraw the contract balance
/// @dev Can only be called by the admin
/// @dev Requires the contract to be paused
function withdraw() external whenPaused onlyRole(DEFAULT_ADMIN_ROLE) {
payable(payoutAddress).transfer(address(this).balance);
}

/// @notice Get the winner token IDs
/// @return The winner token IDs
function getWinnerTokenIds() public view whenPaused returns (uint256[] memory) {
return winnerIds;
}

/// @notice Get the winner addresses
/// @return _winners The winner addresses
function getWinners() public view whenPaused returns (address[] memory _winners) {
_winners = new address[](winnerIds.length);
for (uint256 i = 0; i < winnerIds.length; i++) {
_winners[i] = ownerOf(winnerIds[i]);
}
return _winners;
}

/// @notice supportsInterface implementation
/// @param interfaceId The interface ID
/// @return Whether the interface is supported
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721EnumerableUpgradeable, AccessControlUpgradeable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}

/// @notice Internal method to authorize an upgrade
function _authorizeUpgrade(address) internal virtual override onlyOwner { }
}
5 changes: 5 additions & 0 deletions packages/nfts/data/party-token/metadata/loser.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "[L] KBW Party Raffle Ticket",
"description": "A raffle ticket for the KBW Party. This ticket won nothing at the raffle.",
"image": "https://taikonfts.4everland.link/ipfs/bafybeibmuovja47ghveaiosvz442lkjb5oduiebydsgwob7r2xzm7d3yne/loser.png"
}
5 changes: 5 additions & 0 deletions packages/nfts/data/party-token/metadata/raffle.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "KBW Party Raffle Ticket",
"description": "A raffle ticket for the KBW Party. This ticket gives you a chance to win a special prize.",
"image": "https://taikonfts.4everland.link/ipfs/bafybeibmuovja47ghveaiosvz442lkjb5oduiebydsgwob7r2xzm7d3yne/raffle.png"
}
6 changes: 6 additions & 0 deletions packages/nfts/data/party-token/metadata/winner.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "[W] KBW Party Raffle Ticket",
"description": "A raffle ticket for the KBW Party. This ticket won a special prize at the raffle.",
"image": "https://taikonfts.4everland.link/ipfs/bafybeibmuovja47ghveaiosvz442lkjb5oduiebydsgwob7r2xzm7d3yne/winner.gif",
"animation_url": "https://taikonfts.4everland.link/ipfs/bafybeibmuovja47ghveaiosvz442lkjb5oduiebydsgwob7r2xzm7d3yne/winner.gif"
}
Binary file added packages/nfts/data/party-token/static/loser.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/nfts/data/party-token/static/raffle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/nfts/data/party-token/static/winner.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/nfts/deployments/party-ticket/hekla.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"TaikoPartyTicket": "0x166fbdF89A3bFE470cA2BA5Bd4a7058Bb93b3595"
}
3 changes: 2 additions & 1 deletion packages/nfts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"tbzb:deploy:hekla": "forge clean && pnpm compile && forge script script/trailblazers-badges/sol/Deploy.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200",
"galxe:deploy:mainnet": "forge clean && pnpm compile && forge script script/galxe/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --legacy --with-gas-price 1",
"tbzb:deploy:mainnet": "forge clean && pnpm compile && forge script script/trailblazers-badges/sol/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --legacy --with-gas-price 13 ",
"taikoon:deploy:v2": "forge clean && pnpm compile && forge script script/taikoon/sol/UpgradeV2.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast"
"taikoon:deploy:v2": "forge clean && pnpm compile && forge script script/taikoon/sol/UpgradeV2.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast",
"kbw:deploy:hekla": "forge clean && pnpm compile && forge script script/party-ticket/sol/Deploy.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200"
},
"devDependencies": {
"@types/node": "^20.11.30",
Expand Down
60 changes: 60 additions & 0 deletions packages/nfts/script/party-ticket/sol/Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import { UtilsScript } from "./Utils.s.sol";
import { Script, console } from "forge-std/src/Script.sol";
import { Merkle } from "murky/Merkle.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { TaikoPartyTicket } from "../../../contracts/party-ticket/TaikoPartyTicket.sol";
import { IMinimalBlacklist } from "@taiko/blacklist/IMinimalBlacklist.sol";

contract DeployScript is Script {
UtilsScript public utils;
string public jsonLocation;
uint256 public deployerPrivateKey;
address public deployerAddress;

// Hardhat Testnet Values
string baseURI =
"https://taikonfts.4everland.link/ipfs/bafybeighqzbsghqsnlo2ksf2afvbhyym6xde7cdoz2nri2xcoctuy7rya4";
IMinimalBlacklist blacklist = IMinimalBlacklist(0xe61E9034b5633977eC98E302b33e321e8140F105);

uint256 mintFee = 0.03 ether;
address payoutWallet = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;

function setUp() public {
utils = new UtilsScript();
utils.setUp();

jsonLocation = utils.getContractJsonLocation();
deployerPrivateKey = utils.getPrivateKey();
deployerAddress = utils.getAddress();
}

function run() public {
string memory jsonRoot = "root";

vm.startBroadcast(deployerPrivateKey);

// deploy token with empty root
address impl = address(new TaikoPartyTicket());
address proxy = address(
new ERC1967Proxy(
impl,
abi.encodeCall(
TaikoPartyTicket.initialize, (payoutWallet, mintFee, baseURI, blacklist)
)
)
);

TaikoPartyTicket token = TaikoPartyTicket(proxy);

console.log("Token Base URI:", baseURI);
console.log("Deployed TaikoPartyTicket to:", address(token));

string memory finalJson = vm.serializeAddress(jsonRoot, "TaikoPartyTicket", address(token));
vm.writeJson(finalJson, jsonLocation);

vm.stopBroadcast();
}
}
Loading

0 comments on commit 99ec5cb

Please sign in to comment.