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 8 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
212 changes: 212 additions & 0 deletions packages/nfts/contracts/party-ticket/TaikoPartyTicket.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// 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";

/// @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,
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 Gap for upgrade safety
uint256[47] private __gap;
bearni95 marked this conversation as resolved.
Show resolved Hide resolved

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");

mintFee = _mintFee;
baseURI = _baseURI;
payoutAddress = _payoutAddress;
blacklist = _blacklistAddress;

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

/// @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(winnerBaseURI, Strings.toString(tokenId)));
}
return string(abi.encodePacked(baseURI, Strings.toString(tokenId)));
}

/// @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 and update the metadata
/// @param _winners The list of winners
/// @param _winnerBaseURI Base URI for the winners tokens
/// @param _loserBaseURI Base URI for the losers tokens
function setWinners(
uint256[] calldata _winners,
string memory _winnerBaseURI,
string memory _loserBaseURI
)
external
whenNotPaused
onlyRole(DEFAULT_ADMIN_ROLE)
{
winnerBaseURI = _winnerBaseURI;
baseURI = _loserBaseURI;
for (uint256 i = 0; i < _winners.length; i++) {
winners[_winners[i]] = true;
}
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 {
if (blacklist.isBlacklisted(_msgSender())) revert ADDRESS_BLACKLISTED();
if (msg.value < mintFee) revert INSUFFICIENT_MINT_FEE();
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) {
if (blacklist.isBlacklisted(to)) revert ADDRESS_BLACKLISTED();
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
}

/// @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[newWinnerId]) revert CANNOT_REVOKE_NON_WINNER();
bearni95 marked this conversation as resolved.
Show resolved Hide resolved
winners[revokeId] = false;
winners[newWinnerId] = true;
}

/// @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 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);
}
}
63 changes: 63 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,63 @@
// 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
address owner = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
string baseURI =
"https://taikonfts.4everland.link/ipfs/bafybeierqzehlrqeqqeb6fwmil4dj3ij2p6exgoj4lysl53fsxwob6wbdy";
IMinimalBlacklist blacklist = IMinimalBlacklist(0xe61E9034b5633977eC98E302b33e321e8140F105);

uint256 mintFee = 0.1 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";

require(owner != address(0), "Owner must be specified");

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();
}
}
78 changes: 78 additions & 0 deletions packages/nfts/script/party-ticket/sol/Utils.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import { Script, console } from "forge-std/src/Script.sol";
import "forge-std/src/StdJson.sol";
import { IMinimalBlacklist } from "@taiko/blacklist/IMinimalBlacklist.sol";
import { MockBlacklist } from "../../../test/util/Blacklist.sol";

contract UtilsScript is Script {
using stdJson for string;

address public nounsTokenAddress;

uint256 public chainId;

string public lowercaseNetworkKey;
string public uppercaseNetworkKey;

function setUp() public {
// load all network configs
chainId = block.chainid;

if (chainId == 31_337) {
lowercaseNetworkKey = "localhost";
uppercaseNetworkKey = "LOCALHOST";
} else if (chainId == 17_000) {
lowercaseNetworkKey = "holesky";
uppercaseNetworkKey = "HOLESKY";
} else if (chainId == 167_001) {
lowercaseNetworkKey = "devnet";
uppercaseNetworkKey = "DEVNET";
} else if (chainId == 11_155_111) {
lowercaseNetworkKey = "sepolia";
uppercaseNetworkKey = "SEPOLIA";
} else if (chainId == 167_008) {
lowercaseNetworkKey = "katla";
uppercaseNetworkKey = "KATLA";
} else if (chainId == 167_000) {
lowercaseNetworkKey = "mainnet";
uppercaseNetworkKey = "MAINNET";
} else if (chainId == 167_009) {
lowercaseNetworkKey = "hekla";
uppercaseNetworkKey = "HEKLA";
} else {
revert("Unsupported chainId");
}
}

function getPrivateKey() public view returns (uint256) {
string memory lookupKey = string.concat(uppercaseNetworkKey, "_PRIVATE_KEY");
return vm.envUint(lookupKey);
}

function getAddress() public view returns (address) {
string memory lookupKey = string.concat(uppercaseNetworkKey, "_ADDRESS");
return vm.envAddress(lookupKey);
}

function getContractJsonLocation() public view returns (string memory) {
string memory root = vm.projectRoot();
return
string.concat(root, "/deployments/trailblazers-badges/", lowercaseNetworkKey, ".json");
}

function getBlacklist() public view returns (IMinimalBlacklist blacklistAddress) {
if (block.chainid == 167_000) {
// mainnet blacklist address
blacklistAddress = IMinimalBlacklist(vm.envAddress("BLACKLIST_ADDRESS"));
} else {
// deploy a mock blacklist otherwise
blacklistAddress = IMinimalBlacklist(0xbdEd0D2bf404bdcBa897a74E6657f1f12e5C6fb6);
}

return blacklistAddress;
}

function run() public { }
}
Loading