Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
759ad4f
feat: add BatchMintNFT contract with upgradeable functionality and de…
Douglasacost Dec 28, 2025
e432624
chore: update cspell configuration to include additional reserved names
Douglasacost Dec 28, 2025
ff51641
feat: implement BridgeL2 contract and related event handling
Douglasacost Dec 28, 2025
3973f7e
fix: update L2Bridge datasource with correct startBlock and address
Douglasacost Dec 28, 2025
18027bf
feat: implement multi-chain support for NODL Bridge
Douglasacost Dec 30, 2025
c2197cd
feat: enhance testing framework and improve Docker configuration
Douglasacost Dec 30, 2025
f23ada6
refactor: streamline bridge mapping logic and enhance error handling
Douglasacost Dec 30, 2025
e3ddcab
refactor: update bridge mapping logic and enhance GraphQL schema
Douglasacost Jan 5, 2026
15728b6
refactor: update Docker Compose commands for consistency and remove u…
Douglasacost Jan 5, 2026
0b2f193
refactor: simplify Docker Compose configuration by removing unused op…
Douglasacost Jan 5, 2026
2b0f2dd
chore: add block confirmations option to Docker Compose configuration…
Douglasacost Jan 5, 2026
ec0876d
refactor: simplify GraphQL schema and bridge mapping logic
Douglasacost Jan 8, 2026
65a0940
refactor: enhance RPC request handling with rate limit detection and …
Douglasacost Jan 8, 2026
72d327f
refactor: enhance BridgeDeposit entity and mapping logic
Douglasacost Jan 9, 2026
b0d200b
Merge origin/main into douglasacost/bridge-indexing
aliXsed Jul 2, 2026
38a8776
fix(bridge-indexing): address review findings on PR #98
aliXsed Jul 3, 2026
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
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
"MRPC",
"girlnext",
"Checksummed",
"checksummed"
"checksummed",
"BMNFT"
]
}
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ subquery/project.yaml
subquery/mainnet*.yaml
subquery/testnet*.yaml

# Subquery-multichain files
subquery-multichain/dist/
subquery-multichain/.data/
subquery-multichain/**/types/
subquery-multichain/project.yaml
subquery-multichain/mainnet*.yaml
subquery-multichain/testnet*.yaml

# ZKSync files
artifacts-zk/
cache-zk/
Expand Down
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");
}
}
195 changes: 195 additions & 0 deletions src/contentsign/BatchMintNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// 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 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 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;
}
Loading
Loading