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): kbw party ticket smart contracts #17808

Merged
merged 32 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
70c03b3
contract and basic tests
bearni95 Jul 17, 2024
06b0607
fixed comment typo
bearni95 Jul 17, 2024
287c4f1
linted and formatted
bearni95 Jul 30, 2024
6d6a6b7
deployment scripts
bearni95 Jul 30, 2024
e1aa36d
cleanup
bearni95 Jul 30, 2024
68fedc1
reverted changes to taikoon data
bearni95 Jul 30, 2024
fed511c
change revert cleanup
bearni95 Jul 30, 2024
cbfe8ee
Merge branch 'main' into kbw-contracts-setup
bearni95 Jul 30, 2024
1e101bb
Update packages/nfts/contracts/party-ticket/TaikoPartyTicket.sol
bearni95 Jul 30, 2024
5d92631
removal of tokenId from tokenURI logic, beyond determining the winner
bearni95 Jul 30, 2024
ba7845a
Merge branch 'main' into kbw-contracts-setup
bearni95 Jul 30, 2024
61f729e
added mintWinner method
bearni95 Jul 30, 2024
96d7309
Merge branch 'main' into kbw-contracts-setup
bearni95 Jul 31, 2024
3e4f009
Deployment updates for taiko-party-ticket
bearni95 Aug 7, 2024
5dc4952
Merge branch 'kbw-contracts-setup' of ssh://github.com/taikoxyz/taiko…
bearni95 Aug 7, 2024
bfaa57f
modifications for single-dir ipfs data root
bearni95 Aug 7, 2024
00d48a2
proper values for ipfs hekla testing
bearni95 Aug 7, 2024
4e78bc4
dev push
bearni95 Aug 7, 2024
7f47c85
uncommented dev lines
bearni95 Aug 7, 2024
a3aef0e
proper hekla deployment
bearni95 Aug 7, 2024
7d057a9
reverted error changes
bearni95 Aug 7, 2024
6f6f51d
patched up tests
bearni95 Aug 7, 2024
d7049cf
gap allocation fix; blacklist modifier
bearni95 Aug 8, 2024
3dcf0e2
added revokeWinners method
bearni95 Aug 8, 2024
5f7f4c1
modified code and added tests for upgradeability
bearni95 Aug 8, 2024
7d405f2
Merge branch 'main' into kbw-contracts-setup
bearni95 Aug 8, 2024
1ee33de
upgrade test confirmations
bearni95 Aug 8, 2024
997174e
Merge branch 'kbw-contracts-setup' of ssh://github.com/taikoxyz/taiko…
bearni95 Aug 8, 2024
c535e82
a typo...
bearni95 Aug 8, 2024
1ceacc3
uncommented blacklist modifier
bearni95 Aug 8, 2024
c660131
updated ipfs data with winner image
bearni95 Aug 8, 2024
6283bfc
Merge branch 'main' into kbw-contracts-setup
bearni95 Aug 9, 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
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
bearni95 marked this conversation as resolved.
Show resolved Hide resolved
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) {
bearni95 marked this conversation as resolved.
Show resolved Hide resolved
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) {
dantaik marked this conversation as resolved.
Show resolved Hide resolved
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