Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
"Frontends",
"testuser",
"testhandle",
"douglasacost"
"douglasacost",
"UUPS",
"BMNFT"
]
}
5 changes: 5 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ solc = "0.8.26"

# necessary as some of the zksync contracts are big
via_ir = true

remappings = [
"@openzeppelin/contracts-upgradeable/=lib/era-contracts/lib/openzeppelin-contracts-upgradeable-v4/contracts/",
"@openzeppelin/=lib/openzeppelin-contracts/"
]
3 changes: 2 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
@openzeppelin=lib/openzeppelin-contracts/
@openzeppelin=lib/openzeppelin-contracts/
@openzeppelin/contracts-upgradeable/=lib/era-contracts/lib/openzeppelin-contracts-upgradeable-v4/contracts/
63 changes: 63 additions & 0 deletions script/DeployBatchMintNFT.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear

pragma solidity ^0.8.26;

import {Script, console} from "forge-std/Script.sol";
import {BatchMintNFT} from "../src/contentsign/BatchMintNFT.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

/// @notice Deployment script for BatchMintNFT upgradeable contract
/// @dev Deploys implementation and proxy, then initializes the contract
contract DeployBatchMintNFT is Script {
string internal name;
string internal symbol;
address internal admin;

function setUp() public {
name = vm.envString("BATCH_MINT_NFT_NAME");
symbol = vm.envString("BATCH_MINT_NFT_SYMBOL");
admin = vm.envAddress("BATCH_MINT_NFT_ADMIN");

vm.label(admin, "ADMIN");
}

function run() public {
vm.startBroadcast();

// Deploy implementation
console.log("Deploying BatchMintNFT implementation...");
BatchMintNFT implementation = new BatchMintNFT();
console.log("Implementation deployed at:", address(implementation));

// Encode initialize function call
bytes memory initData = abi.encodeWithSelector(
BatchMintNFT.initialize.selector,
name,
symbol,
admin
);

// Deploy proxy with initialization
console.log("Deploying ERC1967Proxy...");
ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData);
console.log("Proxy deployed at:", address(proxy));

// Attach to proxy to get the initialized contract
BatchMintNFT nft = BatchMintNFT(address(proxy));

vm.stopBroadcast();

// Verify deployment
console.log("\n=== Deployment Summary ===");
console.log("Implementation address:", address(implementation));
console.log("Proxy address:", address(proxy));
console.log("Contract name:", nft.name());
console.log("Contract symbol:", nft.symbol());
console.log("Admin address:", admin);
console.log("Next token ID:", nft.nextTokenId());
console.log("Max batch size:", nft.MAX_BATCH_SIZE());
console.log("\nContract is ready for use!");
console.log("Users can now call safeMint() and batchSafeMint() publicly.");
console.log("Only admin can upgrade the contract using upgradeTo().");
}
}
48 changes: 48 additions & 0 deletions script/UpgradeBatchMintNFT.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear

pragma solidity ^0.8.26;

import {Script, console} from "forge-std/Script.sol";
import {BatchMintNFT} from "../src/contentsign/BatchMintNFT.sol";

/// @notice Script to upgrade BatchMintNFT proxy to a new implementation
/// @dev Only admin can execute this upgrade
contract UpgradeBatchMintNFT is Script {
address internal proxy;
address internal newImplementation;

function setUp() public {
proxy = vm.envAddress("BATCH_MINT_NFT_PROXY");
newImplementation = vm.envAddress("BATCH_MINT_NFT_NEW_IMPL");
}

function run() public {
vm.startBroadcast();

console.log("Current proxy address:", proxy);
console.log("New implementation address:", newImplementation);

// Attach to proxy
BatchMintNFT nft = BatchMintNFT(proxy);

// Verify current state before upgrade
console.log("Current nextTokenId:", nft.nextTokenId());
console.log("Current mintingEnabled:", nft.mintingEnabled());

// Perform upgrade
console.log("\nPerforming upgrade...");
nft.upgradeTo(newImplementation);

vm.stopBroadcast();

// Verify upgrade
console.log("\n=== Upgrade Summary ===");
console.log("Proxy address:", proxy);
console.log("New implementation:", newImplementation);
console.log("Upgrade completed successfully!");
console.log("\nVerify that:");
console.log("1. Data is preserved (tokens, balances, etc.)");
console.log("2. New methods are available");
console.log("3. Existing methods still work");
}
}
205 changes: 205 additions & 0 deletions src/contentsign/BatchMintNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear

