diff --git a/packages/hardhat/contracts/BatchGraduationNFT.sol b/packages/hardhat/contracts/BatchGraduationNFT.sol
new file mode 100644
index 0000000..c349c6a
--- /dev/null
+++ b/packages/hardhat/contracts/BatchGraduationNFT.sol
@@ -0,0 +1,97 @@
+//SPDX-License-Identifier: MIT
+pragma solidity >=0.8.0 <0.9.0;
+
+import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
+import "@openzeppelin/contracts/utils/Counters.sol";
+import "@openzeppelin/contracts/utils/Strings.sol";
+import "@openzeppelin/contracts/utils/Base64.sol";
+
+interface IBatchRegistry {
+ function BATCH_NUMBER() external view returns (uint16);
+}
+
+interface IGraduateNFTMetadata {
+ function getName() external view returns (string memory);
+ function getColor() external view returns (uint8, uint8, uint8);
+}
+
+contract BatchGraduationNFT is ERC721 {
+ IBatchRegistry public batchRegistry;
+
+ using Counters for Counters.Counter;
+ Counters.Counter private _tokenIds;
+
+ mapping(address => address) public yourGraduationContractAddress;
+ mapping(uint256 => address) public tokenToMetadataContract;
+
+ event MetadataSet(address builder, address metadataContract);
+
+ // Errors
+ error NoMetadataSet();
+ error UnauthorizedCaller();
+
+ constructor(address _batchRegistry) ERC721("BatchGraduate", "BGRAD") {
+ batchRegistry = IBatchRegistry(_batchRegistry);
+ }
+
+ modifier callerIsBatchRegistry() {
+ if (msg.sender != address(batchRegistry)) revert UnauthorizedCaller();
+ _;
+ }
+
+ function setMetadataContract(address metadataContract) public {
+ // Verify that the contract has the required methods
+ IGraduateNFTMetadata(metadataContract).getName();
+ IGraduateNFTMetadata(metadataContract).getColor();
+
+ yourGraduationContractAddress[msg.sender] = metadataContract;
+ emit MetadataSet(msg.sender, metadataContract);
+ }
+
+ function mint(address builder) public callerIsBatchRegistry returns (uint256) {
+ if (yourGraduationContractAddress[builder] == address(0)) revert NoMetadataSet();
+
+ _tokenIds.increment();
+ uint256 newTokenId = _tokenIds.current();
+ _safeMint(builder, newTokenId);
+
+ tokenToMetadataContract[newTokenId] = yourGraduationContractAddress[builder];
+ return newTokenId;
+ }
+
+ function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
+ require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
+
+ address metadataContract = tokenToMetadataContract[tokenId];
+ IGraduateNFTMetadata metadata = IGraduateNFTMetadata(metadataContract);
+
+ string memory name = metadata.getName();
+ (uint8 r, uint8 g, uint8 b) = metadata.getColor();
+
+ string memory svg = generateSVG(name, r, g, b);
+ string memory json = Base64.encode(
+ bytes(string(
+ abi.encodePacked(
+ '{"name": "', name, ' Graduate", ',
+ '"description": "Batch', Strings.toString(batchRegistry.BATCH_NUMBER()), ' Graduation NFT", ',
+ '"image": "data:image/svg+xml;base64,', Base64.encode(bytes(svg)), '"}'
+ )
+ ))
+ );
+
+ return string(abi.encodePacked("data:application/json;base64,", json));
+ }
+
+ function generateSVG(string memory name, uint8 r, uint8 g, uint8 b) internal view returns (string memory) {
+ return string(abi.encodePacked(
+ ''
+ ));
+ }
+}
diff --git a/packages/hardhat/contracts/BatchRegistry.sol b/packages/hardhat/contracts/BatchRegistry.sol
index eb1971f..ee22122 100644
--- a/packages/hardhat/contracts/BatchRegistry.sol
+++ b/packages/hardhat/contracts/BatchRegistry.sol
@@ -2,13 +2,17 @@
pragma solidity >=0.8.0 <0.9.0;
import "@openzeppelin/contracts/access/Ownable.sol";
+import "./BatchGraduationNFT.sol";
contract BatchRegistry is Ownable {
+ uint16 public immutable BATCH_NUMBER;
uint256 constant CHECK_IN_REWARD = 0.01 ether;
+ BatchGraduationNFT public batchGraduationNFT;
mapping(address => bool) public allowList;
mapping(address => address) public yourContractAddress;
+ mapping(address => uint256) public graduatedTokenId;
bool public isOpen = true;
uint256 public checkedInCounter;
@@ -18,7 +22,8 @@ contract BatchRegistry is Ownable {
error BatchNotOpen();
error NotAContract();
error NotInAllowList();
-
+ error AlreadyGraduated();
+ error NotCheckedIn();
modifier batchIsOpen() {
if (!isOpen) revert BatchNotOpen();
@@ -30,8 +35,10 @@ contract BatchRegistry is Ownable {
_;
}
- constructor(address initialOwner) {
+ constructor(address initialOwner, uint16 batchNumber) {
super.transferOwnership(initialOwner);
+ batchGraduationNFT = new BatchGraduationNFT(address(this));
+ BATCH_NUMBER = batchNumber;
}
function updateAllowList(address[] calldata builders, bool[] calldata statuses) public onlyOwner {
@@ -61,6 +68,14 @@ contract BatchRegistry is Ownable {
emit CheckedIn(wasFirstTime, tx.origin, msg.sender);
}
+ function graduate() public {
+ if (graduatedTokenId[msg.sender] != 0) revert AlreadyGraduated();
+ if (yourContractAddress[msg.sender] == address(0)) revert NotCheckedIn();
+
+ uint256 newTokenId = batchGraduationNFT.mint(msg.sender);
+ graduatedTokenId[msg.sender] = newTokenId;
+ }
+
// Withdraw function for admins in case some builders don't end up checking in
function withdraw() public onlyOwner {
(bool success, ) = payable(owner()).call{value: address(this).balance}("");
diff --git a/packages/hardhat/deploy/00_deploy_your_contract.ts b/packages/hardhat/deploy/00_deploy_your_contract.ts
index 88a2f59..72eb987 100644
--- a/packages/hardhat/deploy/00_deploy_your_contract.ts
+++ b/packages/hardhat/deploy/00_deploy_your_contract.ts
@@ -2,6 +2,9 @@ import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { Contract } from "ethers";
+// Update with your Batch number
+const BATCH_NUMBER = "8";
+
/**
* Deploys a contract named "deployYourContract" using the deployer account and
* constructor arguments set to the deployer address
@@ -25,7 +28,7 @@ const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEn
await deploy("BatchRegistry", {
from: deployer,
// Contract constructor arguments
- args: [deployer],
+ args: [deployer, BATCH_NUMBER],
log: true,
// autoMine: can be passed to the deploy function to make the deployment process faster on local networks by
// automatically mining the contract deployment transaction. There is no effect on live networks.
@@ -34,7 +37,12 @@ const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEn
// Get the deployed contract to interact with it after deploying.
const batchRegistry = await hre.ethers.getContract("BatchRegistry", deployer);
- console.log("BatchRegistry deployed to:", await batchRegistry.getAddress());
+ console.log("\nBatchRegistry deployed to:", await batchRegistry.getAddress());
+ console.log("Remember to update the allow list!\n");
+
+ // The GraduationNFT contract is deployed on the BatchRegistry constructor.
+ const batchGraduationNFTAddress = await batchRegistry.batchGraduationNFT();
+ console.log("BatchGraduation NFT deployed to:", batchGraduationNFTAddress, "\n");
};
export default deployYourContract;
diff --git a/packages/nextjs/contracts/externalContracts.ts b/packages/nextjs/contracts/externalContracts.ts
index 7ab7d56..bdf69f9 100644
--- a/packages/nextjs/contracts/externalContracts.ts
+++ b/packages/nextjs/contracts/externalContracts.ts
@@ -233,6 +233,464 @@ const externalContracts = {
transferOwnership: "@openzeppelin/contracts/access/Ownable.sol",
},
},
+ BatchGraduationNFT: {
+ address: "0x0",
+ abi: [
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "_batchRegistry",
+ type: "address",
+ },
+ ],
+ stateMutability: "nonpayable",
+ type: "constructor",
+ },
+ {
+ inputs: [],
+ name: "NoMetadataSet",
+ type: "error",
+ },
+ {
+ inputs: [],
+ name: "UnauthorizedCaller",
+ type: "error",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ {
+ indexed: true,
+ internalType: "address",
+ name: "approved",
+ type: "address",
+ },
+ {
+ indexed: true,
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "Approval",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ {
+ indexed: true,
+ internalType: "address",
+ name: "operator",
+ type: "address",
+ },
+ {
+ indexed: false,
+ internalType: "bool",
+ name: "approved",
+ type: "bool",
+ },
+ ],
+ name: "ApprovalForAll",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: false,
+ internalType: "address",
+ name: "builder",
+ type: "address",
+ },
+ {
+ indexed: false,
+ internalType: "address",
+ name: "metadataContract",
+ type: "address",
+ },
+ ],
+ name: "MetadataSet",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: "address",
+ name: "from",
+ type: "address",
+ },
+ {
+ indexed: true,
+ internalType: "address",
+ name: "to",
+ type: "address",
+ },
+ {
+ indexed: true,
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "Transfer",
+ type: "event",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "to",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "approve",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ ],
+ name: "balanceOf",
+ outputs: [
+ {
+ internalType: "uint256",
+ name: "",
+ type: "uint256",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "batchRegistry",
+ outputs: [
+ {
+ internalType: "contract IBatchRegistry",
+ name: "",
+ type: "address",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "getApproved",
+ outputs: [
+ {
+ internalType: "address",
+ name: "",
+ type: "address",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ {
+ internalType: "address",
+ name: "operator",
+ type: "address",
+ },
+ ],
+ name: "isApprovedForAll",
+ outputs: [
+ {
+ internalType: "bool",
+ name: "",
+ type: "bool",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "builder",
+ type: "address",
+ },
+ ],
+ name: "mint",
+ outputs: [
+ {
+ internalType: "uint256",
+ name: "",
+ type: "uint256",
+ },
+ ],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "name",
+ outputs: [
+ {
+ internalType: "string",
+ name: "",
+ type: "string",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "ownerOf",
+ outputs: [
+ {
+ internalType: "address",
+ name: "",
+ type: "address",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "from",
+ type: "address",
+ },
+ {
+ internalType: "address",
+ name: "to",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "safeTransferFrom",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "from",
+ type: "address",
+ },
+ {
+ internalType: "address",
+ name: "to",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ {
+ internalType: "bytes",
+ name: "data",
+ type: "bytes",
+ },
+ ],
+ name: "safeTransferFrom",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "operator",
+ type: "address",
+ },
+ {
+ internalType: "bool",
+ name: "approved",
+ type: "bool",
+ },
+ ],
+ name: "setApprovalForAll",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "metadataContract",
+ type: "address",
+ },
+ ],
+ name: "setMetadataContract",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "bytes4",
+ name: "interfaceId",
+ type: "bytes4",
+ },
+ ],
+ name: "supportsInterface",
+ outputs: [
+ {
+ internalType: "bool",
+ name: "",
+ type: "bool",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "symbol",
+ outputs: [
+ {
+ internalType: "string",
+ name: "",
+ type: "string",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "",
+ type: "uint256",
+ },
+ ],
+ name: "tokenToMetadataContract",
+ outputs: [
+ {
+ internalType: "address",
+ name: "",
+ type: "address",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "tokenURI",
+ outputs: [
+ {
+ internalType: "string",
+ name: "",
+ type: "string",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "from",
+ type: "address",
+ },
+ {
+ internalType: "address",
+ name: "to",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "transferFrom",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "",
+ type: "address",
+ },
+ ],
+ name: "yourGraduationContractAddress",
+ outputs: [
+ {
+ internalType: "address",
+ name: "",
+ type: "address",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ ],
+ },
},
} as const;