pragma solidity ^0.8.26;

import {ERC721Upgradeable} from "openzeppelin-contracts-upgradeable-v4/contracts/token/ERC721/ERC721Upgradeable.sol";
import {ERC721URIStorageUpgradeable} from "openzeppelin-contracts-upgradeable-v4/contracts/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
import {ERC721BurnableUpgradeable} from "openzeppelin-contracts-upgradeable-v4/contracts/token/ERC721/extensions/ERC721BurnableUpgradeable.sol";
import {AccessControlUpgradeable} from "openzeppelin-contracts-upgradeable-v4/contracts/access/AccessControlUpgradeable.sol";
import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable-v4/contracts/proxy/utils/UUPSUpgradeable.sol";

/// @notice An upgradeable ERC-721 contract that allows public batch minting
/// @dev Uses AccessControl for administrative functions and UUPS for upgradeability
/// @dev Unlike BaseContentSign which uses whitelist-based minting, this contract allows
/// anyone to mint tokens publicly. AccessControl is only used for upgrade authorization.
contract BatchMintNFT is
ERC721Upgradeable,
ERC721URIStorageUpgradeable,
ERC721BurnableUpgradeable,
AccessControlUpgradeable,
UUPSUpgradeable
{
/// @notice The next token ID to be minted
uint256 public nextTokenId;

/// @notice Maximum batch size to prevent DoS attacks
uint256 public constant MAX_BATCH_SIZE = 100;

/// @notice Whether minting is currently enabled
bool public mintingEnabled;

/// @notice Role identifier for minters (reserved for future use if minting restrictions are needed)
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

/// @notice Error thrown when arrays length mismatch in batch operations
error UnequalLengths();
/// @notice Error thrown when zero address is provided
error ZeroAddress();
/// @notice Error thrown when URI is empty
error EmptyURI();
/// @notice Error thrown when batch size exceeds maximum
error BatchTooLarge();
/// @notice Error thrown when minting is disabled
error MintingDisabled();

/// @notice Emitted when multiple tokens are minted in a batch
/// @param recipients Array of addresses that received tokens
/// @param tokenIds Array of token IDs that were minted
event BatchMinted(address[] recipients, uint256[] tokenIds);

/// @notice Emitted when minting is enabled or disabled
/// @param enabled Whether minting is enabled
event MintingEnabledChanged(bool indexed enabled);

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

/// @notice Initialize the contract
/// @param name The name of the NFT collection
/// @param symbol The symbol of the NFT collection
/// @param admin The address that will have DEFAULT_ADMIN_ROLE
function initialize(string memory name, string memory symbol, address admin) public initializer {
if (admin == address(0)) {
revert ZeroAddress();
}

__ERC721_init(name, symbol);
__ERC721URIStorage_init();
__ERC721Burnable_init();
__AccessControl_init();
__UUPSUpgradeable_init();

_grantRole(DEFAULT_ADMIN_ROLE, admin);
mintingEnabled = true;
}

/// @notice Mint a single NFT to an address
/// @param to The address to mint the NFT to
/// @param uri The URI for the token metadata
function safeMint(address to, string memory uri) public {
if (!mintingEnabled) {
revert MintingDisabled();
}
if (to == address(0)) {
revert ZeroAddress();
}
if (bytes(uri).length == 0) {
revert EmptyURI();
}

uint256 tokenId = nextTokenId;
unchecked {
++nextTokenId;
}
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
}

/// @notice Batch mint NFTs to multiple addresses
/// @param recipients Array of addresses to mint NFTs to
/// @param uris Array of URIs for token metadata (must match recipients length)
function batchSafeMint(address[] calldata recipients, string[] calldata uris) public {
if (!mintingEnabled) {
revert MintingDisabled();
}
if (recipients.length != uris.length) {
revert UnequalLengths();
}
if (recipients.length > MAX_BATCH_SIZE) {
revert BatchTooLarge();
}

uint256 currentTokenId = nextTokenId;
uint256 length = recipients.length;

// Pre-allocate arrays for event emission
uint256[] memory tokenIds = new uint256[](length);

for (uint256 i = 0; i < length; ) {
if (recipients[i] == address(0)) {
revert ZeroAddress();
}
if (bytes(uris[i]).length == 0) {
revert EmptyURI();
}

_safeMint(recipients[i], currentTokenId);
_setTokenURI(currentTokenId, uris[i]);
tokenIds[i] = currentTokenId;
unchecked {
++currentTokenId;
++i;
}
}

nextTokenId = currentTokenId;

// Emit batch event
emit BatchMinted(recipients, tokenIds);
}

/// @notice Get the URI for a specific token
/// @param tokenId The token ID to query
/// @return The URI string for the token metadata
function tokenURI(uint256 tokenId)
public
view
override(ERC721Upgradeable, ERC721URIStorageUpgradeable)
returns (string memory)
{
return super.tokenURI(tokenId);
}

/// @notice Check interface support
/// @param interfaceId The interface identifier to check
/// @return Whether the contract supports the interface
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721Upgradeable, ERC721URIStorageUpgradeable, AccessControlUpgradeable)
returns (bool)
{
return
ERC721Upgradeable.supportsInterface(interfaceId) ||
ERC721URIStorageUpgradeable.supportsInterface(interfaceId) ||
AccessControlUpgradeable.supportsInterface(interfaceId);
}

/// @notice Enable or disable minting (only admin can change)
/// @param enabled Whether to enable minting
function setMintingEnabled(bool enabled) public onlyRole(DEFAULT_ADMIN_ROLE) {
mintingEnabled = enabled;
emit MintingEnabledChanged(enabled);
}

/// @notice Authorize upgrades (only admin can upgrade)
/// @param newImplementation The address of the new implementation contract
function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {
// Access control is handled by the onlyRole modifier
// This function intentionally left empty as the authorization is done via modifier
newImplementation; // Silence unused parameter warning
}

/// @notice Burn a token (only owner or approved operator can burn)
/// @param tokenId The token ID to burn
/// @dev The caller must own the token or be an approved operator
function burn(uint256 tokenId) public override(ERC721BurnableUpgradeable) {
super.burn(tokenId);
}

/// @notice Internal burn function (required override for ERC721URIStorage)
/// @param tokenId The token ID to burn
function _burn(uint256 tokenId) internal override(ERC721Upgradeable, ERC721URIStorageUpgradeable) {
super._burn(tokenId);
}

/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* Note: Reduced by 1 slot to account for mintingEnabled (bool)
*/
uint256[49] private __gap;
}
28 changes: 28 additions & 0 deletions subquery/mainnet-complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,34 @@ const project: EthereumProject = {
],
},
},
{
kind: EthereumDatasourceKind.Runtime,
startBlock: 67286803, // This is the block that the contract was deployed on
options: {
abi: "ClickContentSign",
address: "0xaF4D027599D1d74844505d1Cb029be0e8EEd31bF",
},
assets: new Map([
[
"ClickContentSign",
{
file: "./abis/ClickContentSign.abi.json",
},
],
]),
mapping: {
file: "./dist/index.js",
handlers: [
{
kind: EthereumHandlerKind.Event,
handler: "handleTransfer",
filter: {
topics: ["Transfer(address from,address to,uint256 tokenId)"],
},
},
],
},
},
{
kind: EthereumDatasourceKind.Runtime,
startBlock: 51533910, // This is the block that the contract was deployed on
Expand Down
1 change: 1 addition & 0 deletions subquery/src/utils/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export const nodleContracts = [
"0x9Fed2d216DBE36928613812400Fd1B812f118438",
"0x999368030Ba79898E83EaAE0E49E89B7f6410940",
"0x6FE81f2fDE5775355962B7F3CC9b0E1c83970E15", // vivendi
"0xaF4D027599D1d74844505d1Cb029be0e8EEd31bF", // BatchMintNFT proxy
].map((address) => address.toLowerCase());

export const contractForSnapshot = [
Expand Down
Loading